天天看點

流量錄制與回放技術實踐

本文主要介紹了流量錄制與回放技術在壓測場景下的應用。通過閱讀本篇文章,你将了解到開源的錄制工具如何與内部系統內建、如何進行二次開發以支援 Dubbo 流量錄制、怎樣通過 Java 類加載機制解決 jar 包版本沖突問題、以及流量錄制在自動化測試場景下的應用與價值等。文章共約 1.4 萬字,配圖17張。本篇文章是對我個人過去一年所負責的工作的總結,裡面涉及到了很多技術點,個人從中學到了很多東西,也希望這篇文章能讓大家有所收獲。當然個人能力有限,文中不妥之處也歡迎大家指教。具體章節安排如下:

流量錄制與回放技術實踐

本篇文章記錄和總結了自己過去一年所主導的項目——流量錄制與回放,該項目主要用于為業務團隊提供壓測服務。作為項目負責人,我承擔了約 70% 的工作,是以這個項目承載了自己很多的記憶。從需求提出、技術調研、選型驗證、問題處置、方案設計、兩周内上線最小可用系統、推廣使用、支援年中/終全鍊路壓測、疊代優化、支援 dubbo 流量錄制、到新場景落地産生價值。這裡列舉每一項自己都深度參與了,是以也從中學習到了很多東西。包含但不限于 go 語言、網絡知識、Dubbo 協定細節,以及 Java 類加載機制等。除此之外,項目所産生的價值也讓自己很欣喜。項目上線一年,幫助業務線發現了十幾個性能問題,幫助中間件團隊發現了基礎元件多個嚴重的問題。總的來說,這個項目對于我個人來說具有非凡意義,受益良多。這裡把過去一年的項目經曆記錄下來,做個總結。本篇文章着重講實作思路,不會貼太多代碼,有興趣的朋友可以根據思路自己定制一套。好了,下面開始正文吧。

項目的出現源自業務團隊的一個訴求——使用線上真實的流量進行壓測,使壓測更為“真實”一些。之是以業務團隊覺得使用老的壓測平台(基于 Jmeter 實作)不真實,是因為壓測資料的多樣性不足,對代碼的覆寫度不夠。正常壓測任務通常都是對應用的 TOP 30 接口進行壓測,如果人工去完善這些接口的壓測資料,成本是會非常高的。基于這個需求,我們調研了一些工具,并最終選擇了 Go 語言編寫的 GoReplay 作為流量錄制和回放工具。至于為什麼選擇這個工具,接下來聊聊。

一開始選型的時候,經驗不足,并沒有考慮太多因素,隻從功能性和知名度兩個次元進行了調研。首先功能上一定要能滿足我們的需求,比如具備流量過濾功能,這樣可以按需錄制指定接口。其次,候選項最好有大廠背書,github 上有很多 star。根據這兩個要求,選出了如下幾個工具:

流量錄制與回放技術實踐

圖1:技術選型

第一個是選型是阿裡開源的工具,全稱是 jvm-sandbox-repeater,這個工具其實是基于 JVM-Sandbox 實作的。原理上,工具通過位元組碼增強的形式,對目标接口進行攔截,以擷取接口參數和傳回值,效果等價于 AOP 中的環繞通知 (Around advice)。

第二個選型是 GoReplay,基于 Go 語言實作。底層依賴 pcap 庫提供流量錄制能力。著名的 tcpdump 也依賴于 pcap 庫,是以可以把 GoReplay 看成極簡版的 tcpdump,因為其支援的協定很單一,隻支援錄制 http 流量。

第三個選型是 Nginx 的流量鏡像子產品 ngx_http_mirror_module,基于這個子產品,可以将流量鏡像到一台機器上,實作流量錄制。

第四個選型是阿裡雲雲效裡的子産品——雙引擎回歸測試平台,從名字上可以看出來,這個系統是為回歸測試開發的。而我們需求是做壓測,是以這個服務裡的很多功能我們用不到。

經過比較篩選後,我們選擇了 GoReplay 作為流量錄制工具。在分析 GoReplay 優缺點之前,先來分析下其他幾個工具存在的問題。

jvm-sandbox-repeater 這個插件底層基于 JVM-Sandbox 實作,使用時需要把兩個項目的代碼都加載到目标應用内,對應用運作時環境有侵入。如果兩個項目代碼存在問題,造成類似 OOM 這種問題,會對目标應用造成很大大的影響。另外因為方向小衆,導緻 JVM-Sandbox 應用并不是很廣泛,社群活躍度較低。是以我們擔心出現問題官方無法及時修複,是以這個選型待定。

ngx_http_mirror_module 看起來是個不錯的選擇,出生“名門”。但問題也有一些。首先隻能支援 http 流量,而我們以後一定會支援 dubbo 流量錄制。其次這個插件要把請求鏡像一份出去,勢必要消耗機器的 TCP 連接配接數、網絡帶寬等資源。考慮到我們的流量錄制會持續運作在網關上,是以這些資源消耗一定要考慮。最後,這個子產品沒法做到對指定接口進行鏡像,且鏡像功能開關需要修改 nginx 配置實作。線上的配置是不可能,尤其是網關這種核心應用的配置是不能随便改動的。綜合這些因素,這個選型也被放棄了。

阿裡雲的引擎回歸測試平台在我們調研時,自身的功能也在打磨,用起來挺麻煩的。其次這個産品屬于雲效的子産品,不單獨出售。另外這個産品主要還是用于回歸測試的,與我們的場景存在較大偏差,是以也放棄了。

接着來說一下 GoReplay 的優缺點,先說優點:

單體程式,除了 pcap 庫,沒有其他依賴,也無需配置,是以環境準備很簡單

本身是個可執行程式,可直接運作,很輕量。隻要傳入合适的參數就能錄制,易使用

