字節跳動開源 Shmipc:基于共享內存的高性能 IPC

簡介
CloudWeGo - Shmipc 是字節跳動服務框架團隊研發的高性能進程間通訊庫,它基于共享內存構建,具有零拷貝的特點,同時它引入的同步機制具有批量收割 IO 的能力,相對于其他進程間通訊方式能明顯提升性能。在字節內部,Shmipc 應用于 Service Mesh 場景下,mesh proxy 進程與業務邏輯進程、與通用 sidecar 進程的通訊, 在大包場景和 IO 密集型場景能夠取得顯著的性能收益。
開源社區關于這方面的資料不多,Shmipc 的開源希望能為社區貢獻一份力量,提供一份參考。本文主要介紹 Shmipc 的一些主要的設計思路以及后續的演進規劃。?
go 版本實現:
??https://github.com/cloudwego/shmipc-go??
設計細節:
??https://github.com/cloudwego/shmipc-spec???
項目背景
在字節,Service Mesh 在落地的過程中進行了大量的性能優化工作,其中 Service Mesh 的流量劫持是通過,mesh proxy 與微服務框架約定的地址進行進程間通訊來完成,性能會優于開源方案中的 iptables。但常規的優化手段已不能帶來明顯的性能提升。于是我們把目光放到了進程間通訊上,Shmipc 由此誕生。
設計思路
零拷貝
在生產環境中比較廣泛使用的進程間通訊方式是 unix domain socket 與 TCP loopback(localhost:$PORT),兩者從 benchmark 看性能差異不大。從技術細節看,都需要將通訊的數據在用戶態和內核態之間進行拷貝。在 RPC場景下,一次 RPC 流程中在進程間通訊上會有四次的內存拷貝,Request 路徑兩次, Response 路徑兩次。

雖然現代 CPU 上進行順序的 copy 非常快,但如果我們能夠消除這多達四次的內存拷貝,在大包場景下也能在一定程度上節省 CPU 使用。而基于共享內存通訊零拷貝的特性,我們可以很容易達成這一點。但為了達到零拷貝的效果,圍繞共享內存本身,還會產生有許多額外的工作,比如:
- 深入微服務框架的序列化與反序列化。我們希望當 Request 或 Response 序列化完成時,對應的二進制數據已經存在共享內存中。而不是序列化到一塊非共享內存的 buffer 中,然后再拷貝到共享內存 buffer。
- 實現一種進程同步機制。當一個進程把數據寫入共享內存后,另外一個進程并不知道,因此需要同步機制進行通知。
- 高效的內存分配和回收。保證跨進程的共享內存的分配和回收機制的開銷足夠低,避免其掩蓋零拷貝的特性帶來的收益。
同步機制
分場景考慮:
- 按需實時同步。適用于在線場景,對時延極其敏感,每次寫入操作完成后都通知對端進程。Linux 下,可做選擇的比較多,TCP loopback、unix domain socket、event fd 等。event fd的 benchmark 性能會略好,但跨進程傳遞 fd 會引入過多復雜性,其帶來的性能提升在 IPC 上不太明顯,復雜性與性能中間的權衡需要慎重考慮。在字節,我們選擇了 unix domain socket 來進行進程同步。
- 定時同步。適用于離線場景,對時延不敏感。通過高間隔的 sleep 訪問共享內存中自定義的標志位來鑒別是否有數據寫入。但注意 sleep 本身也需要系統調用,開銷大于 unix domain socket 的讀寫。
- 輪詢同步。適用于時延非常敏感,CPU不那么敏感的場景。可以通過單核輪詢共享內存中的自定義標志位來完成。
總的來說按需實時同步和定期同步需要系統調用來完成,輪詢同步不需要系統調用,但需要常態跑滿一個 CPU 核心。
批量收割 IO
在線場景中按需實時同步,每次數據寫入都需要進行一次進行進程同步(下圖中的4),雖然延遲問題解決了,但在性能上,需要交互的數據包需要大于一個比較大的閾值,零拷貝帶來的收益才能突顯。因此在共享內存中構造了一個 IO 隊列的來完成批量收割 IO,使其在小包 IO 密集場景也能顯現收益。核心思想是:當一個進程把請求寫入 IO隊列后,會給另外一個進程發通知來處理。那么在下一個請求進來時(對應下圖中的 IOEvent 2~N,一個 IOEvent 可以獨立描述一個請求在共享內存中的位置),如果對端進程還在處理 IO 隊列中的請求,那么就不必進行通知。因此,IO越密集,批處理效果就越好。

