B 站壓測實踐之壓測平臺的演進

本期作者

徐光耀

bilibili 資深測試開發工程師

負責 B 站工程效率相關平臺的設計開發;壓測、ci 流水線、雲構建等多個平臺基於雲原生方案的落地實踐者。

01 背景

壓測的重要性毋庸置疑,相比於監控,壓測可以說是主動手段,通過高負載的預演,及時發現線上服務的瓶頸和缺陷,對線上服務質量保障起到了至關重要的作用。而在 B 站,核心業務都會頻繁進行壓測,主要有幾種場景:

我們會分上下兩篇來分別介紹 B 站壓測平臺的演進和全鏈路壓測實踐。本文主要介紹 B 站壓測平臺的演進。

02 B 站壓測演進之路

B 站的壓測演進之路經歷了 3 個階段:手工階段 → 平臺 1.0 階段 → 平臺 2.0 階段。

手工階段: 2018 年之前,壓測完全由各個團隊研發或測試各自寫腳本並輔助開源壓測工具(如 Jmeter)完成。由於單節點難以實現高併發,通常需要實現多節點分佈式壓測能力,門檻較高,所以壓測由各團隊分治的缺點就很明顯,人力成本 ++,資源成本 ++,而完全可以通過平臺來提供通用的壓測能力,既降低了壓測的使用門檻,同時資源也能統籌利用。

平臺 1.0 階段: 於是 2018 年推出了第一版壓測平臺,通過平臺交互將用戶的接口配置轉化爲 Jmeter 腳本,而底層壓測引擎結合了雲原生 + Jmeter,每一個 pod 的容器執行一個 Jmeter 腳本,通過調度增減 Pod 數量來控制併發數。但是經過較長一段時間的平臺運營之後,我們就發現這種設計存在較多瓶頸和問題:

問題一:Jmeter 需要人爲設定併發數,容易造成不合理配置,資源浪費或過載。

問題二:Jmeter 不支持分佈式文件傳參且保證參數不重複。

問題三:Jmeter 在傳參文件過小的情況會出現磁盤 IO 瓶頸。

問題四:Jmeter 場景以 xml 格式描述,場景模版事先定義,難以動態擴展,靈活性差。

平臺 2.0 階段: 在 2021 年年初,我們推翻了壓測 1.0 設計,推出了全新的壓測平臺 2.0,解決了 Jmeter 帶來的諸多瓶頸和限制,通過自研發壓客服端以及基於監控的自適應調度算法,實現了智能發壓引擎。

03 壓測平臺設計

接下來具體介紹壓測平臺 2.0 是如何設計實現的。首先明確我們的目標是提供自助壓測平臺,這裏有兩層含義:

其一,平臺提供的功能操作要儘可能簡化,降低平臺使用門檻,用戶可以自主完成壓測的全部操作,無需平臺維護人員介入;

其二,平臺支持的場景要儘可能豐富,滿足各種壓測需求,儘量減少用戶平臺外的操作成本。

3.1  壓測引擎實現

基於平臺目標,理想的壓測引擎是:用戶只需要給出他期望壓力值即可,壓測引擎按照期望值去施壓,用戶無需關注分佈式節點併發數分配的所有細節。

3.1.1 設計考量與反思

在設計底層引擎的時候,我們首要考慮的是如何儘可能提高施壓上限,這裏有兩個關鍵點:

第一,發壓客戶端採用分佈式部署,並且節點數量可擴展。

第二,充分利用分佈式施壓集羣裏每一個節點的資源。

針對第一點,我們結合雲原生技術,給壓測單獨分配一個 Paas 資源池,根據資源總量,以每個 Pod 分配 4C8G 來換算 Pod 數量,每個 Pod 裏起一個容器運行發壓客戶端作爲一個節點。這樣做有兩個好處:

而第二點是我們着重要解決的問題。首先我們來看單節點併發數與 QPS 之間的關係曲線,如圖 1,假設服務端承載足夠大的情況下,單實例發壓客戶端隨併發數的增長,QPS 趨勢呈 3 個階段:

圖片

圖 1:單實例併發和 QPS 關係

第一階段,節點處於低負載狀態,QPS 隨併發數呈線性增長,此時資源還沒有被充分利用。

第二階段,節點處於接近滿負載狀態,QPS 增長趨勢開始變緩,此時資源得到充分利用。

