歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

億級 Web 系統的容錯性建設實踐

三年多前,我在騰訊負責的活動運營系統,因為業務流量規模的數倍增長,系統出現了各種各樣的異常,當時,作為開發的我,7*24小時地沒日沒夜處理告警,周末和凌晨也經常上線,疲於奔命。後來,當時的老領導對我說:你不能總扮演一個“救火隊長”的角色, 要嘗試從系統整體層面思考產生問題的根本原因,然後推進解決。我幡然醒悟,“火”是永遠救不完的,讓系統能夠自動”滅火”,才是解決問題的正確方向。簡而言之,系統的異常不能總是依賴於“人”去恢復,讓系統本身具備“容錯”能力,才是根本解決之道。三年多過去了,我仍然負責著這個系統,而它也已經從一個日請求百萬級的小Web系統,逐步成長為一個高峰日請求達到8億規模的平台級系統,走過一段令人難忘的技術歷程。

容錯其實是系統健壯性的重要指標之一,而本文會主要聚焦於“容錯”能力的實踐,希望對做技術的同學有所啟發和幫助。

(備注:QQ會員活動運營平台,後面統一簡稱AMS)

一、 重試機制

最容易也最簡單被人想到的容錯方式,當然就是“失敗重試”,總而言之,簡單粗暴!簡單是指它的實現通常很簡單,粗暴則是指使用不當,很可能會帶來系統“雪崩”的風險,因為重試意味著對後端服務的雙倍請求。

1. 簡單重試

我們請求一個服務,如果服務請求失敗,則重試一次。假設,這個服務在常規狀態下是99.9%的成功率,因為某一次波動性的異常,成功率下跌到95%,那麼如果有重試機制,那麼成功率大概還能保持在99.75%。而簡單重試的缺陷也很明顯,如果服務真的出問題,很可能帶來雙倍流量,沖擊服務系統,有可能直接將服務沖垮。而在實際的真實業務場景,往往更嚴重,一個功能不可用,往往更容易引起用戶的“反復點擊”,反而制造更大規模的流量沖擊。比起服務的成功率比較低,系統直接被沖擊到“掛掉”的後果明顯更嚴重。

簡單重試,要使用在恰當的場景。或者,主動計算服務成功率,成功率過低,就直接不做重試行為,避免帶來過高的流量沖擊。

2. 主備服務自動切換

既然單一服務的重試,可能會給該帶來雙倍的流量沖擊,而最終導致更嚴重的後果,那麼我們不如將場景變為主備服務的自動重試或者切換。例如,我們搭建了兩套獲取openid的服務,如果服務A獲取失敗,則嘗試從服務B中獲取。因為重試的請求壓力是壓到了服務B上,服務A通常不會因為重試而產生雙倍的流量沖擊。

這種重試的機制,看似比較可用,而實際上也存在一些問題:

(1) 通常會存在“資源浪費”的問題。因為備份服務系統,很可能長期處於閒置狀態,只有在主服務異常的時候,它的資源才會被比較充分地使用。不過,如果對於核心的服務業務(例如核心數據、營收相關)進行類似的部署,雖然會增加一些機器成本和預算,但這個付出通常也是物有所值的。

(2) 觸發重試機制,對於用戶的請求來說,耗時必然增加。主服務請求失敗,然後再到備份服務請求,這個環節的請求耗時就至少翻倍增長,假設主服務出現連接(connect)超時,那麼耗時就更是大幅度增加。一個服務在正常狀態下,獲取數據也許只要50ms,而服務的超時時間通常會設置到500-1000ms,甚至更多,一旦出現超時重試的場景,請求耗時必然大幅度增長,很可能會比較嚴重地影響用戶體驗。

(3) 主備服務一起陷入異常。如果是因為流量過大問題導致主服務異常,那麼備份服務很可能也會承受不住這種級別的流量而掛掉。

重試的容錯機制,在AMS上有使用,但是相對比較少,因為我們認為主備服務,還是不足夠可靠。

二、 動態剔除或者恢復異常機器

在AMS裡,我們的後端涉及數以百計的各類服務,來支撐整個運營系統的正常運作。所有後端服務或者存儲,首先是部署為無狀態的方式提供服務(一個服務通常很多台機器),然後,通過公司內的一個公共的智能路由服務L5,納入到AMS中。(1) 所有服務與存儲,無狀態路由。這樣做的目的,主要是為了避免單點風險,就是避免某個服務節點掛了,導致整個服務就癱瘓了。實際上,即使像一些具有主備性質(主機器掛了,支持切換到備份機器)的接入服務,也是不夠可靠的,畢竟只有2台,它們都掛了的情況,還是可能發生的。我們後端的服務,通常都以一組機器的形式提供服務,彼此之間沒有狀態關系,支撐隨機分配請求。