另外就是離線場景中,定時同步本身就是批量處理 IO 的,批處理的效果能夠有效減少進程同步帶來的系統調用,sleep 間隔越高,進程同步的開銷就越低。
對于輪詢同步則不需要考慮批量收割 IO,因為這個機制本身是為了減少進程同步開銷。而輪詢同步直接占滿一個 CPU 核心,相當于默認把同步機制的開銷拉滿以獲取極低的同步延遲。
性能收益
Benchmark

其中X 軸為數據包大小,Y軸為一次 Ping-Pong 的耗時,單位為微秒,越小越好。可以看到在小包場景下,Shmipc 相對于 unix domain socket 也能獲得一些收益,并且隨著包大小越大性能越好。
數據源:??git
clone https://github.com/cloudwego/shmipc-go && go test
-bench=BenchmarkParallelPingPong -run BenchmarkParallelPingPong??
生產環境
在字節生產環境的 Service Mesh 生態中,我們在 3000+ 服務、100w+ 實例上應用了 Shmipc。不同的業務場景顯現出不同的收益,其中收益最高的風控 業務降低了整體24%的資源使用,當然也有無明顯收益的甚至劣化的場景出現。但在大包和 IO 密集型場景均能顯現出顯著收益。
采坑記錄
在字節實際落地的過程中我們也踩了一些坑,導致一些線上事故,比較具有參考價值。
- 共享內存泄漏。IPC 過程共享內存分配和回收涉及到兩個進程,稍有不慎就容易發生共享內存的泄漏。問題雖然非常棘手,但只要能夠做到泄漏時主動發現,以及泄漏之后有觀測手段可以排查即可。
- 主動發現。可以通過增加一些統計信息然后匯總到監控系統來做到主動發現,比如總分配和總回收的內存大小。
- 觀測手段。在設計共享內存的布局時增加一些元信息,使得在發生泄漏之后,我們可以通過內置的 debug 工具dump 泄漏時刻的共享內存來進行分析。能夠知道所泄漏的內存有多少,里面的內容是什么,以及和這部分內容相關的一些元信息。
- 串包。串包是最頭疼的問題,出現的原因是千奇百怪的,往往造成嚴重后果。我們曾在某業務上發生串包事故,出現的原因是因為大包導致共享內存耗盡,fallback 到常規路徑的過程中設計存在缺陷,小概率出現串包。排查過程和原因并不具備共性,可以提供更多的參考是增加更多場景的集成測試和單元測試將串包扼殺在搖籃中。
- 共享內存踩踏。應該盡可能使用 memfd
來共享內存,而不是 mmap 文件系統中的某個路徑。早期我們通過 mmap 文件系統的路徑來共享內存,Shmipc
的開啟和共享內存的路徑由環境變量指定,啟動過程由引導進程注入應用進程。那么存在一種情況是應用進程可能會 fork
出一個進程,該進程繼承了應用進程的環境變量并且也集成了 Shmipc,然后 fork 的進程和應用進程 mmap
了同一塊共享內存,發現踩踏。在字節的事故場景是應用進程使用了 golang 的 plugin 機制從外部加載
.so來運行,該.so集成了 Shmipc,并且跑在應用進程里,能看到所有環境變量,于是就和應用進程 mmap 了同一片共享內存,運行過程發生未定義行為。 - Sigbus coredump。早期我們通過
mmap /dev/shm/路徑(tmpfs)下的文件來共享內存,應用服務大都運行在 docker 容器實例中。容器實例對 tmpfs 有容量限制(可以通過 df -h 觀測),這會使得 mmap 的共享內存如果超過該限制就會出現 Sigbus,并且 mmap 本身不會有任何報錯,但在運行期,使用到超過限制的地址空間時才會出現 Sigbus 導致應用進程崩潰。解決方式和第三點一樣,使用 memfd 來共享內存。
后續演進
- 整合至微服務 RPC 框架 CloudWeGo/Kitex。
- 整合至微服務 HTTP 框架 CloudWeGo/Hertz。
- 開源 Rust 版本的 Shmipc 并整合至 Rust RPC 框架 CloudWeGo/Volo。
- 開源 C++ 版本的 Shmipc。
- 引入定時同步機制適用于離線場景。
- 引入輪詢同步的同步機制適用于對延遲有極致要求的場景。
- 賦能其他 IPC 場景, 比如 Log SDK 與 Log Agent, Metrics SDK 與 Metrics Agent 等。
總結
希望本文能讓大家對于 Shmipc 有一個初步的了解,知曉其設計原理,更多實現細節以及使用方法請參考文章開頭給出的項目地址。歡迎各位感興趣的同學向 Shmipc 項目提交 Issue 和 PR,共同建設 CloudWeGo 開源社區,也期望 Shmipc 在 IPC 領域助力越來越多開發者和企業構建高性能云原生架構。

