第三階段,節點處於超負載狀態,QPS 隨着併發數增長,出現緩慢下降;此時過多線程的競爭起到了副作用,不僅 QPS 出現了下降,而且客戶端的響應耗時也遠遠大於服務端實際值。

顯而易見,第二階段是最優併發數配額,單節點發揮了自身全部的資源價值,資源利用率是最優的。

回顧平臺 1.0 的設計,當時採用的 Jmeter 作爲發壓引擎,啓動的時候需要用戶來指定該節點的併發數,顯然這對於用戶來說是個難題,他沒法事先知道最優的併發數是多少,這個數值與實際的壓測場景緊密相關,不同的接口以及接口組成的場景,這個最優解都是不同的。理論上用戶只能通過多次嘗試去探測出這個值。事實上大部分用戶都不會這麼做,“保守”的用戶會給每個節點配置少量併發數,這樣就需要更多的實例才能滿足他的壓力需求,很可能導致節點數量不足,而實際上並非資源不足,只是使用不合理;而 “激進” 的用戶會給每個節點配置大量併發數,每個節點都超負載運行,雖然壓力需求得到滿足,但是客戶端的請求響應耗時會遠遠大於服務端的統計,造成用戶的疑惑。

3.1.2 新框架誕生

平臺 2.0 針對上述問題,對底層發壓框架進行了全面改造升級,核心有兩點:

第一,通過自研發壓客戶端取代 Jmeter,除了基本的 HTTP 和 GRPC 接口請求功能外,額外實現了動態線程池功能,可在運行時動態增減併發數。

第二,通過基於實例監控的自適應調度算法,自動探測每個實例的最優併發數配置,自動增減實例,實現智能分佈式發壓引擎。

圖片

圖 2:壓測平臺框架

平臺 2.0 整體框架如上圖 2 所示:

3.1.3 調度原理

下圖 3 爲具體的調度原理示意圖:

圖片

圖 3:調度原理示意圖

影響客戶端實例調度的有兩個因素,一個是客戶端自身狀態變化,一個是用戶行爲。

發壓客戶端有 3 種狀態:空閒狀態、加減壓狀態、穩定狀態。

用戶有兩種操作行爲會影響容器調度,第一個是啓動任務,如圖 3 第一步所示,平臺會往 Redis 隊列塞任務;第二個是加速,如圖三第五步所示,此處加速是指當單實例加壓速率(上限爲單次增加 30 併發)無法滿足需求時,平臺會通過增加並行實例數量的方式,提高加壓速率。

3.1.4 發壓模式

平臺 2.0 在上層暴露給用戶 2 種加壓模式選項:指定 QPS 模式和指定併發數模式。

兩種模式相同的是,在壓測前,都需要指定本次壓測最大期望 QPS,用於流程審覈,風險把控;在壓測過程中,一旦當前 QPS 超出最大期望 QPS,便強制通過減小併發數來降低 QPS,直到低於最大期望,此爲發壓客戶端裏主循環控制器的首要判斷條件。

兩種模式不同的是,指定 QPS 模式在壓測過程中,用戶可調整 “當前期望 QPS” 以及“加壓速率”,實現精準的 QPS 數值控制以及 QPS 變化的速度(本質上發壓引擎還是通過併發數、時間間隔的調整,轉化爲 QPS 的變化,只不過這一步交給底層邏輯實現,對用戶透明);指定併發數模式在壓測過程中,用戶可調整“當前期望併發數”,實現自由的併發數控制。

實際上,80% 以上的壓測場景更適合採用指定 QPS 模式,用戶主要關心 QPS 指標,這種模式下用戶一次壓測過程中可以逐步加壓,例如他的預期最大 QPS 是 10w,但能不能達到 10w,或者甚至超出 10w,心裏也沒底,這時就可以採用這種模式,從初始 1w 開始,分階梯加壓,比如每次增加 2w,那麼依次調整預期 QPS 值爲 1w、3w、5w、7w、9w,甚至更大(上限爲最大預期 QPS),每次達到階梯預期目標時可稍作停留,觀察客戶端的響應耗時和報錯數量的變化以及服務端的相關監控數據。通過這種方式,可以輕鬆探測出服務的承壓極限。那麼什麼場景下更適合採用指定併發數模式呢?如果壓測場景對瞬時併發數量有要求,可以採用這種模式,例如電商的搶購場景,此時可設置較大的初始併發數,來模擬大量用戶瞬時搶購的場景。