github 上的 star 數較多,知名度較大,且社群活躍

支援流量過濾功能、按倍速回放功能、回放時改寫接口參數等功能,功能上貼合我們的需求

資源消耗小,不侵入業務應用 JVM 運作時環境,對目标應用影響較小

對于以 Java 技術棧為基礎的公司來說,GoReplay 由于是 Go 語言開發的,技術棧差異很大,日後的維護和拓展是個大問題。是以單憑這一點,淘汰掉這個選型也是很正常的。但由于其優點也相對突出,綜合其他選型的優缺點考慮後,我們最終還是選擇了 GoReplay 作為最終的選型。最後大家可能會疑惑,為啥不選擇 tcpdump。原因有兩點,我們的需求比較少,用 tcpdump 有種大炮打蚊子的感覺。另一方面,tcpdump 給我們的感覺是太複雜了,駕馭不住(流下了沒有技術的眼淚😭),是以我們一開始就沒怎麼考慮過這個選型。

選型

語言

是否開源

優點

缺點

GoReplay

Go

1. 開源項目,代碼簡單,友善定制

2. 單體持續,依賴少,無需配置,環境準備簡單

3. 工具很輕量,易使用

3. 功能相對豐富,能夠滿足我們所有的需求

4. 自帶回放功能,能夠直接使用錄制資料,無需單獨開發

5. 資源消耗少,且不侵入目标應用的 JVM 運作時環境,影響小

6. 提供了插件機制,且插件實作不限制語言,友善拓展

1. 應用不夠廣泛,無大公司背書,成熟度不夠

2. 問題比較多,1.2.0 版本官方直接不推薦使用

3. 接上一條,對使用者的要求較高,出問題情況下要能自己讀源碼解決,官方響應速度一般

4. 社群版隻支援 HTTP 協定,不支援二進制協定,且核心邏輯與 HTTP 協定耦合了,拓展較麻煩

5. 隻支援指令行啟動,沒有内置服務,不好進行內建

JVM-Sandbox

jvm-sandbox-repeater

Java

1. 通過增強的方式,可以直接對 Java 類方法進行錄制,十分強大

2. 功能比較豐富,較為符合需求

3. 對業務代碼透明無侵入

1. 會對應用運作時環境有一定侵入,如果發生問題,對應用可能會造成影響

2. 工具本身仍然偏向測試回歸,是以導緻一些功能在我們的場景下沒法使用,比如不能使用它的回放功能進行高倍速壓測

3. 社群活躍度較低,有停止維護的風險

4. 底層實作确實比較複雜,維護成本也比較高。再次留下了沒有技術的眼淚😢

5. 需要搭配其他的輔助系統,整合成本不低

ngx_http_mirror_module

C

1. nginx 出品,成熟度可以保證

2. 配置比較簡單

1. 不友善啟停,也不支援過濾

2. 必須和 nginx 搭配隻用,是以使用範圍也比較受限

阿裡雲引擎回歸測試平台

-

選型完成後,緊接着要進行功能、性能、資源消耗等方面的驗證,測試選型是否符合要求。根據我們的需求,做了如下的驗證:

錄制功能驗證,驗證流量錄制的是否完整,包含請求數量完整性和請求資料準确性。以及在流量較大情況下,資源消耗情況驗證

流量過濾功能驗證,驗證能否過濾指定接口的流量,以及流量的完整性

回放功能驗證,驗證流量回放是否能如預期工作,回放的請求量是否符合預期

倍速回放驗證,驗證倍速功能是否符合預期,以及高倍速回放下資源消耗情況

以上幾個驗證當時線上下都通過了,效果很不錯,大家也都挺滿意的。可是倍速回放這個功能,在生産環境上進行驗證時,回放壓力死活上不去,隻能壓到約 600 的 QPS。之後不管再怎麼增壓,QPS 始終都在這個水位。我們與業務線同僚使用不同的錄制資料線上上測試了多輪均不行,開始以為是機器資源出現了瓶頸。可是我們看了 CPU 和記憶體消耗都非常低,TCP 連接配接數和帶寬也是很富餘的,是以資源是不存在瓶頸的。這裡也凸顯了一個問題,早期我們隻對工具做了功能測試,沒有做性能測試,導緻這個問題沒有盡早暴露出來。于是我自己線上下用 nginx 和 tomcat 搭建了一個測試服務,進行了一些性能測試,發現随随便便就能壓到幾千的 QPS。看到這個結果啼笑皆非,腦裂了😭。後來發現是因為線下的服務的 RT 太短了,與線上差異很大導緻的。于是讓線程随機睡眠幾十到上百毫秒,此時效果和線上很接近。到這裡基本上能夠大緻确定問題範圍了,應該是 GoReplay 出現了問題。但是 GoReplay 是 Go 語言寫的,大家對 Go 語言都沒經驗。眼看着問題解決唾手可得,可就是無處下手,很窒息。後來大佬們拍闆決定投入時間深入 GoReplay 源碼,通過分析源碼尋找問題,自此我開始了 Go 語言的學習之路。原計劃兩周給個初步結論,沒想到一周就找到了問題。原來是因為 GoReplay v1.1.0 版本的使用文檔與代碼實作出現了很大的偏差,導緻按照文檔操作就是達不到預期效果。具體細節如下:

流量錄制與回放技術實踐

圖2:GoReplay 使用說明

先來看看坑爹的文檔是怎麼說的,<code>--output-http-workers</code> 這個參數表示有多少個協程同時用于發生 http 請求,預設值是0,也就是無限制。再來看看代碼(output_http.go)是怎麼實作的:

流量錄制與回放技術實踐

圖3:GoRepaly 協程并發數決策邏輯

