揭秘大眾點評賬號業(yè)務(wù)高可用的“三大法寶”
在任何一家互聯(lián)網(wǎng)公司,不管其主營業(yè)務(wù)是什么,都會有一套自己的賬號體系。
賬號既是公司所有業(yè)務(wù)發(fā)展留下的最寶貴資產(chǎn),它可以用來衡量業(yè)務(wù)指標(biāo),例如日活、月活、留存等。
同時它也給不同業(yè)務(wù)線提供了大量潛在用戶。業(yè)務(wù)可以基于賬號來做用戶畫像,制定各自的發(fā)展路徑。
因此,賬號服務(wù)的重要性不言而喻,同時美團業(yè)務(wù)飛速發(fā)展,對賬號業(yè)務(wù)的可用性要求也越來越高。
本文將從以下幾個方面分享一些我們在高可用探索中的實踐:
- 業(yè)務(wù)監(jiān)控
- 柔性可用
- 異地多活
- 總結(jié)
衡量一個系統(tǒng)的可用性有兩個指標(biāo):
- MTBF (Mean Time Between Failure),即平均多長時間不出故障。
- MTTR (Mean Time To Recovery),即出故障后的平均恢復(fù)時間。
通過這兩個指標(biāo)可以計算出可用性,也就是我們大家比較熟悉的“幾個 9”。
因此提升系統(tǒng)的可用性,就得從這兩個指標(biāo)入手,要么降低故障恢復(fù)的時間,要么延長不出故障的時間。
業(yè)務(wù)監(jiān)控
要降低故障恢復(fù)的時間,首先得盡早的發(fā)現(xiàn)故障,然后才能解決故障,這就需要依賴業(yè)務(wù)監(jiān)控系統(tǒng)。
業(yè)務(wù)監(jiān)控不同于其他監(jiān)控系統(tǒng),業(yè)務(wù)監(jiān)控關(guān)注的是各個業(yè)務(wù)指標(biāo)是否正常,比如賬號的登錄曲線。
大眾點評登錄入口有很多,從終端上分有 App、PC、M 站,從登錄類型上分有密碼登錄、快捷登錄、第三方登錄(微信/QQ/微博)、小程序登錄等。
需要監(jiān)控的維度有登錄總數(shù)、成功數(shù)、失敗分類、用戶地區(qū)、App 版本號、瀏覽器類型、登錄來源 Referer、服務(wù)所在機房等等。業(yè)務(wù)監(jiān)控最能從直觀上告訴我們系統(tǒng)的運行狀況。
由于業(yè)務(wù)監(jiān)控的維度很多很雜,有時還要增加新的監(jiān)控維度,并且告警分析需要頻繁聚合不同維度的數(shù)據(jù),因此我們采用 Elasticsearch 作為日志存儲。
整體架構(gòu)如下圖:
每條監(jiān)控都會根據(jù)過去的業(yè)務(wù)曲線計算出一條基線(見下圖),用來跟當(dāng)前數(shù)據(jù)做對比,超出設(shè)定的閾值后就會觸發(fā)告警。
每次收到告警,我們都要去找出背后的原因,如果是流量漲了,是有活動了還是被刷了?如果流量跌了,是日志延時了還是服務(wù)出問題了?
另外值得重視的是告警的頻次,如果告警太多就會稀釋大家的警惕性。我們曾經(jīng)踩過一次坑,因為告警太多就把告警關(guān)了,結(jié)果就在關(guān)告警的這段時間業(yè)務(wù)出問題了,我們沒有及時發(fā)現(xiàn)。
為了提高每條告警的定位速度,我們在每條告警后面加上維度分析。如下圖(非真實數(shù)據(jù)),告警里直接給出分析結(jié)果。
柔性可用
柔性可用的目的是延長不出故障的時間,當(dāng)業(yè)務(wù)依賴的下游服務(wù)出故障時不影響自身的核心功能或服務(wù)。
賬號對上層業(yè)務(wù)提供的鑒權(quán)和查詢服務(wù)即核心服務(wù),這些服務(wù)的 QPS 非常高,業(yè)務(wù)方對服務(wù)的可用性要求很高,別說是服務(wù)故障,就連任何一點抖動都是不能接受的。
對此我們先從整體架構(gòu)上把服務(wù)拆分,其次在服務(wù)內(nèi)對下游依賴做資源隔離,都盡可能的縮小故障發(fā)生時的影響范圍。
另外對非關(guān)鍵路徑上的服務(wù)故障做了降級。例如賬號的一個查詢服務(wù)依賴 Redis,當(dāng) Redis 抖動的時候服務(wù)的可用性也隨之降低。
我們通過公司內(nèi)部另外一套緩存中間件 Tair 來做 Redis 的備用存儲,當(dāng)檢測到 Redis 已經(jīng)非常不可用時就切到 Tair 上。
通過開源組件 Hystrix 或者我們公司自研的中間件 Rhino 就能非常方便地解決這類問題。
其原理是根據(jù)最近一個時間窗口內(nèi)的失敗率來預(yù)測下一個請求需不需要快速失敗,從而自動降級。
這些步驟都能在毫秒級完成,相比人工干預(yù)的情況提升幾個數(shù)量級,因此系統(tǒng)的可用性也會大幅提高。
下圖是優(yōu)化前后的對比圖,可以非常明顯的看到,系統(tǒng)的容錯能力提升了,TP999 也能控制在合理范圍內(nèi)。
對于關(guān)鍵路徑上的服務(wù)故障,我們可以減少影響的用戶數(shù)。比如手機快捷登錄流程里的某個關(guān)鍵服務(wù)掛了,我們可以在返回的失敗文案上做優(yōu)化。
并且在登錄入口掛小黃條提示,讓用戶主動去其他登錄途徑,這樣對于那些設(shè)置過密碼或者綁定了第三方的用戶還有其他選擇。
具體的做法是我們在每個登錄入口都關(guān)聯(lián)了一個計數(shù)器,一旦其中的關(guān)鍵節(jié)點不可用,就會在受影響的計數(shù)器上加 1,如果節(jié)點恢復(fù),則會減 1。
每個計數(shù)器還分別對應(yīng)一個標(biāo)志位,當(dāng)計數(shù)器大于 0 時,標(biāo)志位為 1,否則標(biāo)志位為 0。
我們可以根據(jù)當(dāng)前標(biāo)志位的值得知登錄入口的可用情況,從而在登錄頁展示不同的提示文案,這些提示文案一共有 2^5 = 32 種。
下圖是我們在做故障模擬時的降級提示文案:
異地多活
除了柔性可用,還有一種思路可以來延長不出故障的時間,那就是做冗余。冗余的越多,系統(tǒng)的故障率就越低,并且是呈指數(shù)級降低。
不管是機房故障,還是存儲故障,甚至是網(wǎng)絡(luò)故障,都能依賴冗余去解決。
比如數(shù)據(jù)庫可以通過增加從庫的方式做冗余,服務(wù)層可以通過分布式架構(gòu)做冗余。
但是冗余也會帶來新的問題,比如成本翻倍,復(fù)雜性增加,這就要衡量投入產(chǎn)出比。
目前美團的數(shù)據(jù)中心機房主要在北京上海,各個業(yè)務(wù)都直接或間接的依賴賬號服務(wù)。
盡管公司內(nèi)已有北上專線,但因為專線故障或抖動引發(fā)的賬號服務(wù)不可用,間接導(dǎo)致的業(yè)務(wù)損失也不容忽視,我們就開始考慮做跨城的異地冗余,即異地多活。
方案設(shè)計
首先我們調(diào)研了業(yè)界比較成熟的做法,主流思路是分 set 化,優(yōu)點是非常利于擴展,缺點是只能按一個維度劃分。
比如按用戶 ID 取模劃分 set,其他的像手機號和郵箱的維度就要做出妥協(xié),尤其是這些維度還有唯一性要求,這就使得數(shù)據(jù)同步或者修改都增加了復(fù)雜度,而且極易出錯,給后續(xù)維護帶來困難。
考慮到賬號讀多寫少的特性(讀寫比是 350:1),我們采用了一主多從的數(shù)據(jù)庫部署方案,優(yōu)先解決讀多活的問題。
Redis 如果也用一主多從的模式可行嗎?答案是不行,因為 Redis 主從同步機制會優(yōu)先嘗試增量同步。
當(dāng)增量同步不成功時,再去嘗試全量同步,一旦專線發(fā)生抖動就會把主庫拖垮,并進一步阻塞專線,形成“雪崩效應(yīng)”。
因此兩地的 Redis 只能是雙主模式,但是這種架構(gòu)有一個問題,就是我們得自己去解決數(shù)據(jù)同步的問題,除了保證數(shù)據(jù)不丟,還要保證數(shù)據(jù)一致。
另外從用戶進來的每一層路由都要是就近的,因此 DNS 需要開啟智能解析,SLB 要開啟同城策略,RPC 已默認(rèn)就近訪問。
總體上賬號的異地多活遵循以下三個原則:
- 北上任何一地故障,另一地都可提供完整服務(wù)。
- 北上兩地同時對外提供服務(wù),確保服務(wù)隨時可用。
- 兩地服務(wù)都遵循 BASE 原則,確保數(shù)據(jù)最終一致。
最終設(shè)計方案如下:
數(shù)據(jù)同步
首先要保證數(shù)據(jù)在傳輸?shù)倪^程中不能丟,因此需要一個可靠接收數(shù)據(jù)的地方,于是我們采用了公司內(nèi)部的 MQ 平臺 Mafka(類 Kafka)做數(shù)據(jù)中轉(zhuǎn)站。
可是消息在經(jīng)過 Mafka 傳遞之后可能是亂序的,這導(dǎo)致對同一個 key 的一串操作序列可能導(dǎo)致不一致的結(jié)果,這是不可忍受的。
但 Mafka 只是不保證全局有序,在單個 partition 內(nèi)卻是有序的,于是我們只要對每個 key 做一遍一致性散列算法對應(yīng)一個 partitionId,這樣就能保證每個 key 的操作是有序的。
但僅僅有序還不夠,兩地的并發(fā)寫仍然會造成數(shù)據(jù)的不一致。這里涉及到分布式數(shù)據(jù)的一致性問題,業(yè)界有兩種普遍的認(rèn)知,一種是 Paxos 協(xié)議,一種是 Raft 協(xié)議。
我們吸取了對實現(xiàn)更為友好的 Raft 協(xié)議,它主張有一個主節(jié)點,其余是從節(jié)點,并且在主節(jié)點不可用時,從節(jié)點可晉升為主節(jié)點。
簡單來說就是把這些節(jié)點排個序,當(dāng)寫入有沖突時,以排在最前面的那個節(jié)點為準(zhǔn),其余節(jié)點都去 follow 那個主節(jié)點的值。
在技術(shù)實現(xiàn)上,我們設(shè)計出一個版本號(見下圖),實際上是一個 long 型整數(shù),其中數(shù)據(jù)源大小即表示節(jié)點的順序,把版本號存入 value 里面。
當(dāng)兩個寫入發(fā)生沖突的時候只要比較這個版本號的大小即可,版本號大的覆蓋小的,這樣能保證寫沖突時的數(shù)據(jù)一致性。
寫并發(fā)時數(shù)據(jù)同步過程如下圖:
這種同步方式的好處顯而易見,可以適用于所有的 Redis 操作且能保證數(shù)據(jù)的最終一致性。
但這也有一些弊端,由于多存了版本號導(dǎo)致 Redis 存儲會增加,另外在該機制下兩地的數(shù)據(jù)其實是全量同步的。
這對于那些僅用做緩存的存儲來說是非常浪費資源的,因為緩存有數(shù)據(jù)庫可以回源。
而賬號服務(wù)幾乎一半的 Redis 存儲都是緩存,因此我們需要對緩存同步做優(yōu)化。
賬號服務(wù)的緩存加載與更新模式如下圖:
我們優(yōu)化的方向是在緩存加載時不同步,只有在數(shù)據(jù)庫有更新時才去同步。
但是數(shù)據(jù)更新這個流程里不能再使用 delete 操作,這樣做有可能使緩存出現(xiàn)臟數(shù)據(jù),比如下面這個例子:
我們對這個問題的解決辦法是用 set(若 key 不存在則添加,否則覆蓋)代替 delete。
而緩存的加載用 add(若 key 不存在則添加,否則不修改),這樣能保證緩存更新時的強一致性卻不需要增加額外存儲。
考慮到賬號修改的入口比較多,我們希望緩存更新的邏輯能拎出來單獨處理減少耦合。
***發(fā)現(xiàn)公司內(nèi)部數(shù)據(jù)同步組件 Databus 非常適用于該場景,其主要功能是把數(shù)據(jù)庫的變更日志以消息的形式發(fā)出來。
于是優(yōu)化后的緩存模式如下圖:
從理論變?yōu)楣こ虒崿F(xiàn)的時候還有些需要注意的地方,比如同步消息沒發(fā)出去、數(shù)據(jù)收到后寫失敗了。
因此我們還需要一個方法來檢測數(shù)據(jù)不一致的數(shù)量,為了做到這點,我們新建了一個定時任務(wù)去 scan 兩地的數(shù)據(jù)做對比統(tǒng)計,如果發(fā)現(xiàn)有不一致的還能及時修復(fù)掉。
項目上線后,我們也取得一些成果,首先性能提升非常明顯,異地的調(diào)用平均耗時和 TP99、TP999 均至少下降 80%。
并且在一次線上專線故障期間,賬號讀服務(wù)對外的可用性并沒有受影響,避免了更大范圍的損失。
總結(jié)
服務(wù)的高可用需要持續(xù)性的投入與維護,比如我們會每月做一次容災(zāi)演練。
高可用也不止體現(xiàn)在某一兩個重點項目上,更多的體現(xiàn)在每個業(yè)務(wù)開發(fā)同學(xué)的日常工作里。
任何一個小 Bug 都可能引起一次大的故障,讓你前期所有的努力都付之東流,因此我們的每一行代碼,每一個方案,每一次線上改動都應(yīng)該是仔細推敲過的。
高可用應(yīng)該成為一種思維方式。***希望我們能在服務(wù)高可用的道路上越走越遠。