最後總結一下框架設計,底層通過自研發壓客戶端和基於負載監控的自適應調度算法實現了智能發壓引擎,而上層平臺完全屏蔽了底層複雜邏輯,用戶只需要選擇合適的加壓模式,並指定期望 QPS、加壓速率或期望併發數等參數即可。

3.2 壓測場景模式

用戶的壓測場景是多樣化的,大致可歸納爲以下幾類:

需求一:單接口請求內容支持不同傳參值進行壓測,例如 HTTP 的 Header,GET 請求的 param,POST 請求的 body,GRPC 的 metadata 等等

需求二:在需求一的基礎上進行多接口組合,支持並行或串行接口組合方式進行壓測

需求三:線上真實流量錄製保存,在對其預處理之後進行回放

平臺 1.0 基於 Jmeter 的預設場景模版,在一定程度上滿足了需求一和需求二,但缺乏靈活性。而平臺 2.0 基於上述需求,重新設計,抽象成 2 類場景模式,分別稱爲自定義場景和流量錄製回放場景。

3.2.1 自定義場景

自定義場景,顧名思義,用戶可靈活定製壓測場景,滿足多樣性需求。

接口內容的多樣性

a. 傳參

壓測接口往往需要傳遞不同的參數值,滿足多樣化場景需求。爲此我們支持 3 種接口內的傳參方式:

例如有一個 HTTP 的 GET 接口 A:http://api.bilibili.com/test?name=${name}&type=${type}&value=${__RANDOMNUM,1,12},其中有 3 個參數 name、type 和 value 需要動態傳值:

文件傳參是在平臺後臺生成執行文件的過程中進行值替換的,而隨機函數和簽名函數是在發壓客戶端實際執行壓測的過程中替換的。

b. 轉發

接口支持 3 種轉發方式可選:外網模式、內網 slb 模式以及內網服務發現直連模式,可根據需求自行選配。如果想壓測完整鏈路,可配外網模式;如果想針對部分實例壓測,可選內網服務發現直連模式,選擇部分在線的節點或集羣。

c. 斷言

接口響應返回內容支持斷言,斷言方式支持等於、不等於、包含、不包含等多種判斷條件。針對 http 協議接口,先斷言 http 狀態碼是否在 [200, 300) 之間,如果是再斷言返回內容,否則即認爲斷言失敗;針對 grpc 協議接口,判斷有無報錯異常,如果正常返回,再斷言返回內容。

接口之間組合的多樣性

支持接口串並行組合,多個接口串行組成子任務,多個子任務並行組成完整壓測任務,如圖四所示。串行接口之間支持將上游接口響應內容作爲下游接口輸入參數值,首先上游接口先定義其響應內容(必須是 Json 格式)的哪個字段值作爲傳參值,並定義其參數名,而在下游接口中用該參數名佔位即可,方式類似文件傳參。在實際壓測執行過程中施壓客戶端會提取上游接口的響應內容並識別下游接口對應的佔位符並進行替換,從而實現上下游傳參。

圖片

圖 4:接口組合示意圖

用戶在平臺上配置完自定義場景之後,後臺會根據場景的接口配置和傳參文件內容自動拼接成執行文件,每一個子任務對應一個執行文件,執行文件的每一行對應一組串行接口。提前拼接所有傳參文件,主要基於以下幾點考慮:

文件內容按重複和不重分成兩種模式,如圖 5 所示:

圖片

圖 5:不同模式下文件預處理邏輯

3.2.2 流量錄製回放場景

除了設計自定義壓測場景,還有一種訴求是通過錄制線上真實流量,並進行回放進行壓測。這種方式有 2 個優點,其一線上真實流量對接口的各種參數覆蓋更加全面,其二錄製省去了人工構造的成本。

在業界像 Java 技術棧已有比較成熟的流量錄製方案,採用 jvm-sandbox-repeater 實現。在 B 站大部分微服務應用都採用 Golang 技術棧,少部分採用 Java 技術棧,所以需要一套跨語言通用的流量錄製方案。有人提出基於 sdk 的方式,業務代碼集成流量錄製的 sdk 來實現,這種方案存在以下幾個問題:

經過調研和嘗試,最終我們提出一種跨語言通用的非代碼侵入式的微服務流量錄製方法,具體可以實現以下效果:

圖片

圖 6:流量錄製回放框架

其架構如圖 6 所示,分成三個部分:

流量監聽伴生服務

利用同一個 pod 內所有容器共享網絡棧的原理,基於伴生容器,通過旁路方式監聽微服務的入口端口以及所有出口端口,從而獲得所有出入口流量,並且做到主服務無感知,不受影響。

主進程開啓定時輪詢,每 60 秒請求一次平臺 heartbeat 接口,請求內容帶上應用和實例的相關信息(包括當前開關狀態),一來在平臺上註冊該應用實例的存活狀態,記錄最新存活心跳(時刻),以及當前開關狀態,二來 heartbeat 接口返回內容包含了平臺上用戶對該應用的相關配置信息,包括接口錄製規則、流量採用率等等,還有預期開關狀態,決定當前開啓還是關閉流量錄製。

如果開啓錄製,則根據不同的服務類型和出入端口,分別執行開源工具 tshark 相應的網卡監聽命令;特別地,對於 grpc 服務,tshark 啓動時需要指定該服務定義的 proto 文件,平臺返回的相關配置信息中包含 grpc 服務的 proto 文件所在 git 倉庫信息,所以在啓動 tshark 之前通過 git 接口獲取所需 proto 文件,並置於 tshark 所要求的路徑下;將 tshark 監聽網卡錄製到的指定出入端口的流量進行格式化處理,tshark 錄製到的流量是按請求和響應拆分的,每一條流量信息要麼是一次接口請求,要麼是一次接口響應,將監聽到的請求先緩存在內存中,等其對應的響應到來時,拼接成完整的一次接口信息(包含請求和響應);將上述拼接完的接口信息,對其中部分可能存在敏感信息的字段進行加密處理,然後整體序列化後上傳到 Kafka,進行收集。

流量收集系統

採用業界通用流式數據處理方案。按如下步驟:

a. 首先 Kafka 集羣負責接收從各個微服務實例的伴生容器發送過來的格式化後的流量數據;

b. 由 Logstash 服務負責從 Kafka 隊列中去消費流量數據,並按寫入 ElasticSearch 數據引擎;

c. ElasticSearch 按微服務應用名分索引存儲各應用的流量數據,供平臺查詢進行展示。

流量錄製管理平臺

用戶在平臺上註冊應用並配置相關信息,伴生容器定時請求平臺,將當前伴生容器的心跳發生給平臺,表明當前存活狀態,同時平臺將當前應用的用戶配置信息返回給伴生容器,包括接口錄製規則、採樣率、預期開關狀態等信息,通過這種方式靈活控制開啓錄製的實例個數、流量錄製的採樣率以及接口錄製規則。分爲三個部分:

第一部分是心跳收集 / 配置同步,主要負責與各微服務的伴生進行交互,平臺暴露一個 heartbeat 接口,供伴生服務調用,平臺收到心跳請求時,按如下步驟執行:

a. 根據傳參信息(應用、實例、開關狀態),在數據庫中更新該應用該實例的心跳時刻和當前開關狀態,如果原先數據庫中不存在該應用該實例的心跳記錄,則新插入一條記錄;

b. 查詢該應用的相關配置信息,包括流量錄製規則、流量採用率、當前實例預期開關狀態,作爲請求的響應返回給伴生服務;

此外,平臺後臺定時執行配置同步(10 秒一次),查詢應用預期開啓錄製的實例個數,查詢該應用實例的心跳記錄,篩選出當前存活的實例,統計當前開啓監聽的實例個數,與預期啓動監聽的實例個數進行比較:如果當前開啓的個數 n 少於預期 m,則從當前存活且預期開關狀態爲關的實例中隨機選取(m-n)個實例,將數據庫中其預期開關狀態變更爲開;如果當前開啓的個數 n 大於預期 m,則從當前存活且預期狀態爲開的實例中隨機選取(n-m)個實例,將數據庫中其預期開關狀態變更爲關;如果當前開啓的個數等於預期,則維持不動。