文檔裡說預設 http 發送協程數無限制,結果代碼裡設定了 10,差異太大了。為什麼 10 個協程不夠用呢,因為協程需要原地等待響應結果,也就是會被阻塞住,是以10個協程能夠打出的 QPS 是有限的。原因找到後,我們明确設定 --output-http-workers 參數值,倍速回放的 QPS 最終驗證下來能夠達到要求。

這個問題發生後,我們對 GoReplay 産生了很大的懷疑,感覺這個問題比較低級。這樣的問題都會出現,那後面是否還會出現有其他問題呢,是以用起來心裡發毛。當然,由于這個項目維護的人很少,基本可以認定是個人項目。且該項目經過沒有大規模的應用,尤其沒有大公司的背書,出現這樣的問題也能了解,沒必要太苛責。是以後面碰到問題隻能見招拆招了,反正代碼都有了,直接白盒審計吧。

先說說選型過程中存在的問題吧。從上面的描述上來看,我在選型和驗證過程均犯了一些較為嚴重的錯誤,被自己生動的上了一課。在選型階段,對于知名度,居然認為 star 比較多就算比較有名了,現在想想還是太幼稚了。比起知名度,成熟度其實更重要,穩定坑少下班早🤣。另外,可觀測性也一定要考慮,否則查問題時你将體驗到什麼是無助感。

在驗證階段,功能驗證沒有太大問題。但性能驗證隻是象征性的搞了一下,最終在與業務線同僚一起驗證時翻車了。是以驗證期間,性能測試是不能馬虎的,一旦相關問題上線後才發現,那就很被動了。

根據這次的技術選型經曆做個總結,以後搞技術選型時再翻出來看看。選型次元總結如下:

次元

說明

功能性

1. 選型的功能是否能夠滿足需求,如果不滿足,二次開發的成本是怎樣的

成熟度

1. 在相關領域内,選型是否經過大範圍使用。比如 Java Web 領域,Spring 技術棧基本人盡皆知

2. 一些小衆領域的選型可能應用并不是很廣泛,那隻能自己多去看看 issue,搜尋一些踩坑記錄,自行評估了

可觀測性

1. 内部狀态資料是否有觀測手段,比如 GoReplay 會把内部狀态資料定時列印出來

2. 方不友善接入公司的監控系統也要考慮,畢竟人肉觀察太費勁

驗證總結如下:

根據要求一項一項的去驗證選型的功能是否符合預期,可以搞個驗證的 checklist 出來,逐項确認

從多個可能的方面對選型進行性能測試,在此過程中注意觀察各種資源消耗情況。比如 GoReplay 流量錄制、過濾和回放功能都是必須要做性能測試的

對選型的長時間運作的穩定性要進行驗證,對驗證期間存在的異常情況注意觀測和分析

更嚴格一點,可以做一些故障測試。比如殺程序,斷網等

關于選型更詳細的實戰經驗,可以參考李運華大佬的文章:如何正确的使用開源項目。

當技術選型和驗證都完成後,接下來就是要把想法變為現實的時候了。按照現在小步快跑,快速疊代的模式,啟動階段通常我們僅會規劃最核心的功能,保證流程走通。接下來再根據需求的優先級進行疊代,逐漸完善。接下來,我将在按照項目的疊代過程來進行介紹。

序号

分類

需求點

1

錄制

流量過濾,按需錄制

支援按 HTTP 請求路徑過濾流量,這樣可以錄制指定接口的流量

2

錄制時長可指定

可設定錄制時長,一般情況下都是錄制10分鐘,把流量波峰錄制下來

3

錄制任務詳情

包含錄制狀态、錄制結果統計等資訊

4

回放

回放時長可指定

支援設定 1 ~ 10 分鐘的回放時長

5

回放倍速可指定

根據錄制時的 QPS,按倍數進行流量放大,最小粒度為 1 倍速

6

回放過程允許人為終止

在發現被壓測應用出現問題時,可人為終止回放過程

7

回放任務詳情

包含回放狀态、回放結果統計

以上就是項目啟動階段的需求清單,這些都是最基本需求。隻要完成這些需求,一個最小可用的系統就實作了。

流量錄制與回放技術實踐

圖4:壓測系統一期架構圖

上面的架構圖經過編輯,與實際有一定差異,但不影響講解。需要說明的是,我們的網關服務、壓測機以及壓測服務都是分别由多台構成,所有網關和壓測執行個體均部署了 GoRepaly 及其控制器。這裡為了簡化架構圖,隻畫了一台機器。下面對一些核心流程進行介紹。

在介紹其他内容之前,先說一下 Gor 控制器的用途。用一句話介紹:引入這個中間層的目的是為了将 GoReplay 這個指令行工具與我們的壓測系統進行整合。這個子產品是我們自己開發,最早使用 shell 編寫的(苦不堪言😭),後來用 Go 語言重寫了。Gor 控制器主要負責下面一些事情:

掌握 GoRepaly 生殺大權,可以調起和終止 GoReplay 程式

屏蔽掉 GoReplay 使用細節,降低複雜度,提高易用性

回傳狀态,在 GoReplay 啟動前、結束後、其他标志性事件結束後都會向壓測系統回傳狀态

對錄制和回放産生資料進行處理與回傳

打日志,記錄 GoRepaly 輸出的狀态資料,便于後續排查

GoReplay 本身隻提供最基本的功能,可以把其想象成一個隻有底盤、輪子、方向盤和發動機等基本配件的汽車,雖然能開起來,但是比較費勁。而我們的 Gor 控制器相當于在其基礎上提供了一鍵啟停,轉向助力、車聯網等增強功能,讓其變得更好用。當然這裡隻是一個近似的比喻,不要糾結合理性哈。知曉控制器的用途後,下面介紹啟動和回放的執行過程。

