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