(2) 支持平行擴容。遇到大流量場景,支持加機器擴容。

(3) 自動剔除異常機器。在我們的路由服務,發現某個服務的機器異常的時候(成功率低於50%),就會自動剔除該機器,後續,會發出試探性的請求,確認等它恢復正常之後,再重新加回到服務機器組。

例如,假如一組服務下擁有服務機器四台(ABCD),假設A機器的服務因為某種未知原因,完全不可用了,這個時候L5服務會主動將A機器自動從服務組裡剔除,只保留BCD三台機器對外提供服務。而在後續,假如A機器從異常中恢復了,那麼L5再主動將機器A加回來,最後,又變成ABCD四台機器對外提供服務。

在過去的3年裡,我們逐步將AMS內的服務,漸漸從寫死IP列表或者主備狀態的服務,全部升級和優化為L5模式的服務,慢慢實現了AMS後端服務的自我容錯能力。至少,我們已經比較少遇到,再因為某一台機器的軟件或者硬件故障,而不得不人工介入處理的情況。我們也慢慢地從疲於奔命地處理告警的苦難中,被解放出來。

三、 超時時間

1. 為服務和存儲設置合理的超時時間

調用任何一個服務或者存儲,一個合理的超時時間(超時時間,就是我們請求一個服務時,等待的最長時間),是非常重要的,而這一點往往比較容易被忽視。通常Web系統和後端服務的通信方式,是同步等待的模式。這種模式,它會帶來的問題比較多。對於服務端,影響比較大的一個問題,就是它會嚴重影響系統吞吐率。假設,我們一個服務的機器上,啟用了100個處理請求的worker,worker的超時時間設置為5秒,1個worker處理1個任務的平均處理耗時是100ms。那麼1個work在5秒鐘的時間裡,能夠處理50個用戶請求,然而,一旦網絡或者服務偶爾異常,響應超時,那麼在本次處理的後續整整5秒裡,它僅僅處理了1個等待超時的失敗任務。一旦比較大概率出現這類型的超時異常,系統的吞吐率就會大面積下降,有可能耗盡所有的worker(資源被占據,全部在等待狀態,直到5s超時才釋放),最終導致新的請求無worker可用,只能陷入異常狀態。

算上網絡通信和其他環節的耗時,用戶就等待了超過5s時間,最後卻獲得一個異常的結果,用戶的心情通常是崩潰的。

解決這個問題的方式,就是設置一個合理的超時時間。例如,回到上面的的例子,平均處理耗時是100ms,那麼我們不如將超時時間從5s下調到500ms。從直觀上看,它就解決了吞吐率下降和用戶等待過長的問題。然而,這樣做本身又比較容易帶來新的問題,就是會引起服務的成功率下降。因為平均耗時是100ms,但是,部分業務請求本身耗時比較長,耗時超過500ms也比較多。例如,某個請求服務端耗時600ms才處理完畢,然後這個時候,客戶端認為等待超過500ms,已經斷開了連接。處理耗時比較長的這類型業務請求會受到比較明顯的影響。

2. 超時時間設置過短帶來的成功率下降

超時時間設置過短,會將很多本來處理成功的請求,當做服務超時處理掉,進而引起服務成功率下降。將全部業務服務,以一刀切的方式設置一個超時時間,是比較不可取的。優化的方法,我們分為兩個方向。

(1) 快慢分離

根據實際的業務維度,區分對待地給各個業務服務配置不同的超時時間,同時,最好也將它們的部署服務也分離出來。例如,天天酷跑的查詢服務耗時通常為100ms,那麼超時時間我們就設置為1s,某新手游的查詢服務通常耗時為700ms,那麼我們就設置為5s。這樣的話,整體系統的成功率,就不會受到比較大的影響。

(2) 解決同步阻塞等待

“快慢分離”可以改善系統的同步等待問題,但是,對於某些耗時本來就比較長的服務而言,系統的進程/線程資源仍然在同步等待過程中,無法響應其他新的請求,只能阻塞等待,它的資源仍然是被占據,系統的整體吞吐率仍然被大幅度拉低。

