官方自爆了!去年今天的B站原來是這樣崩潰的……
一、至暗時刻
2021年7月13日22:52,SRE收到大量服務(wù)和域名的接入層不可用報警,客服側(cè)開始收到大量用戶反饋B站無法使用,同時內(nèi)部同學(xué)也反饋B站無法打開,甚至APP首頁也無法打開?;趫缶瘍?nèi)容,SRE第一時間懷疑機房、網(wǎng)絡(luò)、四層LB、七層SLB等基礎(chǔ)設(shè)施出現(xiàn)問題,緊急發(fā)起語音會議,拉各團隊相關(guān)人員開始緊急處理(為了方便理解,下述事故處理過程做了部分簡化)。
二、初因定位
22:55 遠程在家的相關(guān)同學(xué)登陸VPN后,無法登陸內(nèi)網(wǎng)鑒權(quán)系統(tǒng)(B站內(nèi)部系統(tǒng)有統(tǒng)一鑒權(quán),需要先獲取登錄態(tài)后才可登陸其他內(nèi)部系統(tǒng)),導(dǎo)致無法打開內(nèi)部系統(tǒng),無法及時查看監(jiān)控、日志來定位問題。
22:57 在公司Oncall的SRE同學(xué)(無需VPN和再次登錄內(nèi)網(wǎng)鑒權(quán)系統(tǒng))發(fā)現(xiàn)在線業(yè)務(wù)主機房七層SLB(基于OpenResty構(gòu)建) CPU 100%,無法處理用戶請求,其他基礎(chǔ)設(shè)施反饋未出問題,此時已確認是接入層七層SLB故障,排除SLB以下的業(yè)務(wù)層問題。
23:07 遠程在家的同學(xué)緊急聯(lián)系負責(zé)VPN和內(nèi)網(wǎng)鑒權(quán)系統(tǒng)的同學(xué)后,了解可通過綠色通道登錄到內(nèi)網(wǎng)系統(tǒng)。
23:17 相關(guān)同學(xué)通過綠色通道陸續(xù)登錄到內(nèi)網(wǎng)系統(tǒng),開始協(xié)助處理問題,此時處理事故的核心同學(xué)(七層SLB、四層LB、CDN)全部到位。
三、故障止損
23:20 SLB運維分析發(fā)現(xiàn)在故障時流量有突發(fā),懷疑SLB因流量過載不可用。因主機房SLB承載全部在線業(yè)務(wù),先Reload SLB未恢復(fù)后嘗試拒絕用戶流量冷重啟SLB,冷重啟后CPU依然100%,未恢復(fù)。
23:22 從用戶反饋來看,多活機房服務(wù)也不可用。SLB運維分析發(fā)現(xiàn)多活機房SLB請求大量超時,但CPU未過載,準備重啟多活機房SLB先嘗試止損。
23:23 此時內(nèi)部群里同學(xué)反饋主站服務(wù)已恢復(fù),觀察多活機房SLB監(jiān)控,請求超時數(shù)量大大降低,業(yè)務(wù)成功率恢復(fù)到50%以上。此時做了多活的業(yè)務(wù)核心功能基本恢復(fù)正常,如APP推薦、APP播放、評論&彈幕拉取、動態(tài)、追番、影視等。非多活服務(wù)暫未恢復(fù)。
23:25 - 23:55 未恢復(fù)的業(yè)務(wù)暫無其他立即有效的止損預(yù)案,此時嘗試恢復(fù)主機房的SLB。
- 我們通過Perf發(fā)現(xiàn)SLB CPU熱點集中在Lua函數(shù)上,懷疑跟最近上線的Lua代碼有關(guān),開始嘗試回滾最近上線的Lua代碼。
- 近期SLB配合安全同學(xué)上線了自研Lua版本的WAF,懷疑CPU熱點跟此有關(guān),嘗試去掉WAF后重啟SLB,SLB未恢復(fù)。
- SLB兩周前優(yōu)化了Nginx在balance_by_lua階段的重試邏輯,避免請求重試時請求到上一次的不可用節(jié)點,此處有一個最多10次的循環(huán)邏輯,懷疑此處有性能熱點,嘗試回滾后重啟SLB,未恢復(fù)。
- SLB一周前上線灰度了對 HTTP2 協(xié)議的支持,嘗試去掉 H2 協(xié)議相關(guān)的配置并重啟SLB,未恢復(fù)。
???1、新建源站SLB
00:00 SLB運維嘗試回滾相關(guān)配置依舊無法恢復(fù)SLB后,決定重建一組全新的SLB集群,讓CDN把故障業(yè)務(wù)公網(wǎng)流量調(diào)度過來,通過流量隔離觀察業(yè)務(wù)能否恢復(fù)。
00:20 SLB新集群初始化完成,開始配置四層LB和公網(wǎng)IP。
01:00 SLB新集群初始化和測試全部完成,CDN開始切量。SLB運維繼續(xù)排查CPU 100%的問題,切量由業(yè)務(wù)SRE同學(xué)協(xié)助。
01:18 直播業(yè)務(wù)流量切換到SLB新集群,直播業(yè)務(wù)恢復(fù)正常。
01:40 主站、電商、漫畫、支付等核心業(yè)務(wù)陸續(xù)切換到SLB新集群,業(yè)務(wù)恢復(fù)。
01:50 此時在線業(yè)務(wù)基本全部恢復(fù)。
???2、恢復(fù)SLB
01:00 SLB新集群搭建完成后,在給業(yè)務(wù)切量止損的同時,SLB運維開始繼續(xù)分析CPU 100%的原因。
01:10 - 01:27 使用Lua 程序分析工具跑出一份詳細的火焰圖數(shù)據(jù)并加以分析,發(fā)現(xiàn) CPU 熱點明顯集中在對 lua-resty-balancer 模塊的調(diào)用中,從 SLB 流量入口邏輯一直分析到底層模塊調(diào)用,發(fā)現(xiàn)該模塊內(nèi)有多個函數(shù)可能存在熱點。
01:28 - 01:38 選擇一臺SLB節(jié)點,在可能存在熱點的函數(shù)內(nèi)添加 debug 日志,并重啟觀察這些熱點函數(shù)的執(zhí)行結(jié)果。
01:39 - 01:58 在分析 debug 日志后,發(fā)現(xiàn) lua-resty-balancer模塊中的 _gcd 函數(shù)在某次執(zhí)行后返回了一個預(yù)期外的值:nan,同時發(fā)現(xiàn)了觸發(fā)誘因的條件:某個容器IP的weight=0。
01:59 - 02:06 懷疑是該 _gcd 函數(shù)觸發(fā)了 jit 編譯器的某個 bug,運行出錯陷入死循環(huán)導(dǎo)致SLB CPU 100%,臨時解決方案:全局關(guān)閉 jit 編譯。
02:07 SLB運維修改SLB 集群的配置,關(guān)閉 jit 編譯并分批重啟進程,SLB CPU 全部恢復(fù)正常,可正常處理請求。同時保留了一份異?,F(xiàn)場下的進程core文件,留作后續(xù)分析使用。
02:31 - 03:50 SLB運維修改其他SLB集群的配置,臨時關(guān)閉 jit 編譯,規(guī)避風(fēng)險。
四、根因定位
11:40 在線下環(huán)境成功復(fù)現(xiàn)出該 bug,同時發(fā)現(xiàn)SLB 即使關(guān)閉 jit 編譯也仍然存在該問題。此時我們也進一步定位到此問題發(fā)生的誘因:在服務(wù)的某種特殊發(fā)布模式中,會出現(xiàn)容器實例權(quán)重為0的情況。
12:30 經(jīng)過內(nèi)部討論,我們認為該問題并未徹底解決,SLB 仍然存在極大風(fēng)險,為了避免問題的再次產(chǎn)生,最終決定:平臺禁止此發(fā)布模式;SLB 先忽略注冊中心返回的權(quán)重,強制指定權(quán)重。
13:24 發(fā)布平臺禁止此發(fā)布模式。
14:06 SLB 修改Lua代碼忽略注冊中心返回的權(quán)重。
14:30 SLB 在UAT環(huán)境發(fā)版升級,并多次驗證節(jié)點權(quán)重符合預(yù)期,此問題不再產(chǎn)生。
15:00 - 20:00 生產(chǎn)所有 SLB 集群逐漸灰度并全量升級完成。
五、原因說明
???1、背景
B站在19年9月份從Tengine遷移到了OpenResty,基于其豐富的Lua能力開發(fā)了一個服務(wù)發(fā)現(xiàn)模塊,從我們自研的注冊中心同步服務(wù)注冊信息到Nginx共享內(nèi)存中,SLB在請求轉(zhuǎn)發(fā)時,通過Lua從共享內(nèi)存中選擇節(jié)點處理請求,用到了OpenResty的lua-resty-balancer模塊。到發(fā)生故障時已穩(wěn)定運行快兩年時間。
在故障發(fā)生的前兩個月,有業(yè)務(wù)提出想通過服務(wù)在注冊中心的權(quán)重變更來實現(xiàn)SLB的動態(tài)調(diào)權(quán),從而實現(xiàn)更精細的灰度能力。SLB團隊評估了此需求后認為可以支持,開發(fā)完成后灰度上線。
???2、誘因
- 在某種發(fā)布模式中,應(yīng)用的實例權(quán)重會短暫的調(diào)整為0,此時注冊中心返回給SLB的權(quán)重是字符串類型的"0"。此發(fā)布模式只有生產(chǎn)環(huán)境會用到,同時使用的頻率極低,在SLB前期灰度過程中未觸發(fā)此問題。
- SLB 在balance_by_lua階段,會將共享內(nèi)存中保存的服務(wù)IP、Port、Weight 作為參數(shù)傳給lua-resty-balancer模塊用于選擇upstream server,在節(jié)點 weight = "0" 時,balancer 模塊中的 _gcd 函數(shù)收到的入?yún)?b 可能為 "0"。
???3、根因
- Lua 是動態(tài)類型語言,常用習(xí)慣里變量不需要定義類型,只需要為變量賦值即可。
- Lua在對一個數(shù)字字符串進行算術(shù)操作時,會嘗試將這個數(shù)字字符串轉(zhuǎn)成一個數(shù)字。
- 在 Lua 語言中,如果執(zhí)行數(shù)學(xué)運算 n % 0,則結(jié)果會變?yōu)?nan(Not A Number)。
- _gcd函數(shù)對入?yún)]有做類型校驗,允許參數(shù)b傳入:"0"。同時因為"0" != 0,所以此函數(shù)第一次執(zhí)行后返回是 _gcd("0",nan)。如果傳入的是int 0,則會觸發(fā)[ if b == 0 ]分支邏輯判斷,不會死循環(huán)。
- _gcd("0",nan)函數(shù)再次執(zhí)行時返回值是 _gcd(nan,nan),然后Nginx worker開始陷入死循環(huán),進程 CPU 100%。
六、問題分析
1.為何故障剛發(fā)生時無法登陸內(nèi)網(wǎng)后臺?
事后復(fù)盤發(fā)現(xiàn),用戶在登錄內(nèi)網(wǎng)鑒權(quán)系統(tǒng)時,鑒權(quán)系統(tǒng)會跳轉(zhuǎn)到多個域名下種登錄的Cookie,其中一個域名是由故障的SLB代理的,受SLB故障影響當時此域名無法處理請求,導(dǎo)致用戶登錄失敗。流程如下:
事后我們梳理了辦公網(wǎng)系統(tǒng)的訪問鏈路,跟用戶鏈路隔離開,辦公網(wǎng)鏈路不再依賴用戶訪問鏈路。
2.為何多活SLB在故障開始階段也不可用?
多活SLB在故障時因CDN流量回源重試和用戶重試,流量突增4倍以上,連接數(shù)突增100倍到1000W級別,導(dǎo)致這組SLB過載。后因流量下降和重啟,逐漸恢復(fù)。此SLB集群日常晚高峰CPU使用率30%左右,剩余Buffer不足兩倍。如果多活SLB容量充足,理論上可承載住突發(fā)流量, 多活業(yè)務(wù)可立即恢復(fù)正常。此處也可以看到,在發(fā)生機房級別故障時,多活是業(yè)務(wù)容災(zāi)止損最快的方案,這也是故障后我們重點投入治理的一個方向。
3.為何在回滾SLB變更無效后才選擇新建源站切量,而不是并行?
我們的SLB團隊規(guī)模較小,當時只有一位平臺開發(fā)和一位組件運維。在出現(xiàn)故障時,雖有其他同學(xué)協(xié)助,但SLB組件的核心變更需要組件運維同學(xué)執(zhí)行或review,所以無法并行。
4.為何新建源站切流耗時這么久?
我們的公網(wǎng)架構(gòu)如下:
此處涉及三個團隊:
- SLB團隊:選擇SLB機器、SLB機器初始化、SLB配置初始化
- 四層LB團隊:SLB四層LB公網(wǎng)IP配置
- CDN團隊:CDN更新回源公網(wǎng)IP、CDN切量
SLB的預(yù)案中只演練過SLB機器初始化、配置初始化,但和四層LB公網(wǎng)IP配置、CDN之間的協(xié)作并沒有做過全鏈路演練,元信息在平臺之間也沒有聯(lián)動,比如四層LB的Real Server信息提供、公網(wǎng)運營商線路、CDN回源IP的更新等。所以一次完整的新建源站耗時非常久。在事故后這一塊的聯(lián)動和自動化也是我們的重點優(yōu)化方向,目前一次新集群創(chuàng)建、初始化、四層LB公網(wǎng)IP配置已經(jīng)能優(yōu)化到5分鐘以內(nèi)。
5.后續(xù)根因定位后證明關(guān)閉jit編譯并沒有解決問題,那當晚故障的SLB是如何恢復(fù)的?
當晚已定位到誘因是某個容器IP的weight="0"。此應(yīng)用在1:45時發(fā)布完成,weight="0"的誘因已消除。所以后續(xù)關(guān)閉jit雖然無效,但因為誘因消失,所以重啟SLB后恢復(fù)正常。
如果當時誘因未消失,SLB關(guān)閉jit編譯后未恢復(fù),基于定位到的誘因信息:某個容器IP的weight=0,也能定位到此服務(wù)和其發(fā)布模式,快速定位根因。
七、優(yōu)化改進
此事故不管是技術(shù)側(cè)還是管理側(cè)都有很多優(yōu)化改進。此處我們只列舉當時制定的技術(shù)側(cè)核心優(yōu)化改進方向。
???1、多活建設(shè)
在23:23時,做了多活的業(yè)務(wù)核心功能基本恢復(fù)正常,如APP推薦、APP播放、評論&彈幕拉取、動態(tài)、追番、影視等。故障時直播業(yè)務(wù)也做了多活,但當晚沒及時恢復(fù)的原因是:直播移動端首頁接口雖然實現(xiàn)了多活,但沒配置多機房調(diào)度。導(dǎo)致在主機房SLB不可用時直播APP首頁一直打不開,非??上?。通過這次事故,我們發(fā)現(xiàn)了多活架構(gòu)存在的一些嚴重問題:
1)多活基架能力不足
- 機房與業(yè)務(wù)多活定位關(guān)系混亂。
- CDN多機房流量調(diào)度不支持用戶屬性固定路由和分片。
- 業(yè)務(wù)多活架構(gòu)不支持寫,寫功能當時未恢復(fù)。
- 部分存儲組件多活同步和切換能力不足,無法實現(xiàn)多活。
2)業(yè)務(wù)多活元信息缺乏平臺管理
- 哪個業(yè)務(wù)做了多活?
- 業(yè)務(wù)是什么類型的多活,同城雙活還是異地單元化?
- 業(yè)務(wù)哪些URL規(guī)則支持多活,目前多活流量調(diào)度策略是什么?
- 上述信息當時只能用文檔臨時維護,沒有平臺統(tǒng)一管理和編排。
3)多活切量容災(zāi)能力薄弱
- 多活切量依賴CDN同學(xué)執(zhí)行,其他人員無權(quán)限,效率低
- 無切量管理平臺,整個切量過程不可視。
- 接入層、存儲層切量分離,切量不可編排。
- 無業(yè)務(wù)多活元信息,切量準確率和容災(zāi)效果差。
我們之前的多活切量經(jīng)常是這么一個場景:業(yè)務(wù)A故障了,要切量到多活機房。SRE跟研發(fā)溝通后確認要切域名A+URL A,告知CDN運維。CDN運維切量后研發(fā)發(fā)現(xiàn)還有個URL沒切,再重復(fù)一遍上面的流程,所以導(dǎo)致效率極低,容災(zāi)效果也很差。
所以我們多活建設(shè)的主要方向:
4)多活基架能力建設(shè)
- 優(yōu)化多活基礎(chǔ)組件的支持能力,如數(shù)據(jù)層同步組件優(yōu)化、接入層支持基于用戶分片,讓業(yè)務(wù)的多活接入成本更低。
- 重新梳理各機房在多活架構(gòu)下的定位,梳理Czone、Gzone、Rzone業(yè)務(wù)域。
- 推動不支持多活的核心業(yè)務(wù)和已實現(xiàn)多活但架構(gòu)不規(guī)范的業(yè)務(wù)改造優(yōu)化。
5)多活管控能力提升
- 統(tǒng)一管控所有多活業(yè)務(wù)的元信息、路由規(guī)則,聯(lián)動其他平臺,成為多活的元數(shù)據(jù)中心。
- 支持多活接入層規(guī)則編排、數(shù)據(jù)層編排、預(yù)案編排、流量編排等,接入流程實現(xiàn)自動化和可視化。
- 抽象多活切量能力,對接CDN、存儲等組件,實現(xiàn)一鍵全鏈路切量,提升效率和準確率。
- 支持多活切量時的前置能力預(yù)檢,切量中風(fēng)險巡檢和核心指標的可觀測。
???2、SLB治理
1)架構(gòu)治理
- 故障前一個機房內(nèi)一套SLB統(tǒng)一對外提供代理服務(wù),導(dǎo)致故障域無法隔離。后續(xù)SLB需按業(yè)務(wù)部門拆分集群,核心業(yè)務(wù)部門獨立SLB集群和公網(wǎng)IP。
- 跟CDN團隊、四層LB&網(wǎng)絡(luò)團隊一起討論確定SLB集群和公網(wǎng)IP隔離的管理方案。
- 明確SLB能力邊界,非SLB必備能力,統(tǒng)一下沉到API Gateway,SLB組件和平臺均不再支持,如動態(tài)權(quán)重的灰度能力。
2)運維能力
- SLB管理平臺實現(xiàn)Lua代碼版本化管理,平臺支持版本升級和快速回滾。
- SLB節(jié)點的環(huán)境和配置初始化托管到平臺,聯(lián)動四層LB的API,在SLB平臺上實現(xiàn)四層LB申請、公網(wǎng)IP申請、節(jié)點上線等操作,做到全流程初始化5分鐘以內(nèi)。
- SLB作為核心服務(wù)中的核心,在目前沒有彈性擴容的能力下,30%的使用率較高,需要擴容把CPU降低到15%左右。
- 優(yōu)化CDN回源超時時間,降低SLB在極端故障場景下連接數(shù)。同時對連接數(shù)做極限性能壓測。
3)自研能力
- 運維團隊做項目有個弊端,開發(fā)完成自測沒問題后就開始灰度上線,沒有專業(yè)的測試團隊介入。此組件太過核心,需要引入基礎(chǔ)組件測試團隊,對SLB輸入?yún)?shù)做完整的異常測試。
- 跟社區(qū)一起,Review使用到的OpenResty核心開源庫源代碼,消除其他風(fēng)險。基于Lua已有特性和缺陷,提升我們Lua代碼的魯棒性,比如變量類型判斷、強制轉(zhuǎn)換等。
- 招專業(yè)做LB的人。我們選擇基于Lua開發(fā)是因為Lua簡單易上手,社區(qū)有類似成功案例。團隊并沒有資深做Nginx組件開發(fā)的同學(xué),也沒有做C/C++開發(fā)的同學(xué)。
???3、故障演練
本次事故中,業(yè)務(wù)多活流量調(diào)度、新建源站速度、CDN切量速度&回源超時機制均不符合預(yù)期。所以后續(xù)要探索機房級別的故障演練方案:
- 模擬CDN回源單機房故障,跟業(yè)務(wù)研發(fā)和測試一起,通過雙端上的業(yè)務(wù)真實表現(xiàn)來驗收多活業(yè)務(wù)的容災(zāi)效果,提前優(yōu)化業(yè)務(wù)多活不符合預(yù)期的隱患。
- 灰度特定用戶流量到演練的CDN節(jié)點,在CDN節(jié)點模擬源站故障,觀察CDN和源站的容災(zāi)效果。
- 模擬單機房故障,通過多活管控平臺,演練業(yè)務(wù)的多活切量止損預(yù)案。
???4、應(yīng)急響應(yīng)
B站一直沒有NOC/技術(shù)支持團隊,在出現(xiàn)緊急事故時,故障響應(yīng)、故障通報、故障協(xié)同都是由負責(zé)故障處理的SRE同學(xué)來承擔。如果是普通事故還好,如果是重大事故,信息同步根本來不及。所以事故的應(yīng)急響應(yīng)機制必須優(yōu)化:
- 優(yōu)化故障響應(yīng)制度,明確故障中故障指揮官、故障處理人的職責(zé),分擔故障處理人的壓力。
- 事故發(fā)生時,故障處理人第一時間找backup作為故障指揮官,負責(zé)故障通報和故障協(xié)同。在團隊里強制執(zhí)行,讓大家養(yǎng)成習(xí)慣。
- 建設(shè)易用的故障通告平臺,負責(zé)故障摘要信息錄入和故障中進展同步。
本次故障的誘因是某個服務(wù)使用了一種特殊的發(fā)布模式觸發(fā)。我們的事件分析平臺目前只提供了面向應(yīng)用的事件查詢能力,缺少面向用戶、面向平臺、面向組件的事件分析能力:
- 跟監(jiān)控團隊協(xié)作,建設(shè)平臺控制面事件上報能力,推動更多核心平臺接入。
- SLB建設(shè)面向底層引擎的數(shù)據(jù)面事件變更上報和查詢能力,比如服務(wù)注冊信息變更時某個應(yīng)用的IP更新、weight變化事件可在平臺查詢。
- 擴展事件查詢分析能力,除面向應(yīng)用外,建設(shè)面向不同用戶、不同團隊、不同平臺的事件查詢分析能力,協(xié)助快速定位故障誘因。
八、總結(jié)
此次事故發(fā)生時,B站掛了迅速登上全網(wǎng)熱搜,作為技術(shù)人員,身上的壓力可想而知。事故已經(jīng)發(fā)生,我們能做的就是深刻反思,吸取教訓(xùn),總結(jié)經(jīng)驗,砥礪前行。
此篇作為“713事故”系列之第一篇,向大家簡要介紹了故障產(chǎn)生的誘因、根因、處理過程、優(yōu)化改進。后續(xù)文章會詳細介紹“713事故”后我們是如何執(zhí)行優(yōu)化落地的,敬請期待。
最后,想說一句:多活的高可用容災(zāi)架構(gòu)確實生效了。