使用者的錄制指令首先會發送給壓測服務,壓測服務原本可以通過 SSH 直接将錄制指令發送給 Gor 控制器的,但出于安全考慮必須繞道運維系統。Gor 控制器收到錄制指令後,參數驗證無誤,就會調起 GoReplay。錄制結束後,Gor 控制器會将狀态回傳給壓測系統,由壓測判定錄制任務是否結束。詳細的流程如下:

使用者設定錄制參數,送出錄制請求給壓測服務

壓測服務生成壓測任務,并根據使用者指定的參數生成錄制指令

錄制指令經由運維系統下發到具體的機器上

Gor 控制器收到錄制指令,回傳“錄制即将開始”的狀态給壓測服務,随後調起 GoReplay

錄制結束,GoReplay 退出,Gor 控制器回傳“錄制結束”狀态給壓測服務

Gor 控制器回傳其他資訊給壓測系統

壓測服務判定錄制任務結束後,通知壓測機将錄制資料讀取到本地檔案中

錄制任務結束

這裡說明一下,要想使用 GoReplay 倍速回放功能,必須要将錄制資料存儲到檔案中。然後通過下面的參數設定倍速:

回放過程與錄制過程基本相似,隻不過回放的指令是固定發送給壓測機的,具體過程就不贅述了。下面說幾個不同點:

給回放流量打上壓測标:回放流量要與真實流量區分開,需要一個标記,也就是壓測标

按需改寫參數:比如把 user-agent 改為 goreplay,或者增加測試賬号的 token 資訊

GoReplay 運作時狀态收集:包含 QPS,任務隊列積壓情況等,這些資訊可以幫助了解 GoReplay 的運作狀态

這個最小可用系統線上上差不多運作了4個月,沒有出現過太大的問題,但仍然有一些不足之處。主要有兩點:

指令傳遞的鍊路略長,增大的出錯的機率和排查的難度。比如運維系統的接口偶爾失敗,關鍵還沒有日志,一開始根本沒法查問題

Gor 控制器是用 shell 寫的,約 300 行。shell 文法和 Java 差異比較大,代碼也不好調試。同時對于複雜的邏輯,比如生成 JSON 字元串,寫起來很麻煩,後續維護成本較高

這兩點不足一直伴随着我們的開發和運維工作,直到後面進行了一些優化,才算是徹底解決掉了這些問題。

流量錄制與回放技術實踐

圖5:Gor 控制器優化後的架構圖

針對前面存在的痛點,我們進行了針對性的改進。重點使用 Go 語言重寫了 gor 控制器,新的控制器名稱為 gor-server。從名稱上可以看出,我們内置了一個 HTTP 服務。基于這個服務,壓測服務下發指令終于不用再繞道運維系統了。同時所有的子產品都在我們的掌控中,開發和維護的效率明顯變高了。

我們内部采用 Dubbo 作為 RPC 架構,應用之間的調用均是通過 Dubbo 來完成的,是以我們對 Dubbo 流量錄制也有較大的需求。在針對網關流量錄制取得一定成果後,一些負責内部系統的同僚也希望通過 GoReplay 來進行壓測。為了滿足内部的使用需求,我們對 GoReplay 進行了二次開發,以便支援 Dubbo 流量的錄制與回放。

要對 Dubbo 錄制進行支援,需首先搞懂 Dubbo 協定内容。Dubbo 是一個二進制協定,它的編碼規則如下圖所示:

流量錄制與回放技術實踐

圖6:Dubbo 協定圖示;來源:Dubbo 官方網站

下面簡單對協定做個介紹,按照圖示順序依次介紹各字段的含義。

字段

位數(bit)

含義

Magic High

8

魔數高位

固定為 0xda

Magic Low

魔數低位

固定為 0xbb

Req/Res

資料包類型

0 - Response

1 - Request

2way

調用方式

0 - 單向調用

1 - 雙向調用

Event

事件辨別

比如心跳事件

Serialization ID

序列化器編号

2 - Hessian2Serialization

3 - JavaSerialization

4 - CompactedJavaSerialization

6 - FastJsonSerialization

......

Status

響應狀态

狀态清單如下:

20 - OK

30 - CLIENT_TIMEOUT

31 - SERVER_TIMEOUT

40 - BAD_REQUEST

50 - BAD_RESPONSE

Request ID

64

請求 ID

響應頭中也會攜帶相同的 ID,用于将請求和響應關聯起來

Data Length

32

資料長度

用于辨別 Variable Part 部分的長度

Variable Part(payload)

資料載荷

知曉了協定内容後,我們把官方的 demo 跑起來,抓個包研究一下。

流量錄制與回放技術實踐

圖7:dubbo 請求抓包

首先我們可以看到占用兩個位元組的魔數 0xdabb,接下來的14個位元組是協定頭中的其他内容,簡單分析一下:

流量錄制與回放技術實踐

圖8:dubbo 請求頭資料分析

上面标注的比較清楚了,這裡稍微解釋一下。從第三個位元組可以看出這個資料包是一個 Dubbo 請求,因為是第一個請求,是以請求 ID 是 0。資料的長度是 0xdc,換算成十進制為 220 個位元組。加上16個位元組的消息頭,總長度正好是 236,與抓包結果顯示的長度是一緻。

我們對 Dubbo 流量錄制進行支援,首先需要按照 Dubbo 協定對資料包進行解碼,以判斷錄制到的資料是不是 Dubbo 請求。那麼問題來了,如何判斷所錄制到的 TCP 封包段裡的資料是 Dubbo 請求呢?答案如下:

首先判斷資料長度是不是大于等于協定頭的長度,即 16 個位元組

判斷資料前兩個位元組是否為魔數 0xdabb

判斷第17個比特位是不是 1,不為1可丢棄掉