解決的思路,當然是利用I/O多路復用,通過異步回調的方式,解決同步等待過程中的資源浪費。AMS的一些核心服務,采用的就是“協程”(又叫“微線程”,簡單的說,常規異步程序代碼裡嵌套比較多層的函數回調,編寫復雜。而協程則提供了一種類似寫同步代碼的方式,來寫異步回調程序),以解決同步等待的問題。異步處理的簡單描述,就是當進程遇到I/O網絡阻塞時,就保留現場,立刻切換去處理下一個業務請求,進程不會因為某個網絡等待而停止處理業務,進而,系統吞吐率即使遇到網絡等待時間過長的場景,通常都能保持在比較高的水平。

值得補充一點的是,異步處理只是解決系統的吞吐率問題,對於用戶的體驗問題,並不會有改善,用戶需要等待的時間並不會減少。

3. 防重入,防止重復發貨

前面我們提到,我們設置了一個比較“合理的超時時間”,簡而言之,就是一個比較短的超時時間。而在數據寫入的場景,會引起新的問題,就我們的AMS系統而言,就是發貨場景。如果是發貨請求超時,這個時候,我們需要思考的問題就比較多了。

(1) 發貨等待超時,發貨服務執行發貨失敗。這種場景,問題不大,後續用戶重新點擊領取按鈕,就可以觸發下一次重新發貨。

(2) 發貨等待超時,發貨服務實際在更晚的時候執行發貨成功,我們稱之為“超時成功”。比較麻煩的場景,則是每次都是發貨超時,而實際上都發貨成功,如果系統設計不當,有可能導致用戶可以無限領取禮包,最終造成活動運營事故。

第二種場景,給我們帶來了比較麻煩的問題,如果處理不當,用戶再次點擊,就觸發第多次“額外”發貨。

例如,我們假設某個發貨服務超時時間設置為6s,用戶點擊按鈕,我們的AMS收到請求後,請求發貨服務發貨,等待6s後,無響應,我們給用戶提示“領取失敗”,而實際上發貨服務卻在第8秒執行發貨成功,禮包到了用戶的賬戶上。而用戶看見“領取失敗”,則又再次點擊按鈕,最終導致“額外”多發一個禮包給到這個用戶。

例子的時序和流程圖大致如下:

這裡就提到了防重入,簡單的說,就是如何確認不管用戶點擊多少次這個領取按鈕,我們都確保結果只有一種預期結果,就是只會給用戶發一次禮包,而不引起重復發貨。我們的AMS活動運營平台一年上線的活動超過4000個,涉及數以萬計的各種類型、不同業務系統的禮包發貨,業務通信場景比較復雜。針對不同的業務場景,我們做了不同的解決方案:

(1) 業務層面限制,設置禮包單用戶限量。在發貨服務器的源頭,設置好一個用戶僅能最多獲得1個禮包,直接避免重復發放。但是,這種業務限制,並非每個業務場景都通用的,只限於內部具備該限制能力的業務發貨系統,並且,有一些禮包本身就可以多次領取的,就不適用了。

(2) 訂單號機制。用戶的每一次符合資格的發貨請求,都生成一個訂單號與之對應,通過它來確保1個訂單號,只發貨1次。這個方案雖然比較完善,但是,它是依賴於發貨服務方配合做“訂單號發貨狀態更新“的,而我們的發貨業務方眾多,並非每一個都能支持”訂單號更新“的場景。

(3) 自動重試的異步發貨模式。用戶點擊領取禮包按鈕後,Web端直接返回成功,並且提示禮包在30分鐘內到賬。對於後台,則將該發貨錄入到發貨隊列或者存儲中,等待發貨服務異步發貨。因為是異步處理,可以多次執行發貨重試操作,直到發貨成功為止。同時,異步發貨是可以設置一個比較長的超時等待時間,通常不會出現“超時成功”的場景,並且對於前端響應來說,不需要等待後台發貨狀態的返回。但是,這種模式,會給用戶帶來比較不好的體驗,就是沒有實時反饋,無法立刻告訴用戶,禮包是否到賬。

4. 非訂單號的特殊防刷機制

某些特殊的合作場景,我們無法使用雙方約定訂單號方式,例如一個完全隔離獨立的外部發貨接口,不能和我們做訂單號的約定。基於這種場景,我們AMS專門做了一種防刷的機制,就是通過限制read超時的次數。但是,這種方案並非完美解決重復發貨問題,只是能起到夠盡可能減少避免被刷的作用。一次網絡通信,通常包含:建立連接(connect),寫入數據發包(write),等待並且讀取回包(read),斷開連接(close)。

