01
背景
當前,Thrift 是字節內部主要使用的 RPC 序列化協議,在 CloudWeGo/Kitex 項目中優化和使用后,性能相比使用支持泛型編解碼的協議如 JSON 有較大優勢。但是在和業務團隊進行深入合作優化的過程中,我們發現一些特殊業務場景并不能享受靜態化代碼生成所帶來的高性能:- 動態反射:動態地 讀取、修改、裁剪 數據包中某些字段,如隱私合規場景中字段屏蔽;
- 數據編排:組合多個子數據包進行 排序、過濾、位移、歸并 等操作,如某些 BFF (Backend For Frontent) 服務;
- 協議轉換:作為代理將某種協議的數據轉換另一種協議,如 http-rpc 協議轉換網關。
- 泛化調用:需要秒級熱更新或迭代非常頻繁的 RPC 服務,如大量 Kitex 泛化調用(generic-call)用戶
不難發現,這些業務場景都具有難以統一定義靜態IDL的特點。即使可以通過分布式 sidecar 技術規避這個問題,也往往因為業務需要動態更新而放棄傳統代碼生成方式,訴諸某些自研或開源的 Thrift 泛型編解碼庫進行泛化 RPC 調用。我們經過性能分析發現,目前這些庫相比代碼生成方式有巨大的性能下降。以字節某 BFF 服務為例,僅僅 Thrift 泛化調用產生的 CPU 開銷占比就將近 40%,這幾乎是正常 Thrift RPC 服務的4到8倍。因此,我們自研了一套能動態處理 RPC 數據(不需要代碼生成)同時保證高性能的 Go 基礎庫 —— dynamicgo。
02
設計與實現
首先要搞清楚當前這些泛化調用庫性能為什么差呢?其核心原因是:采用了某種低效泛型容器來承載中間處理過程中的數據(典型如 thrift-iterator 中的 map[string]interface{})。眾所周知,Go 的堆內存管理代價是極高的 (GC +heap bitmap),而采用 interface 不可避免會帶來大量的內存分配。但實際上相當多的業務場景并不真正需要這些中間表示。比如 http-thrift API 網關中的純協議轉換場景,其本質訴求只是將 JSON(或其它協議)數據依據用戶 IDL 轉換為 Thrift 編碼(反之亦然),完全可以基于輸入的數據流逐字進行翻譯。同樣,我們也統計了抖音某 BFF 服務中泛化調用的具體代碼,發現真正需要進行讀(Get)和寫(Set)操作的字段占整個數據包字段不到5%,這種場景下完全可以對不需要的字段進行跳過(Skip)處理而不是反序列化。而 dynamicgo 的核心設計思想是:基于 原始字節流 和 動態類型描述 原地(in-place) 進行數據處理與轉換。為此,我們針對不同的場景設計了不同的 API 去實現這個目標。動態反射對于 thrift 反射代理的使用場景,歸納起來有如下使用需求:
- 有一套完整結構自描述能力,可表達 scalar 數據類型, 也可表達嵌套結構的映射、序列等關系;
- 支持增刪查改(Get/Set/Index/Delete/Add)與遍歷(ForEach);
- 保證數據可并發讀,但是不需要支持并發寫。等價于 map[string]interface{} 或 []interface{}
這里我們參考了 Go reflect 的設計思想,把通過IDL解析得到的準靜態類型描述(只需跟隨 IDL 更新一次)TypeDescriptor 和 原始數據單元 Node 打包成一個完全自描述的結構——Value,提供一套完整的反射 API。
//IDL類型描述
typeTypeDescriptorinterface{
Type()Type//數據類型
Name()string//類型名稱
Key()*TypeDescriptor//formapkey
Elem()*TypeDescriptor//forsliceormapelement
Struct()*StructDescriptor//forstruct
}
//純TLV數據單元
typeNodestruct{
tType//數據類型
vunsafe.Pointer//buffer起始位置
lint//數據單元長度
}
//Node+類型描述descriptor
typeValuestruct{
Node
Descthrift.TypeDescriptor
}
這樣,只要保證 TypeDescriptor 包含的類型信息足夠豐富,以及對應的 thrift 原始字節流處理邏輯足夠健壯,甚至可以實現數據裁剪、聚合等各種復雜的業務場景。
協議轉換
協議轉換的過程可以通過有限狀態機(FSM)來表達。以 JSON->Thrift 流程為例,其轉換過程大致為:
- 預加載用戶 IDL,轉換為運行時的動態類型描述 TypeDescriptor;
- 從輸入字節流中讀取一個 json 值,并判斷其具體類型(object/array/string/number/bool/null):
- 如果是 object 類型,繼續讀取一個 key,再通過對應的 STRUCT 類型描述找到匹配字段的子類型描述;
- 如果是 array 類型,遞歸查找類型描述的子元素類型描述;
- 其它類型,直接使用當前類型描述。
- 基于得到的動態類型描述信息,將該值轉換為等價的 Thrift 字節,寫入到輸出字節流中 ;
- 更新輸入和輸出字節流位置,跳回2進行循環處理,直到輸入終止(EOF)。
圖1 JSON2Thrift 數據轉換流程
整個過程可以完全做到 in-place 進行,僅需為輸出字節流分配一次內存即可。
數據編排
與前面兩個場景稍微有所不同,數據編排場景下可能涉及數據位置的改變(異構轉換),并且往往會訪問大量數據節點(最壞復雜度O(N) )。在與抖音隱私合規團隊的合作研發中我們就發現了類似問題。它們的一個重要業務場景:要橫向遍歷某一個 array 的子節點,查找是否有違規數據并進行整行擦除。這種場景下,直接基于原始字節流進行查找和插入可能會帶來大量重復的skip 定位、數據拷貝開銷,最終導致性能劣化。因此我們需要一種高效的反序列化(帶有指針)結構表示來處理數據。根據以往經驗,我們想到了DOM(Document Object Model),這種結構被廣泛運用在 JSON 的泛型解析場景中(如 rappidJSON、sonic/ast),并且性能相比 map+interface 泛型要好很多。
要用 DOM 來描述一個 Thrift 結構體,首先需要一個能準確描述數據節點之間的關系的定位方式—— Path。其類型應該包括 list index、map key 以及 struct field id等。
typePathTypeuint8
const(
PathFieldIdPathType=1+iota//STRUCT下字段ID
PathFieldName//STRUCT下字段名稱
PathIndex//SET/LIST下的序列號
PathStrKey//MAP下的stringkey
PathIntkey//MAP下的integerkey
PathObjKey//MAP下的objectkey
)
typePathNodestruct{
Path//相對父節點路徑
Node//原始數據單元
Next[]PathNode//存儲子節點
}
在 Path 的基礎上,我們組合對應的數據單元Node,然后再通過一個Next 數組動態存儲子節點,便可以組裝成一個類似于BTree的泛型結構。
圖2 thrift DOM 數據結構
這種泛型結構比 map+interface 要好在哪呢?首先,底層的數據單元 Node 都是對原始 thrift data 的引用,沒有轉換 interface 帶來的二進制編解碼開銷;其次,我們的設計保證所有樹節點 PathNode 的內存結構是完全一樣,并且由于父子關系的底層核心容器是 slice, 我們又可以更進一步采用內存池技術,將整個 DOM 樹的子節點內存分配與釋放都進行池化從而避免調用 go 堆內存管理。測試結果表明,在理想場景下(后續反序列化的DOM樹節點數量小于等于之前反序列化節點數量的最大值——這由于內存池本身的緩沖效應基本可以保證),內存分配次數可為0,性能提升200%!(見【性能測試-全量序列化/反序列化】部分)。
03
性能測試
這里我們分別定義簡單(Small)、復雜(Medium) 兩個基準結構體分別在比較 不同數據量級 下的性能,同時添加簡單部分(SmallPartial)、復雜部分(MediumPartial) 兩個對應子集,用于【反射-裁剪】場景的性能比較:
- Small:114B,6個有效字段
- SmallPartial:small 的子集,55B,3個有效字段
- Medium: 6455B,284個有效字段
-
Small:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L3
Medium:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L12
SmallPartial:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L12
MediumPartial:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L36
其次,我們依據上述業務場景劃分為 反射、協議轉換、全量序列化/反序列化 三套 API,并以代碼生成庫kitex/FastAPI、泛化調用庫kitex/generic、JSON 庫sonic為基準進行性能測試。其它測試環境均保持一致:
- Go 1.18.1
- CPU intel i9-9880H 2.3GHZ
- OS macOS Monterey 12.6
kitex/FastAPI:https://github.com/cloudwego/kitex/blob/aed28371eb88b2668854759ce9f4666595ebc8de/pkg/remote/codec/thrift/thrift.go
kitex/generic:https://github.com/cloudwego/kitex/tree/develop/pkg/generic
sonic:https://github.com/bytedance/sonic
反射
1. 代碼
dynamicgo/testdata/baseline_tg_test.go
2. 用例
- GetOne:查找字節流中最后1個數據字段
- GetMany:查找前中后5個數據字段
- MarshalMany:將 GetMany 中的結果進行二次序列化
- SetOne:設置最后一個數據字段
- SetMany:設置前中后3個節點數據
- MarshalTo:將大 Thrift 數據包裁剪為小 thrift 數據包 (Small -> SmallPartial 或 Medium -> MediumParital)
- UnmarshalAll+MarshalPartial:代碼生成/泛化調用方式裁剪——先反序列化全量數據再序列化部分數據。效果等同于 MarshalTo。
3. 結果
- 簡單(ns/OP)

