B站故障演練平臺實踐

陶然陶然發表於2023-12-01

  背景

  在雲原生的架構下,微服務的數量呈現爆炸式增長,服務間的呼叫關係錯綜複雜,對系統可靠性也提出了更高的要求。在這樣的背景之下,混沌工程的關注度也不斷提升。

  事實上,混沌工程早就不是什麼新鮮的概念,早在2008年開始,混沌工程的思想就已經始萌芽,彼時,網飛公司由於資料庫發生故障,導致了三天時間的停機,使得 DVD 線上租賃業務中斷,造成了巨大的經濟損失,正是這次線上事故推動了後續的 ChaosMonkeyV1 專案的誕生。在那之後,類似於 SimianArmy、ChaosKong、Gremlin、ChaosMonkeyV2、ChaosBlade、ChaosMesh、ChaosMeta 等混沌工程相關產品在各個大公司的實踐中走入公眾視野。  

  B站混沌工程之路

  相比於業界其他公司如火如荼的混沌工程實踐,B站的混沌工程之路起步較晚,期間的資源投入也略顯不足。在2019年時,基於開源的 ChaosBlade工具,開始嘗試線上下環境進行故障注入,做一些簡單的混沌工程實驗;在2021年時,增加了一些周邊平臺打通、實驗場景管理之類的功能。總體來說,工具層面的投入斷斷續續,能力豐富程度不足;業務接入方面也參差不齊,積極度不高。

  然而,多次的線上問題,讓穩定性問題越來越受到各方的重視。例如年初的一次:一個新業務的支撐服務因為有程式碼問題,壓垮了其依賴的某核心服務;而錯綜複雜的依賴關係,造成了大量其他核心業務不可用;在此過程中,降級措施的歷史問題也被觸發,本該止損的容災機制也無法正常工作。

  在之後的事故總結中,兩個與穩定性相關的要點被提了出來:容災失效、依賴混亂。痛定思痛,基於事故總結的要點,B 站的混沌工程之路分兩個方向開始推進:

  容災演練:針對容災失效問題,主要針對基礎設施層面的容災演練,如多活架構的驗證。

  故障演練:針對業務層面的依賴關係混亂問題,展開強弱依賴梳理、故障演練。

  本文主要從業務層面的故障演練展開,介紹 B 站故障演練平臺的實踐。

  也許有人會有疑問,不是一直在說混沌工程,怎麼突然又轉到了故障演練的概念。事實上,這兩個名詞總是經常被一起談論,也確實不容易分清。一些淺薄的理解,這是兩個道和術之間的概念問題。

  混沌工程提出的是系統穩定性之道,是方法論,典型的如網飛公司總結的混沌工程五大原則(建立穩定狀態的假設、用多樣化的現實世界事件做驗證、在生產環境中進行實驗、持續自動化執行實驗、最小化爆炸半徑);故障演練則是在混沌工程理論指導下的具體實踐方法,不同的公司結合自身的實際,有特定的演練的目標和手段。當然也有公司會逐步將通用能力抽象出來,開源甚至產品化相應的能力。

  B站故障演練平臺的定位,在於結合公司的實際情況,貼近實際的業務進行強弱依賴關係的治理;並儘可能線上上真實環境,執行核心業務鏈路的故障演練。

  明確了這樣的定位之後,進一步進行問題拆解,主要包括以下幾個方面:

  故障注入:怎麼建立故障,要演練哪些型別的故障?

  爆炸半徑控制:線上上環境做演練,如何保障安全性?

  依賴採集:不同的業務場景,相應的依賴項都有哪些?

  演練的自動化:故障演練的執行成本如何降低?

  以下就四個方面逐一展開。

  如何實現故障注入

  業務層面故障注入的基本原理,是在某個處理流程中嵌入故障相關的邏輯,也就是AOP( Aspect Oriented Programming 面向切面程式設計)的思想。不同於 JAVA 有位元組碼增強、動態代理,Go 沒有虛擬機器,怎麼實現 AOP 呢?幾個備選方案是:

  動態法:

  透過反射找到執行方法的指標,然後動態插入程式碼。但這種方法目前主要用於測試工具相關的場景下,存在諸多限制,官方也不推薦用於生產環境;

  靜態法:

  程式碼插入法:基於抽象語法樹(AST)做程式碼生成。但是這種方案需要每次都重新生成一次新的程式碼,附加成本比較高;

  程式碼模式法:透過一定的設計模式,在程式碼層面實現 AOP 的思想。

  結合公司的實際,B 站大部分的微服務都是基於同一套 Golang 微服務框架(Kratos)實現的,因此這種方法的可控程度最強、透明度最高,最終也成為了我們的選擇。  

  中介軟體模式簡單來說就是對某種過程的攔截,多箇中介軟體可以疊加使用。

  以Server型別的故障注入為例,如下圖所示:在 DefaultServer 內建立 Engine 例項後,可以應用一個 _globalServerHandlers 的中介軟體陣列。該陣列可以透過 RegisterServerHandler 方法進行註冊。  

  具體到故障演練中介軟體,可以看到下圖的 HttpServerHandler,實現了在Server 處理請求時的攔截,內部可以判斷當前請求是否命中故障演練實驗、故障元資訊注入、Server 型別故障注入等多種能力。  

  其中的故障注入實現 bmServerAction 中,可以看到主要支援了超時、業務錯誤碼、HTTP 返回碼等多種不同的故障形式。  

  再看 HTTP Client 型別的故障注入,也是類似的方式。如下圖,可以看到 Client 例項會用到 _globalClientOpts 的中介軟體選項陣列,該陣列可以透過 RegisterClientOpt 進行註冊。

  在相應的中介軟體內部,可以看到內部可以基於上下文 ctx 資訊獲取匹配的實驗、實現爆炸半徑的控制、進行Client型別的靶點注入等等。  

  而具體的故障型別,可以包含超時、業務錯誤碼、HTTP 返回碼等等。  

  也許有人會疑惑,為什麼既有 Server 型別的故障,又有 Client 型別的故障呢?對於一次呼叫過程來說,在這個過程的一端進行故障注入不就可以了嗎,為什麼要兩端都實現呢?

  事實上,在使用時確實一般不需要兩端都注入。但是選擇在哪一端進行注入,對於實際的使用來說還是有一定差異的。簡單來說,在 Client 端進行注入,是站在呼叫方的視角,可以避免缺少對被依賴應用的許可權問題(對特定應用進行故障注入是需要許可權的),且可以減小影響的範圍;在 Server 端進行注入,是站在服務提供方的視角,更貼近線上真實業務出現故障的情景,且一次可以演練多個下游受影響的情況。  

  除了以上的 Server 類、Client 類的基礎元件,Kratos 框架中的其他元件也都進行了相應的改造和故障演練能力的支援。大致如下:

  Server 類:HTTP Server、gRPC Server (報錯、超時、特殊錯誤碼等)

  Client 類:HTTP Client、gRPC Client (報錯、超時、特殊錯誤碼等)

  資料庫類:MYSQL、TIDB、TAISHAN (報錯、超時等)

  快取類:REDIS 、MC 等 (報錯、超時等)

  訊息佇列類:DATABUS 等 (傳送報錯、傳送超時、接收報錯等)

  其他特殊類:一些公司內部特有的基礎元件,如記憶體聚合工具等(內部容量滿等)

  瞭解具體的基礎元件的故障注入能力之後,再來看整體的故障注入流程。  

  基本流程:

  業務應用 1 行程式碼接入 SDK, fault.Init()

  SDK 註冊各類元件的故障演練中介軟體

  SDK 初始化,負責與Fault-Service資訊互動,gRPC 長連線

  使用者透過 Fault-Console建立場景、編輯靶點、啟動實驗

  Fault-Admin 將實驗資訊寫入 Fault-DB、Fault-KV

  Fault-Service 從DB/KV 獲取實驗,下發給 SDK

  SDK 攔截相關流程,實現故障點注入,資訊上報

  在業務接入過程中,也遇到一些波折,比如業務反饋在接入SDK 之後,故障注入不生效,最終排查發現業務使用了非常底層的程式碼,繞過了故障演練中介軟體的邏輯;再比如,還有業務反饋故障注入只有部分生效,最終排查發現業務在上下文 ctx 傳遞中出現了問題等等。為了方便使用者的排查,我們提供了諸如實時反饋的演練日誌、特定的故障演練除錯請求和響應 Header 資訊等等。

  此外,還有業務在接入過程中對故障演練功能是否會導致效能問題有諸多的擔憂。對此,故障演練平臺透過多種方式來保障效能影響幾乎可忽略不計,比如:在無演練實驗的情況下快速退出中介軟體、內部實現避免耗時操作、實驗有強制超時機制等等,不展開詳述。

  如何實現爆炸半徑控制

  有了各類基礎元件的故障注入能力之後,若要業務能夠安全地進行故障演練實驗,就必須解決故障的爆炸半徑控制問題。

  如下圖所示,首先我們支援了常規的爆炸控制方式,主要包括:

  例項粒度 (位置 A):Fault-Service 只向圈定的例項推送實驗;

  請求粒度 (位置 B):攔截器識別請求資訊,決定是否注入故障fault-ctx; 例如:請求入口 Path 資訊;

  靶點粒度 (位置 C):根據靶點配置的詳細匹配規則決定是否注入;例如:請求 Path、快取 Keys、操作型別、例項地址等;  

  除此之外,在業務使用過程中,新的需求被提了出來,業務期望能夠圈定特定的使用者賬號(通常此類賬號為內部可控的一些賬號,更方便構造相應的資料和後期清理)來進行演練。考慮到此類需求,結合公司內部的賬號體系,故障演練平臺提供了基於使用者的賬號的爆炸半徑控制方式,主要是兩種方式:

  基於賬號的精確匹配:只會對圈定的精確賬號相關的流量進行故障注入;

  基於賬號尾號的群體劃分:根據賬號的尾號資訊,將流量可以進一步劃分成多份,只有滿足特定尾號的賬號流量進行故障注入。

  當然,這種基於賬號的爆炸半徑控制方式,同樣要求使用者正確使用上下文傳遞使用者的賬號資訊,這也是業務的基本要求。

  至此,常規的伺服器型別業務的故障演練爆炸半徑控制透過以上四種方式可以基本滿足要求了。然而,在推進的過程中,又有新的業務遇到了麻煩。除了伺服器型別業務,公司內部還有很多消費場景的業務,比如稿件的稽核任務、點贊數的非同步統計任務等等,這些任務都不是傳統的一個 Server,而是一種 Job 的形式,根本沒有介面的概念。

  比如,如下圖所示,有一個消費場景的任務 A,它消費兩個 topic,並且對外有三個依賴服務。假設當前故障演練的期望是:演練任務 A 在消費 topic_1 時,依賴1 出現故障的表現,並且演練過程線上上執行,只希望影響其中的部分特定的訊息,不能影響線上正常的訊息處理流程。  

  針對這樣的需求,我們對公司的消費場景進行了調研。好訊息是,B站自研了基於 CQRS 架構的非同步事件治理框架(Railgun),公司的消費場景任務廣泛基於該框架實現。

  於是,我們基於 Railgun框架,插入故障中介軟體,基於 Topic、訊息內容判斷是否注入 Fault-ctx,以此來實現爆炸半徑控制。例如:可以對特定的稿件進行線上故障演練,比如指定內部使用者建立的演練稿件。

  如下圖所示,我們在框架從 Topic 接收訊息的位置插入故障處理中介軟體( Fault-Msg-Handler),根據 Topic 的形式(嚴格匹配或者字首匹配等)、以及訊息內容條件(具體的訊息欄位或者特定的 meta 資訊等)判斷是否需要注入故障演練上下文(Fault-ctx),這個上下文資訊隨著訊息不斷向下傳遞,對於下游依賴的呼叫,可以從該上下文中解析條件判斷是否注入故障,基本流程與 Server 型別的故障注入和爆炸半徑控制一致。  

  如何進行依賴採集

  故障注入能力、爆炸半徑控制能力就位之後,使用者就已經可以開始在平臺執行故障演練的實驗了。常規的操作是,使用者可以建立一個應用場景,然後在該應用場景下根據需要演練的依賴項,依次建立需要注入故障的靶點,圈定需要進行故障注入的範圍之後(例項、目標流量、目標使用者等)就可以開始實驗了。然而,真實的業務場景下,依賴關係十分複雜,如果全部需要研發人員根據程式碼人工去梳理採集,效率低下且容易有錯漏的情況發生。如何簡化依賴資訊的採集流程呢?有幾種備選方案:

  根據公司內部的平臺資訊匯入:然而很多平臺只是維護了應用粒度的依賴資訊,比如某應用 A 依賴了某資料庫 D,而無法細化到介面粒度,不滿足要求;

  根據 Trace 資訊進行匯入:這是一種不錯的方案,但是彼時的問題在於公司內部 Trace 的接入程度可能不足,且存在取樣率的問題,線上採集的時候容易遺漏;

  根據故障演練 SDK 在呼叫依賴的時候進行依賴上報。這種方法的優勢是:依賴採集和故障演練完全匹配,可控程度非常高,因此這成為了我們選擇的方案;

  依賴採集的基本流程如下圖所示:  

  以 HTTP Client 型別依賴採集為例,在故障演練 SDK 中依賴採集的位置如下。其中關鍵點主要是兩方面:

  依賴採集配置下發:和故障演練實驗類似,依賴採集的資訊也會根據匹配情況,被注入到 ctx 中,然後傳遞到各個後續的基礎元件位置;

  依賴資訊上報:在各個基礎元件內部,如果判斷滿足依賴採集的條件,則會拼裝相應的資訊,然後上報返回給故障演練平臺;  

  在程式碼層面上,可以看到在原始的注入 HTTP Client 型別故障的中介軟體內部,執行了 bmClientCollect 邏輯,其中就是根據當前入口介面的資訊、目標請求的依賴介面的資訊等,構造的一個依賴上報項。  

  需要說明的是,我們希望採集到的依賴資訊是細化到各個介面粒度的,因此依賴的資訊存在多樣性,體現在:

  同一個應用,不同的對外入口維度下(介面或Topic),依賴項可能不同;

  即使同一個應用的同一個對外入口,引數不同時,依賴項也可能不同;  

  因此,在執行依賴採集的時候,平臺支援使用者圈定採集的範圍,比如只針對特定的介面或者 Topic 進行依賴採集。使用者可以自行控制依賴採集時的輸入流量,比如只構造一類引數條件下的流量進行採集。在採集完成之後,使用者可以將本次採集到的結果按需匯出為特定的故障場景進行固化,方便後續的靶點構造。

  關於演練的自動化

  演練自動化的問題背景,來源於故障演練執行過程中的人力成本高昂問題。理論上,單個應用的故障演練組合數 = 介面數 * 依賴數 * 故障型別種類 * 故障引數種類。雖然在實際執行過程中一般不會窮舉所有可能性,但是重複、繁瑣的操作還是讓業務方深感疲憊。因此平臺希望能夠提供一些能力減輕使用者的人力投入。

  一方面,平臺提供了強弱依賴自動判定的能力。其基本流程如下所示:  

  其中的關鍵點,主要在於:

  將各個依賴項拆分為單個靶點的演練 task,並且限定該 task 的停止條件,在觸發了特定的演練次數之後(一般設定1次),可以自動實現 task 之間的切換;

  根據返回結果是否包含錯誤等,自動判定強弱依賴關係。這一點上,需要使用者規範地構造返回內容,比如強依賴失敗時返回錯誤碼,弱依賴失敗時無明顯報錯等,否則可能導致誤判;

  另一方面,平臺也提供了開放介面用於對接介面自動化、UI 自動化等流程,這些流程中可以加入一些業務資訊的斷言,並整合到 CI 流程,確保強弱依賴關係不被意外破壞。  

  其基本流程如下:

  故障演練平臺提供 OpenAPI 操作故障演練實驗。

  業務 QA 設計測試用例,編排故障場景。

  在自動化指令碼中開啟、關閉對應演練實驗。

  執行特定的測試邏輯。

  對返回結果進行處理(介面斷言、圖片 DIFF、影片錄製、BUG單建立等)

  其中有一些需要關注的要點:

  故障生效的時效性問題(從SDK拉故障改成 Fault-Service推送故障的模式,提高故障生效時效性)

  演練例項的選擇問題 (支援基於染色的自動圈定,與 CI 流程自動釋出染色例項打通)

  問題:業務需要人為建立大量的故障演練場景,且維護好故障場景與 CASE 的對應關係;

  再一方面,平臺也提供了基於多應用場景的自動演練能力。該問題的背景來源於:一個實際的業務場景,通常會同時涉及多個應用,如何管理多應用場景下的演練?相應的解決方案是:

  設計多應用的『業務場景』的概念。一個業務場景對應多個『應用場景』,一個應用場景下可以有多個故障靶點。

  平臺支援多應用故障點應用不同組合策略,自動進行組合和故障注入。支援一鍵啟動。

  支援繫結業務場景級弱依賴 CASE 集、單依賴級測試 CASE 集,強弱依賴執行不同的靶點組合策略;  

  總結及未來展望

  最後,再來看一下故障演練平臺的全景。故障演練SDK主要包括通用能力和各類基礎元件故障演練中介軟體兩部分;故障演練平臺主要包含依賴、場景、靶點、爆炸半徑等各方面的管理能力,以及對外的介面。對於業務應用來說,接入成本低,透明度高,但是必要時需要一定的改造;業務可以選擇手動觸發故障演練實驗,也可以在自動化流程中整合故障演練的能力等。目前,B站故障演練平臺已經在推薦、播放、動態、話題、彈幕、評論、直播、TV、車載等眾多業務線落地,協助業務梳理、發現、改進大量的強弱依賴相關問題,從而提升業務的穩定性,詳情不在此列舉。  

  未來展望方面,一方面我們會繼續豐富微服務框架內的基礎元件的故障注入的豐富度;另一方面,我們會考慮將故障演練的過程更多向閉環方向建設,整合監控、日誌等資訊,讓演練更加安全,逐步可以實現常態化和自動化。

來自 “ 嗶哩嗶哩技術 ”, 原文作者:黃焱&王旭;原文連結:https://server.it168.com/a2023/1124/6830/000006830986.shtml,如有侵權,請聯絡管理員刪除。

相關文章