通過上面的檢測可快速判斷出資料是否符合 Dubbo 請求格式。如果檢測通過,那接下來又如何判斷錄制到的請求資料是否完整呢?答案是通過比較錄制到的資料長度 L1 和 Data Length 字段給出的長度 L2,根據比較結果進行後續操作。有如下幾種情況:

L1 == L2,說明資料接收完整,無需額外的處理邏輯

L1 &lt; L2,說明還有一部分資料沒有接收,繼續等待餘下資料

L1 &gt; L2,說明多收到了一些資料,這些資料并不屬于目前請求,此時要根據 L2 來切分收到的資料

三種情況示意圖如下:

流量錄制與回放技術實踐

圖9:應用層接收端幾種情況

看到這裡,肯定有同學想說,這不就是典型的 TCP “粘包”和“拆包”問題。不過我并不想用這兩個詞來說明上述的一些情況。TCP 是一個面向位元組流的協定,協定本身并不存在所謂的“粘包”和“拆包”問題。TCP 在傳輸資料過程中,并不會理會上層資料是如何定義的,在它看來都是一個個的位元組罷了,它隻負責把這些位元組可靠有序的運送到目标程序。至于情況2和情況3,那是應用層應該去處理的事情。是以,我們可以在 Dubbo 的代碼中找到相關的處理邏輯,有興趣的同學可以閱讀 NettyCodecAdapter.InternalDecoder#decode 方法代碼。

本小節内容就到這裡,最後給大家留下一個問題。在 GoReplay 的代碼中,并沒有對情況3進行處理。為什麼錄制 HTTP 協定流量不會出錯?

GoReplay 社群版目前隻支援 HTTP 流量錄制,其商業版支援部分二進制協定,但不支援 Dubbo。是以為了滿足内部使用需求,隻能進行二次開發了。但由于社群版代碼與 HTTP 協定處理邏輯耦合比較大,是以想要支援一種新的協定錄制,還是比較麻煩的。在我們的實作中,對 GoReplay 的改造主要包含 Dubbo 協定識别,Dubbo 流量過濾,資料包完整性判斷等。資料包的解碼和反序列化則是交給 Java 程式來實作的,序列化結果轉成 JSON 進行存儲。效果如下:

流量錄制與回放技術實踐

圖10:Dubbo 流量錄制效果

GoReplay 用三個猴頭 🐵🙈🙉 作為請求分隔符,第一眼看到感覺挺搞笑的。

大家可能很好奇 GoReplay 是怎麼和 Java 程式配合工作的,原理倒也是很簡單。先看一下怎麼開啟 GoReplay 的插件模式:

通過 middleware 參數可以傳遞一條指令給 GoRepaly,GoReplay 會拉起一個程序執行這個指令。在錄制過程中,GoReplay 通過擷取程序的标準輸入和輸出與插件程序進行通信。資料流向大緻如下:

Dubbo 協定的解碼還是比較容易實作的,畢竟很多代碼 Dubbo 架構已經寫好了,我們隻需要按需對代碼進行修改定制即可。協定頭的解析邏輯在 DubboCodec#decodeBody 方法中,消息體的解析邏輯在 DecodeableRpcInvocation#decode(Channel, InputStream) 方法中。由于 GoReplay 已經對數資料進行過解析和處理,是以在插件裡很多字段就沒必要解析了,隻要解析出 Serialization ID 即可。這個字段将指導我們進行後續的反序列化操作。

對于消息體的解碼稍微麻煩點,我們把 DecodeableRpcInvocation 這個類代碼拷貝一份放在插件項目中,并進行了修改。删除了不需要的邏輯,隻保留了 decode 方法,将其變成了工具類。考慮到我們的插件不友善引入要錄制應用的 jar 包,是以在修改 decode 方法時,還要注意把和類型相關的邏輯移除掉。修改後的代碼大緻如下:

僅從代碼開發的角度來說,難度并不是很大,當然前提是要對 Dubbo 的源碼有一定的了解。對我來說,時間主要花在 GoRepaly 的改造上,主要原因是對 Go 語言不熟,邊寫邊查導緻效率很低。當功能寫好,調試完畢,看到結果正确輸出,确實很開心。但是,這種開心也僅維持了很短的時間。不久在與業務同僚進行線上驗證的時候,插件花樣崩潰,場面一度十分尴尬。報錯資訊看的我一臉懵逼,一時半會解決不了,為了保留點臉面,趕緊終止了驗證🤪。事後排查發現,在将一些的特殊的反序列化資料轉化成 JSON 格式時,出現了死循環,造成 StackOverflowError 錯誤發生。由于插件主流程是單線程的,且僅捕獲了 Exception,是以造成了插件錯誤退出。

流量錄制與回放技術實踐

圖11:循環依賴導緻 Gson 架構報錯

這個錯誤告訴我們,類之間出現了循環引用,我們的插件代碼也确實沒有對循環引用進行處理,這個錯誤發生是合理的。但當找到造成這個錯誤的業務代碼時,并沒找到循環引用,直到我本地調試時才發現了存在某種問題或陰謀。業務代碼類似的代碼如下:

問題出在了内部類上,Inner 會隐式持有 Outer 引用。不出意外,這應該是編譯器幹的。源碼面前了無秘密,我們把内部類的 class 檔案反編譯一下,一切就明了了。

流量錄制與回放技術實踐

圖12:内部類反編譯結果

這應該算是 Java 基本知識了,奈何平時用的少,第一眼看到代碼時,沒看出了隐藏在其中的循環引用。到這裡解釋很合理,這就結束了麼?其實還沒有,實際上 Gson 序列化 Outer 時并不會報錯,調試發現其會排除掉 <code>this$0</code> 這個字段,排除邏輯如下:

那麼我們在把錄制的流量轉成 JSON 時為什麼會報錯呢?原因是我們的插件反序列化時拿不到接口參數的類型資訊,是以我們把參數反序列化成了 <code>Map</code> 對象,這樣 <code>this$0</code> 這個字段和值也會作為鍵值對存儲到 Map 中。此時 Gson 的過濾規則就不生效了,沒法過濾掉 <code>this$0</code> 這個字段,造成了死循環,最終導緻棧溢出。知道原因後,這麼問題怎麼解決呢?下一小節展開。

我開始考慮是不是可以人為清洗一下 Map 裡的資料,但發現好像很難搞。如果 Map 的資料結構很複雜,比如嵌套了很多層,清洗邏輯可能不好實作。還有我也不清楚這裡面會不會有其他的一些彎彎繞,是以放棄了這個思路,這種髒活累活還是丢給反序列化工具去做吧。我們要想辦法把拿到接口的參數類型,插件怎麼拿到業務應用 api 的參數類型呢?一種方式是在插件啟動時,把目标應用的 jar 包下載下傳到本地,然後由單獨的類加載器進行加載。但這裡會有一個問題,業務應用的 api jar 包裡面也存在着一些依賴,這些依賴難道要遞歸去下載下傳?第二種方式,則簡單粗暴點,直接在插件項目中引入業務應用 api 依賴,然後打成 fat jar。這樣既不需要搞單獨的類加載器,也不用去遞歸下載下傳其他的依賴。唯一比較明顯的缺點就是會在插件項目 pom 中引入一些不相關的依賴,但與收益相比,這個缺點根本算不上什麼。為了友善,我們把很多業務應用的 api 都依賴了進來。一番操作後,我們得到了如下的 pom 配置:

接着要改一下 RpcInvocationCodec#decode 方法,其實就是把代碼還原回去😓:

代碼調整完畢,擇日在上線驗證,一切正常,可喜可賀。但不久後,我發現這裡面存在着一些隐患。如果哪天線上上發生了,将會給排查工作帶來比較大的困難。

考慮這樣的情況,業務應用 A 和應用 B 的 api jar 包同時依賴了一些内部的公共包,公共包的版本可能不一緻。這時候,我們怎麼處理依賴沖突?如果内部的公共包做的不好,存在相容性問題怎麼辦。

流量錄制與回放技術實踐

圖13:依賴沖突示意圖

比如這裡的 common 包版本沖突了,而且 3.0 不相容 1.0,怎麼處理呢?

簡單點處理,我們就不在插件 pom 裡依賴所有的業務應用的 api 包了,而是隻依賴一個。但是壞處是,每次都要為不同的應用單獨建構插件代碼,顯然我們不喜歡這樣的做法。

再進一步,我們不在插件中依賴業務應用的 api 包,保持插件代碼幹淨,就不用每次都打包了。那怎麼擷取業務應用的 api jar 包呢?答案是為每個 api jar 專門建個項目,再把項目打成 fat jar,插件代碼使用自定義類加載器去加載業務類。插件啟動時,根據配置去把 jar 包下載下傳到機器上即可。每次隻需要加載一個 jar 包,是以也就不存在依賴沖突問題了。做到這一步,問題就可以解決了。

更進一步,早先在閱讀阿裡開源的 jvm-sandbox 項目源碼時,發現了這個項目實作了一種帶有路由功能的類加載器。那我們的插件能否也搞個類似的加載器呢?出于好奇,嘗試了一下,發現是可以的。最終的實作如下:

流量錄制與回放技術實踐

圖14:自定義類加載機制示意圖

一級類加載器具備根據包名“片段”進行路由的功能,二級類加載器負責具體的加載工作。應用 api jar 包統一放在一個檔案夾下,隻有二級類加載器可以進行加載。對于 JDK 中的一些類,比如 List,還是要交給 JVM 内置的類加載器進行加載。最後說明一下,搞這個帶路由功能的類加載器,主要目的是為了玩。雖然能達到目的,但在實際項目中,還是用上一種方法穩妥點。

我們的流量錄制與回放系統主要的,也是當時唯一的使用場景是做壓測。系統穩定後,我們也在考慮還有沒有其他的場景可以搞。正好在技術選型階段試用過 jvm-sandbox-repeater,這個工具主要應用場景是做流量對比測試。對于代碼重構這種不影響接口傳回值結構的改動,可以通過流量對比測試來驗證改動是否有問題。由于大佬們覺得 jvm-sandbox-repeater 和底層的 jvm-sandbox 有點重,技術複雜度也比較高。加之沒有資源來開發和維護這兩個工具,是以希望我們基于流量錄制和回放系統來做這個事情,先把流程跑通。

項目由 QA 團隊主導,流量重放與 diff 功能由他們開發,我們則提供底層的錄制能力。系統的工作示意圖如下:

流量錄制與回放技術實踐

圖15:對比測試示意圖

我們的錄制系統為重放器提供實時的流量資料,重放器拿到資料後立即向預發和線上環境重放。重放後,重放器可以分别拿到兩個環境傳回的結果,然後再把結果傳給比對子產品進行後續的比對。最後把比對結果存入到資料庫中,比對過程中,使用者可以看到哪些請求比對失敗了。對于錄制子產品來說,要注意過濾重放流量。否則會造成接口 QPS 倍增,重放變壓測了🤣,喜提故障一枚。

這個項目上線3個月,幫助業務線發現了3個比較嚴重的 bug,6個一般的問題,價值初現。雖然項目不是我們主導的,但是作為底層服務的提供方,我們也很開心。期望未來能為我們的系統拓展更多的使用場景,讓其成長為一棵枝繁葉茂的大樹。

截止到文章釋出時間,項目上線接近一年的時間了。總共有5個應用接入使用,錄制和回放次數累計差不多四五百次。使用資料上看起來有點寒碜,主要是因為公司業務是 toB 的,對壓測的需求并沒那麼多。盡管使用資料比較低,但是作為壓測系統,還是發揮了相應價值。主要包含兩方面:

性能問題發現:壓測平台共為業務線發現了十幾個性能問題,幫助中間件團隊發現了6個嚴重的基礎元件問題

使用效率提升:新的壓測系統功能簡單易用,僅需10分鐘就能完成一次線上流量錄制。相較于以往單人半天才能完成的事情,效率至少提升了 20 倍,使用者體驗大幅提升。一個佐證就是目前 90% 以上的壓測任務都是在新平台上完成的。

可能大家對效率提升資料有所懷疑,大家可以思考一下沒有錄制工具如何擷取線上流量。傳統的做法是業務開發修改接口代碼,加一些日志,這要注意日志量問題。之後,把改動的代碼釋出到線上,對于一些比較大的應用,一次釋出涉及到幾十台機器,還是相當耗時的。接着,把接口參數資料從日志檔案中清洗出來。最後,還要把這些資料轉換成壓測腳本。這就是傳統的流程,每個步驟都比較耗時。當然,基建好的公司,可以基于全鍊路追蹤平台拿到接口資料。但對于大多數公司來說,可能還是要使用傳統的方式。而在我們的平台上,隻需要選擇目标應用和接口、錄制時長、點選錄制按鈕就行了,使用者操作僅限這些,是以效率提升還是很明顯的。

項目項目雖然已經上線一年,但由于人手有限,目前基本隻有我一個人在開發維護,是以疊代還是比較慢的。針對目前在實踐中碰到的一些問題,這裡把幾個明顯的問題,希望未來能夠一一解決掉。

1.全鍊路節點壓力圖

目前在壓測的時候,壓測人員需要到監控平台上打開很多個應用的監控頁面,壓測期間需要在多個應用監控之間進行切換。希望未來可以把全鍊路上各節點的壓力圖展示出來,同時可以把節點的報警資訊發送給壓測人員,降低壓測的監視成本。

2.壓測工具狀态收集與可視化

壓測工具自身有一些很有用的狀态資訊,比如任務隊列積壓情況,目前的協程數等。這些資訊在壓測壓力上不去時,可以幫助我們排查問題。比如任務隊列任務數在增大,協程數也保持高位。這時候能推斷出什麼原因嗎?大機率是被壓應用壓力太大,導緻 RT 變長,進而造成施壓協程(數量固定)長時間被阻塞住,最終導緻隊列出現積壓情況。GoReplay 目前這些狀态資訊輸出到控制台上的,檢視起來還是很不友善。同時也沒有告警功能,隻能在出問題時被動去檢視。是以期望未來能把這些狀态資料放到監控平台上,這樣體驗會好很多。

3.壓力感覺與自動調節

目前壓測系統更沒有對業務應用的壓力進行感覺,不管壓測應用處于什麼狀态,壓測系統都會按照既定的設定進行壓測。當然由于 GoReplay 并發模型的限制,這個問題目前不用擔心。但未來不排除 GoReplay 的并發模型會發生變化,比如隻要任務隊列裡有任務,就立即起個協程發送請求,此時就會對業務應用造成很大的風險。

還有一些問題,因為重要程度不高,這裡就不寫了。總的來說,目前我們的壓測需求還是比較少,壓測的 QPS 也不高,導緻很多優化都沒法做。比如壓測機性能調優,壓測機器動态擴縮容。但想想我們就4台壓測機,預設配置完全可以滿足需求,是以這些問題都懶得去折騰🤪。當然從個人技術能力提升的角度來說,這些優化還是很有價值的,有時間可以玩玩。

1. 入門 Go 語言

由于 GoReplay 是 Go 語言開發的,而且我們在使用中确實也遇到了一些問題,不得不深入源碼排查。為了更好的掌控工具,友善排查問題和二次開發,是以專門學習了 Go 語言。目前的水準處于入門階段,菜鳥水準。用 Java 用久了,剛開始學習 Go 語言還是很懵逼的。比如 Go 的方法定義:

當時感覺這個文法非常的奇怪,Area 方法名前面的聲明是什麼鬼。好在我還有點 C 語言的知識,轉念一想,如果讓 C 去實作面向對象又該如何做呢?

搞懂了上面的代碼,就知道 Go 的方法為什麼要那麼定義了。

随着學習的深入,發現 Go 的文法特性和 C 還真的很像,居然也有指針的概念,21 世紀的 C 語言果然名不虛傳。于是在學習過程中,會不由自主的對比兩者的特性,按照 C 的經驗去學習 Go。是以當我看到下面的代碼時,非常的驚恐。

當時預期作業系統會無情的抛個 segmentation fault 錯誤給我,但是編譯運作居然沒有問題...問..題..。難道是我錯了?再看一遍,心想沒問題啊,C 語言裡不能傳回棧空間的指針,Go 語言也不應該這麼操作吧。這裡就展現出兩個語言的差別了,上面的 Rectangle 看起來像是在棧空間裡配置設定到,實際上是在堆空間裡配置設定的,這個和 Java 倒是一樣的。

總的來說,Go 文法和 C 比較像,加之 C 語言是我的啟蒙程式設計語言。多以對于 Go 語言,也是感覺非常親切和喜歡的。其文法簡單,标準庫豐富易用,使用體驗不錯。當然,由于我目前還在新手村混,沒有用 Go 寫過較大的工程,是以對這個語言的認識還比較淺薄。以上有什麼不對的地方,也請大家見諒。

2. 較為熟練掌握了 GoReplay 原理

GoReplay 錄制和回放核心的邏輯基本都看了一遍,并且在内網也寫過文章分享,這裡簡單和大家聊聊這個工具。GoReplay 在設計上,抽象出了一些概念,比如用輸入和輸出來表示資料來源與去向,用介于輸入和輸出子產品之間的中間件實作拓展機制。同時,輸入和輸出可以很靈活的組合使用,甚至可以組成一個叢集。