- 復雜(ns/OP)

4. 結論
- dynamicgo 一次查找+寫入 開銷大約為代碼生成方式的 2 ~ 1/3、為泛化調用方式的 1/12 ~ 1/15,并隨著數據量級增大優勢加大;
- dynamicgo thrift 裁剪 開銷接近于代碼生成方式、約為泛化調用方式的 1/10~1/6,并且隨著數據量級增大優勢減弱。
協議轉換
1. 代碼
- JSON2Thrift:dynamicgo/testdata/baseline_j2t_test.go
- ThriftToJSON:dynamicgo/testdata/baseline_t2j_test.go
2. 用例
- JSON2thrift:JSON 數據轉換為等價結構的 thrift 數據
- thrift2JSON:將 thrift 數據轉換為等價結構的 JSON 數據
- sonic + kitex-fast:表示通過 sonic 處理 json 數據(有結構體),通過kitex代碼生成處理thrift數據
3. 結果
- 簡單(ns/OP)

- 復雜(ns/OP)

4. 結論
- dynamicgo 協議轉換開銷約為代碼生成方式的 1~2/3、泛化調用方式的 1/4~1/9,并且隨著數據量級增大優勢加大;
全量序列化/反序列化
1. 代碼
dynamicgo/testdata/baseline_tg_test.go#BenchmarkThriftGetAll
2. 用例
-
UnmarshalAll:反序列化所有字段。其中對于 dynamicgo 有兩種模式:
- new:每次重新分配 DOM 內存;
- reuse:使用內存池復用 DOM 內存。
-
MarshalAll:序列化所有字段。
3. 結果
- 簡單(ns/OP)