通常一個發貨服務如果出現異常,大多數情況,在connect步驟就是失敗或者超時,而如果一個請求走到等待回包(read)時超時,那麼發貨服務另外一邊就有可能發生了“超時但發貨成功”的場景。這個時候,我們將read超時的發生次數記錄起來,然後提供了一個配置限制次數的能力。假如設置為2次,那麼當一個用戶第一次領取禮包,遇到read超時,我們就允許它重試,當還遇到第二次read超時,就達到我們之前設置的閥值2,我們就認為它可能發貨成功,拒絕用戶的第三次領取請求。

這種做法,假設發貨服務真的出現很多超時成功,那麼用戶也最多只能刷到2次禮包(次數可配置),而避免發生禮包無限制被刷的場景。但是,這種方案並不完全可靠,謹慎使用。

在發貨場景,還會涉及分布式場景下的CAP(一致性、可用性、分區容錯性)問題,不過,我們的系統並非是一個電商服務,大部分的發貨並沒有強烈的一致性要求。因此,總體而言,我們是弱化了一致性問題(核心服務,通過異步重試的方式,達到最終一致性),以追求可用性和分區容錯性的保證。

四、 服務降級,自動屏蔽非核心分支異常

對於一次禮包領取請求,在我們的後端CGI會經過10多個環節和服務的邏輯判斷,包括禮包配置讀取、禮包限量檢查、登陸態校驗、安全保護等等。而這些服務中,就有不可以跳過的核心環節,例如讀取禮包配置的服務,也有非核心環節,例如數據上報。對於非核心環節,我們的做法,就是設置一個比較低的超時時間。例如我們其中一個統計上報服務,平均耗時是3ms,那麼我們就將超時時間設置為20ms,一旦超時則旁路掉,繼續按照正常邏輯走業務流程。

五、 服務解耦、物理隔離

雖然,大家都知道一個服務的設計,要盡可能小和分離部署,如此,服務之間的耦合會比較小,一旦某個模塊出問題,受到影響的模塊就比較少,容錯能力就會更強。可是,從設計之初,就將每一個服務有序的切割地很小,這個需要設計者具備超前的意識,能夠提前意識到業務和系統的發展形態,而實際上,業務的發展往往是比較難以預知的,因為業務的形態會隨著產品的策略的改變而變化。在業務早期流量比較小的時候,通常也沒有足夠的人力和資源,將服務細細的切分。AMS從日請求百萬級的Web系統,逐漸成長為億級,在這個過程中,流量規模增長了100倍,我們經歷了不少服務耦合帶來的陣痛。

1. 服務分離,大服務變成多個小服務

我們常常說,雞蛋不能都放在一個籃子裡。AMS以前是一個比較小的系統(日請求百萬級,在騰訊公司內完全是一個不起眼的小Web系統),因此,很多服務和存儲在早起都是部署在一起的,查詢和發貨服務都放在一起,不管哪一個出問題,都相互影響。後來,我們逐漸的將這些核心的服務和存儲,慢慢地分離出來,細細切分和重新部署。在數據存儲方面,我們將原來3-5個存儲的服務,慢慢地切為20多個獨立部署的存儲。

例如,2015年下半年,我們就將其中一個核心的存儲數據,從1個分離為3個。

這樣做帶來了很多好處:

(1) 原來主存儲的壓力被分流。

(2) 穩定性更高,不再是其中一個出問題,影響整個大的模塊。

(3) 存儲之間是彼此物理隔離的,即使服務器硬件故障,也不會相互影響。

2. 輕重分離,物理隔離

另外一方面,我們對於一些核心的業務,進行“輕重分離”。例如,我們支持2016年“手Q春節紅包”活動項目的服務集群。就將負責信息查詢和紅包禮包發貨的集群分別獨立部署,信息查詢的服務相對沒有那麼重要,業務流程比較輕量級,而紅包禮包發貨則屬於非常核心的業務,業務流程比較重。

輕重分離的這個部署方式,可以給我們帶來一些好處:

(1) 查詢集群即使出問題,也不會影響發貨集群,保證用戶核心功能正常。

(2) 兩邊的機器和部署的服務基本一致,在緊急的情況下,兩邊的集群可以相互支援和切換,起到容災的效果。

(3) 每個集群裡的機器,都是跨機房部署,例如,服務器都是分布在ABC三個機房,假設B機房整個網絡故障了,反向代理服務會將無法接受服務的B機房機器剔除,然後,剩下AC機房的服務器仍然可以正常為外界提供服務。

六、 業務層面的容錯