流量錄制與回放技術實踐

圖16:GoReplay 叢集示意圖

錄制階段,每個 tcp 封包段被抽象為 packet。當資料量較大,需要分拆成多個封包段發送時,收端需要把這些封包段按順序序組合起來,同時還要處理亂序、重複封包等問題,保證向下一個子產品傳遞的是一個完整無誤的 HTTP 資料。這些邏輯統封裝在了 tcp_message 中,tcp_message 與 packet 是一對多的關系。後面的邏輯會将 tcp_message 中的資料取出,打上标記,傳遞給中間件(可選)或者是輸出子產品。

回放階段流程相對簡單,但仍然會按照 輸入 → [中間件] → 輸出 流程執行。通常輸入子產品是 input-file,輸出子產品是 output-http。回放階段一個有意思的點是倍速回放的原理,通過按倍數縮短請求間的間隔實作加速功能,實作代碼也很簡單。

總的來說,這個工具的核心代碼并不多,但是功能還是比較豐富的,可以體驗一下。

3. 對 Dubbo 架構和類加載機制有了更多的認知

在實作 Dubbo 流量錄制時,基本上把解碼相關的邏輯看了一遍。當然這塊邏輯以前也看過,還寫過文章。隻不過這次要去定制代碼,還是會比單純的看源碼寫文章了解的更深入一些,畢竟要去處理一些實際的問題。在此過程中,由于需要自定義類加載器,是以對類加載機制也有了更多的認識,尤其是那個帶路由功能的類加載器,還是挺好玩的。當然,學會這些技術也沒什麼大不了的,重點還是能夠發現問題,解決問題。

4. 其他收獲

其他的收獲都是一些比較小的點,這裡就不多說了,以問題的形式留給大家思考吧。

TCP 協定會保證向上層有序傳遞資料,為何工作在應用層的 GoReplay 還要處理亂序資料?

HTTP 1.1 協定通信過程是怎樣的?如果在一個 TCP 連接配接上連續發送兩個 HTTP 請求會造成什麼問題?

1. 技術選型要慎重

開始搞選型沒什麼經驗,考察次元很少,不夠全面。這就導緻了幾個問題,首先在驗證階段工具一直達不到預期,耽誤了不少時間。其次在後續的疊代期間,發現 GoReplay 的小問題比較多,感覺嚴謹程度不夠。比如 1.1.0 版本使用文檔和代碼有很多處差異,使用時要小心。再比如使用過程中,發現 1.3.0-RC1 版本中存在資源洩露問題 #926,順手幫忙修複了一下 #927。當然 RC 版本有問題也很正常,但是這麼明顯的問題說實話不應該出。不過考慮到這個項目是個人維護的,也不能要求太多。但是對于使用者來說,還是要當心。這種要在生産上運作的程式,不靠譜是很鬧心的事情。是以對于我個人而言,以後選型成熟度一定會排在第一位。對于個人維護的項目,盡量不作為靠前的候選項。

2. 技術驗證要全面

初期的選型沒有進行性能測試和極限測試,這就導緻問題線上上驗證時才發現。這麼明顯的問題,拖到這麼晚才發現,搞的挺尴尬的。是以對于技術驗證,要從不同的角度進行性能測試,極限測試。更嚴格一點,可以向李運華大佬在 如何正确的使用開源項目 文章中提的那樣,搞搞故障測試,比如殺程序,斷電等。把前期工作做足,避免後期被動。

3. 磨刀不誤砍柴工

這個項目涉及到不同的技術,公司現有的開發平台無法支援這種項目,是以打包和釋出是個麻煩事。在開發和測試階段會頻繁的修改代碼,如果手動進行打包,然後上傳的 FTP 伺服器上(無法直接通路線上機器),最後再部署到具體的錄制機器上,這是一件十分機械低效的事情。于是我寫了一個自動化建構腳本,來提升建構和部署效率,實踐證明效果挺好。從此心态穩定多了😀,很少進入暴躁模式了。

流量錄制與回放技術實踐

圖17:自動化建構腳本效果圖

十分尴尬的是,我在項目上線後才把腳本寫好,前期沒有享受到自動化的福利。不過好在後續的疊代中,自動化腳本還是幫了很大的忙。盡早實作編譯和打包自動化工具,有助于提高工作效率。盡管我們會覺得寫工具也要花不少時間,但如果可以預料到很多事情會重複很多次,那麼這些工具帶來的收益将會遠超付出。

非常幸運能夠參與并主導這個項目,總的來說,我個人還是從中學到了很多東西。這算是我職業生涯中第一個深度參與和持續疊代的項目,看着它的功能逐漸完善起來,穩定不間斷給大家提供服務,發揮出其價值。作為項目負責人,我還是非常開心驕傲的。但同時也有些遺憾的,由于公司的業務是 toB 的,對壓測系統的要求并不高。系統目前算是進入了穩定期,沒有太多可做的需求或者大的問題。我雖然可以私下做一些技術上的優化,但很難看出效果,畢竟現有的使用需求還沒達到系統瓶頸,提早優化并不是一個好主意。期望未來公司的業務能有大的發展,對壓測系統提出更高的要求,我也十分樂意繼續優化這個系統。另外,要感謝一起參與項目的同僚,他們的強力輸出得以讓項目在緊張的工期内保質保量上線,如期為業務線提供服務。好了,本篇文章到此結束,感謝閱讀。

本文在知識共享許可協定 4.0 下釋出,轉載請注明出處 作者:田小波 原創文章優先釋出到個人網站,歡迎通路:https://www.tianxiaobo.com
流量錄制與回放技術實踐

本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定進行許可。