- 復雜(ns/OP)

4. 結論
- dynamicgo 全量序列化 開銷約為代碼生成方式的 6~3倍、泛化調用方式的 1/4~1/2,并且隨著數據量級增大優勢減弱;
-
04
應用與展望
當前,dynamicgo 已經應用到許多重要業務場景中,包括:
- 業務隱私合規 中間件(thrift 反射);
- 抖音某 BFF 服務下游數據按需下發(thrift 裁剪);
- 字節跳動某 API 網關協議轉換(JSON<>thrift 協議轉換)。
并且逐步上線并取得收益。目前 dynamic 還在迭代中,接下來的工作包括:
- 集成到 Kitex 泛化調用模塊中,為更多用戶提供高性能的 thrift 泛化調用模塊;
- Thrift DOM 接入 DSL(GraphQL)組件,進一步提升 BFF 動態網關性能;
- 支持 Protobuf 協議。
也歡迎感興趣的個人或團隊參與進來,共同開發!
項目地址GitHub:https://github.com/cloudwego
官網:www.cloudwego.io
審核編輯 :李倩
-
編解碼
+關注
關注
1文章
140瀏覽量
19741 -
開源
+關注
關注
3文章
3468瀏覽量
42928 -
數據包
+關注
關注
0文章
268瀏覽量
24617
原文標題:抖音內部使用的Go基礎庫開源,高性能動態處理RPC數據
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
【GoRK3288】1.Rockchip RK3288, GO!GO!!GO!!!
會go語言能做什么工作?
香山是什么?“香山” 高性能開源 RISC-V 處理器項目介紹
移動抖音卡全新發布免流量刷抖音你會辦理嗎?
抖音再回應起訴騰訊
抖音快手APP大眼特效開源實現,甜美系小姐姐親做效果演示

嵌入式Linux應用開發之內置RPC
Tars框架使用NIO進行網絡編程的源碼分析

評論