7.13事故 | 我們是這樣崩的
至暗時刻
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,未恢復。
新建源站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 此時線上業務基本全部恢復。
恢復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 叢集逐漸灰度並全量升級完成。
原因說明
背景
B站在19年9月份從Tengine遷移到了OpenResty,基於其豐富的Lua能力開發了一個服務發現模組,從我們自研的註冊中心同步服務註冊資訊到Nginx共享記憶體中,SLB在請求轉發時,透過Lua從共享記憶體中選擇節點處理請求,用到了OpenResty的lua-resty-balancer模組。到發生故障時已穩定執行快兩年時間。
在故障發生的前兩個月,有業務提出想透過服務在註冊中心的權重變更來實現SLB的動態調權,從而實現更精細的灰度能力。SLB團隊評估了此需求後認為可以支援,開發完成後灰度上線。
誘因
-
在某種釋出模式中,應用的例項權重會短暫的調整為0,此時註冊中心返回給SLB的權重是字串型別的"0"。此釋出模式只有生產環境會用到,同時使用的頻率極低,在SLB前期灰度過程中未觸發此問題。
-
SLB 在balance_by_lua階段,會將共享記憶體中儲存的服務IP、Port、Weight 作為引數傳給lua-resty-balancer模組用於選擇upstream server,在節點 weight = "0" 時,balancer 模組中的 _gcd 函式收到的入參 b 可能為 "0"。
根因
-
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首頁一直打不開,非常可惜。透過這次事故,我們發現了多活架構存在的一些嚴重問題:
多活基架能力不足
-
機房與業務多活定位關係混亂。
-
CDN多機房流量排程不支援使用者屬性固定路由和分片。
-
業務多活架構不支援寫,寫功能當時未恢復。
-
部分儲存元件多活同步和切換能力不足,無法實現多活。
業務多活元資訊缺乏平臺管理
-
哪個業務做了多活?
-
業務是什麼型別的多活,同城雙活還是異地單元化?
-
業務哪些URL規則支援多活,目前多活流量排程策略是什麼?
-
上述資訊當時只能用文件臨時維護,沒有平臺統一管理和編排。
多活切量容災能力薄弱
-
多活切量依賴CDN同學執行,其他人員無許可權,效率低。
-
無切量管理平臺,整個切量過程不可視。
-
接入層、儲存層切量分離,切量不可編排。
-
無業務多活元資訊,切量準確率和容災效果差。
我們之前的多活切量經常是這麼一個場景:業務A故障了,要切量到多活機房。SRE跟研發溝通後確認要切域名A+URL A,告知CDN運維。CDN運維切量後研發發現還有個URL沒切,再重複一遍上面的流程,所以導致效率極低,容災效果也很差。
所以我們多活建設的主要方向:
多活基架能力建設
-
最佳化多活基礎元件的支援能力,如資料層同步元件最佳化、接入層支援基於使用者分片,讓業務的多活接入成本更低。
-
重新梳理各機房在多活架構下的定位,梳理Czone、Gzone、Rzone業務域。
-
推動不支援多活的核心業務和已實現多活但架構不規範的業務改造最佳化。
多活管控能力提升
-
統一管控所有多活業務的元資訊、路由規則,聯動其他平臺,成為多活的後設資料中心。
-
支援多活接入層規則編排、資料層編排、預案編排、流量編排等,接入流程實現自動化和視覺化。
-
抽象多活切量能力,對接CDN、儲存等元件,實現一鍵全鏈路切量,提升效率和準確率。
-
支援多活切量時的前置能力預檢,切量中風險巡檢和核心指標的可觀測。
2. SLB治理
架構治理
-
故障前一個機房內一套SLB統一對外提供代理服務,導致故障域無法隔離。後續SLB需按業務部門拆分叢集,核心業務部門獨立SLB叢集和公網IP。
-
跟CDN團隊、四層LB&網路團隊一起討論確定SLB叢集和公網IP隔離的管理方案。
-
明確SLB能力邊界,非SLB必備能力,統一下沉到API Gateway,SLB元件和平臺均不再支援,如動態權重的灰度能力。
運維能力
-
SLB管理平臺實現Lua程式碼版本化管理,平臺支援版本升級和快速回滾。
-
SLB節點的環境和配置初始化託管到平臺,聯動四層LB的API,在SLB平臺上實現四層LB申請、公網IP申請、節點上線等操作,做到全流程初始化5分鐘以內。
-
SLB作為核心服務中的核心,在目前沒有彈性擴容的能力下,30%的使用率較高,需要擴容把CPU降低到15%左右。
-
最佳化CDN回源超時時間,降低SLB在極端故障場景下連線數。同時對連線數做極限效能壓測。
自研能力
-
運維團隊做專案有個弊端,開發完成自測沒問題後就開始灰度上線,沒有專業的測試團隊介入。此元件太過核心,需要引入基礎元件測試團隊,對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事故”後我們是如何執行最佳化落地的,敬請期待。
最後,想說一句:多活的高可用容災架構確實生效了。
來自 “ 嗶哩嗶哩技術 ”, 原文作者:嗶哩嗶哩技術;原文連結:https://mp.weixin.qq.com/s/nGtC5lBX_Iaj57HIdXq3Qg,如有侵權,請聯絡管理員刪除。
相關文章
- 我是這樣理解EventLoop的OOP
- 私有化輸出的服務網格我們是這樣做的
- 4人團隊,一年營收300多萬,我們是這樣做的!營收
- 為了落地DDD,我是這樣“PUA”大家的
- 我是這樣成為年薪30萬的前端!前端
- 我們需要怎樣的 Service
- 我們通常這樣進行Linux弱口令檢測!Linux
- 探索React Hooks:原來它們是這樣誕生的!ReactHook
- 我們在開源專案中是怎樣埋彩蛋的
- Flutter Engine 編譯 —— 我是這樣讀原始碼的Flutter編譯原始碼
- 封裝element-ui表格,我是這樣做的封裝UI
- 我是 Netflix的內容分析工程師,我的一天是這樣度過的工程師
- 在鏈圈,我們會稱這樣的科技為“去中心化”中心化
- Free Lives製作人:在南非,我們這樣做遊戲遊戲
- 我們評測了5個主流跨端框架,這是它們的區別跨端框架
- 在面試官面前我是這樣介紹CAS的面試
- 據我瞭解免費OA系統是這樣的
- 面試大廠,我是這樣準備專案的面試
- 為何我們還需要摩卡DHT-PHEV這樣的動力方案?
- GitHub 工程師:我眼中的理想上司是這樣子的Github工程師
- 7.13
- 我應該怎麼樣來推薦我們製作的這款RPG遊戲呢?遊戲
- 在 Ali Kubernetes 系統中,我們這樣實踐混沌工程
- 我們需要什麼樣的 ORM 框架ORM框架
- 老闆今天問我為什麼公司的資料庫這麼爛,我是這樣回答的......資料庫
- 快看,我們的分散式快取就是這樣把註冊中心搞崩塌的分散式快取
- 快看,我們的分散式快取就是這樣把註冊中心搞崩塌分散式快取
- 資料偏移、分割槽陷阱……我們這樣避開DynamoDB的5個坑
- ⚡️ 省錢 90%!我是這樣優化網站圖片的優化網站
- 我是這樣手寫 Spring 的(麻雀雖小五臟俱全)Spring
- 是我們控制著技術,還是技術控制著我們?
- 當我們在談零信任時,我們談的是什麼?
- 『假如我是面試官』RabbitMQ我會這樣問面試MQ
- 假如我是面試官,我會這樣虐你面試
- 我們的口號是什麼?
- 我們這些“攻城獅”的襯衣
- 基於Vue2.x的前端架構,我們是這麼做的Vue前端架構
- 我們分析了10萬條洩露密碼,發現了這樣的套路密碼