場景舉例:微服務 X 有 10 個實例,設置 3 個實例開啓流量錄製。當 X 進行版本升級時,舊版本 A 下線,新版本 B 上線,此時舊版本 A 的 10 個實例的容器都會收到終止信號,執行銷燬操作,這樣舊版本 A 的 10 個實例的心跳就停留在當前時刻,而新版本 B 的 10 個實例啓動之後就開始發心跳,而其當前狀態以及默認初始預期開關狀態都是關,此時平臺定時執行配置同步的時候,就會發現微服務 X 的預期開啓錄製的實例個數是 3,而實際存活的且開啓錄製的實例個數是 0,則就會在當前存活的版本 B 的 10 個實例中隨機挑選 3 個實例,將其數據庫中預期開關狀態更新爲開,這樣後續這 3 個實例的伴生服務發來心跳時,就會獲得最新的預期開關狀態爲開,就會開啓錄製。

第二部分是應用接入 / 規則配置,用戶如果有新應用想使用流量錄製功能,執行如下步驟:

a. 在平臺上新增應用,完成應用相關的信息配置,例如如果該應用提供 grpc 服務,需要配置 grpc 服務定義的 proto 文件所在的倉庫及其路徑;

b. 在平臺上配置該應用的錄製規則,包括:接口 path(支持正則、黑白名單模式)、開啓錄製的實例個數、流量採樣率(基於單個實例);

第三部分是流量查詢展示,用戶在平臺上可查看某個應用下錄製到流量,可根據時間、path 等條件進行流量過濾。平臺從 ElasticSearch 獲取數據時,對相關加密字段進行解密處理,然後傳給前端進行展示。另外,只有該應用有權限的人員才能訪問對應的數據。

上述就是流量錄製的方案。當獲得真實流量之後,就可以通過篩選過濾出想要進行回放的接口,並導出成執行文件,這裏的執行文件的格式與 3.2.1 中保持一致,這樣壓測引擎可以直接適配,進行流量回放壓測。

04 全鏈路數據隔離

壓測過程中,既有讀接口也有寫接口,爲了保證線上數據安全不受污染,需要做到數據隔離。主要有兩種方案:

80% 以上的壓測需求採用賬號隔離的方式即可滿足,剩下一些場景需要採用流量錄製回放模式配合影子庫隔離來進行全鏈路壓測。影子庫隔離的具體實現比較複雜,後續會其他同事單獨寫一篇文章來介紹,敬請期待。

05 壓測實踐效果

當前 B 站 95% 以上的業務都會走壓測平臺進行壓測,尤其在大型活動準備期間,壓測次數就會陡增。使用平臺進行壓測的次數趨勢如圖 7 所示,可以看到在 21 年下半年出現了明顯的上升,這期間正是經歷了 S11 賽事、電商大促、跨晚等大型活動,平臺 2.0 在 21 年年初上線,提供了豐富、完整、穩定的壓測能力,很好地支持了這些活動多樣化的壓測需求。

圖片

圖 7:壓測使用次數趨勢

以 S11 賽事爲例,總共進行了 3 輪集中式的壓測預演,累計進行了 1000 + 次壓測,涉及 130 + 個微服務。在壓測場景設計方面,自定義場景模式滿足了直播間場景的壓測需求,支持十多組接口同時併發,並且在壓測過程中可獨立控制各組接口的壓力。除此之外,電商團隊會使用預期併發數模式進行壓測,模擬瞬時高併發的場景,演練會員購大促搶購活動;部分業務線已開始使用流量錄製回放模式,進行日常壓測迴歸。

06 後續規劃

當前壓測平臺記錄並實時展示了客戶端的性能指標趨勢圖,包括 QPS、平均響應耗時、錯誤數、吞吐量、併發數、不同分位響應耗時等,而用戶如果要觀察服務端的監控數據,則需要去另外的監控平臺查看。後續規劃與線上監控這塊進行有機結合:

第一,壓測平臺與線上監控平臺打通,客戶端與服務端的監控數據在平臺上同屏展示,便於用戶在壓測過程中實時觀察以及定位問題。

第二,當前壓測執行熔斷是以客戶端的接口響應錯誤率作爲判定指標,後續考慮將服務端的性能指標納入熔斷判定機制,例如用戶可配置規則 / 閾值:當服務端負載 cpu 使用率超過 70%,則自動熔斷,停止壓測。

嗶哩嗶哩技術 提供 B 站相關技術的介紹和講解

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-8flTCyNFDKrnnlHYaVAgw