如果系統架構設計層面的“容錯”我們都搭建完善了,那麼再繼續下一層容錯,就需要根據實際的業務來進行,因為,不同的業務擁有不同的業務邏輯特性,也能夠導致業務層面的各種問題。而在業務層面的容錯,簡而言之,避免“人的失誤”。不管一個人做事性格多麼謹慎細心,也總有“手抖”的時候,在不經意間產生“失誤”。AMS是一個活動運營平台,一個月會上線400多個活動,涉及數以千計的活動配置信息(包括禮包、規則、活動參與邏輯等等)。在我們的業務場景下,因為種種原因而導致“人的失誤”並不少。

例如,某個運營同學看錯禮包發放的日限量,將原本只允許1天放量100個禮包的資源,錯誤地配置為每天放量200個。這種錯誤是測試同學比較難測試出來的,等到活動真正上線,禮包發放到101個的時候,就報錯了,因為資源池當天已經沒有資源了。雖然,我們的業務告警系統能夠快速捕獲到這個異常(每10分鐘為一個周期,從十多個維度,監控和計算各個活動的成功率、流量波動等等數據),但是,對於騰訊的用戶量級來說,即使只影響十多分鐘,也可以影響成千上萬的用戶,對於大規模流量的推廣活動,甚至可以影響數十萬用戶了。這樣的話,就很容易就造成嚴重的“現網事故”。

完善的監控系統能夠及時發現問題,防止影響面的進一步擴大和失控,但是,它並不能杜絕現網問題的發生。而真正的根治之法,當然是從起源的地方杜絕這種場景的出現,回到上面“日限量配置錯誤”的例子場景中,用戶在內部管理端發布活動配置時,就直接提示運營同學,這個配置規則是不對的。

在業界,因為配置參數錯誤而導致的現網重大事故的例子,可以說是多不勝數,“配置參數問題”幾乎可以說是一個業界難題,對於解決或者緩解這種錯誤的發生,並沒有放之四海而皆准的方法,更多的是需要根據具體業務和系統場景,亦步亦趨地逐步建設配套的檢查機制程序或者腳本。

因此,我們建設了一套強大並且智能的配置檢查系統,裡面集合了數十種業務的搭配檢查規則,並且檢查規則的數目一直都在增加。這裡規則包括檢查禮包日限量之類比較簡單的規則,也有檢查各種關聯配置參數、相對比較復雜的業務邏輯規則。

另外一方面,流程的執行不能通過“口頭約定”,也應該固化為平台程序的一部分,例如,活動上線之前,我們要求負責活動的同事需要驗證一下“禮包領取邏輯”,也就是真實的去領取一次禮包。然而,這只是一個“口頭約定”,實際上並不具備強制執行力,如果這位同事因為活動的禮包過多,而漏過其中一個禮包的驗證流程,這種事情也的確偶爾會發生,這個也算是“人的失誤”的另外一種場景。

為了解決問題,這個流程在我們AMS的內部管理端中,是通過程序去保證的,確保這位同事的QQ號碼的確領取過全部的禮包。做法其實挺簡單的,就是讓負責活動的同事設置一個驗證活動的QQ號碼,然後,程序在發貨活動時,程序會自動檢查每一個子活動項目中,是否有這個QQ號碼的活動參與記錄。如果都有參與記錄,則說明這位同事完整地領取了全部禮包。同時,其他模塊的驗證和測試,我們也都采用程序和平台來保證,而不是通過“口頭約定”。

通過程序和系統對業務邏輯和流程的保證,盡可能防止“人的失誤”。

這種業務配置檢查程序,除了可以減少問題的發生,實際上也減輕了測試和驗證活動的工作,可以起到節省人力的效果。不過,業務配置檢查規則的建設並不簡單,邏輯往往比較復雜,因為要防止誤殺。

七、 小結

無論是人還是機器,都是會產生“失誤”,只是對於單一個體,發生的概率通常並不大。但是,如果一個系統擁有數百台服務器,或者有一項工作有幾百人共同參與,這種“失誤“的概率就被大大提升,失誤很可能就變為一種常態了。機器的故障,盡可能讓系統本身去兼容和恢復,人的失誤,盡可能通過程序和系統流程來避免,都盡可能做到”不依賴於人“。容錯的核心價值,除了增強系統的健壯性外,我覺得是解放技術人員,盡可能讓我們不用凌晨起來處理告警,或享受一個相對平凡閒暇的周末。對於我們來說,要完全做到這點,還有很長的路要走,與君共勉。

Copyright © Linux教程網 All Rights Reserved