聊聊Sockmap與基本原理
本文轉載自微信公眾號「云巔論劍」,作者寒蟬。轉載本文請聯系云巔論劍公眾號。
什么是sockmap
sockmap是BPF程序的一種map類型,顧名思義,這種map類型中存儲的是對struct sock的引用。熟悉網絡的同學知道struct sock是套接字在網絡層的表示,因此利用sockmap我們可以在網絡層上對TCP套接字進行一些自定義處理和操縱。
背景
在現實環境中,絕大多數的網絡請求并不會像如下左圖一樣直接從客戶端發送到服務端,而是由于負載均衡、安全性等原因像如下右圖一樣,客戶端發出的請求要經過代理服務器轉發才能到達服務端。

很明顯,代理轉發行為拉長了網絡傳輸路徑,相對于直連增加了2次內核態和用戶態之間的切換、2次內核態和用戶態之間的數據拷貝、2次網絡協議棧的處理以及1次用戶態處理,因此代理轉發相比直連大大的降低了網絡性能(提高了時延,降低了帶寬)。
sockmap就是在這個背景下產生的,其目的就是提供一種加速本機內部TCP socket之間數據轉發的機制。
機制1 socket數據轉發卸載
簡介
如圖2.1,通常用戶態程序(如代理程序)需要通過系統調用sys_read從一個socket中讀取數據到用戶態,這個過程涉及到2次上下文切換(用戶態->內核態->用戶態)和1次內核態到用戶態的數據拷貝;用戶態程序對從socket中讀取的數據進行一系列處理后,再通過系統調用sys_write將處理后的數據寫入到另一個socket中去,這個過程涉及到2次上下文切換(用戶態->內核態->用戶態)和1次用戶態到內核態的數據拷貝。
sockmap最初的版本提供了一種在本機TCP socket之間直接進行skb(struct sk_buff,其表示一個包含包頭的數據包,后續均簡稱為skb)轉發的機制。如圖2.2,這種機制允許將原先圖2.1中的用戶態處理部分的邏輯卸載到內核BPF程序中進行處理,處理后的數據包skb直接在內核態中轉發到另一個socket中去進行數據包的發送。整個過程無需的用戶態內核態之間上下文切換,也無需任何用戶態內核態之間的數據拷貝,大大縮短了數據轉發路徑。
關聯的BPF程序
針對本機制,sockmap提供了兩種BPF程序類型:
1. BPF_SK_SKB_STREAM_VERDICT
該類型的BPF程序根據用戶的邏輯對數據進行處理和仲裁,其返回值(有如下三種)決定了源sock中數據的具體轉發行為:
1.1 SK_PASS
該行為表示數據將按圖2.1的方式被用戶態程序接收處理并再發送到目的sock中。
1.2 SK_REDIRECT
該行為表示數據將按圖2.2的方式在內核態中直接被BPF程序處理并發送到目的sock中。
1.3 SK_DROP
該行為表示數據將被直接丟棄。
2. BPF_SK_SKB_STREAM_PARSER
該類型的BPF程序不可單獨使用,必須與BPF_SK_SKB_STREAM_VERDICT程序搭配使用,其用于確定一條完整消息的邊界。通常數據流協議,在協議頭中會指定playload有幾個字節,然后通過底層tcp讀取完協議頭header和完整的payload后,才形成一條完整的消息記錄。當BPF_SK_SKB_STREAM_VERDICT程序不能自行確定一條完整的消息長度時,就需要該BPF程序來確定是否讀取到一條完整消息的尾部。
大多數情況下,BPF_SK_SKB_STREAM_VERDICT程序要么是可以自行確定消息的邊界要么只是從skb中獲取一些元數據例如IP地址。這時,該類型的BPF程序一般就直接返回SKB長度即可,如下。在內核5.10版本之后,這種情形的BPF_SK_SKB_STREAM_PARSER程序可以省略,僅使用BPF_SK_SKB_STREAM_VERDICT即可。
- SEC("stream_parser")
- int parser(struct __sk_buff *skb)
- {
- return skb->len;
- }
實現原理
那么現在我們要實現機制一的效果,即從圖2.1到圖2.2,核心是兩點:
1. 用戶態對數據包的處理(圖2.1)-> 內核態對數據包的處理(圖2.2)
2. 用戶態收發網絡數據包(圖2.1)-> 內核態收發網絡包(圖2.2)
用戶態數據包處理轉換到內核態數據包處理
將用戶態數據包的處理卸載到內核態進行處理的方法很簡單,就是將用戶態的處理邏輯用BPF程序在內核態重寫一遍即可。
用戶態收發包轉換到內核態收發包
回想一下用戶態應用是如何從網絡收發數據的。
通常用戶態應用阻塞在系統調用recv/recvfrom/read上或是通過select/poll/epoll等方式監聽socket,當socket接收緩存有數據到達時,內核會通過回調函數sk_data_ready(sk_data_ready是結構體struct sk的一個函數指針成員)喚醒用戶態應用進程從socket中收包并進行用戶態的處理。
相對應的,通常用戶態調用send/sendto/write等系統調用向socket的發送緩存寫入數據時,內核會通過回調函數sk_write_space(sk_write_space也是結構體struct sk的一個函數指針程序)喚醒內核態進程并把數據從用戶態進程轉交到內核協議棧。
sockmap利用了Linux內核的一個名為Stream Parser的框架,該框架提供了在內核中做基于tcp之上的數據流協議解析的能力,其核心原理就是替換了sk_data_ready和sk_write_space的實現。
當tcp收到數據后,通過回調函數sk_data_ready進行事件通知,將收到的數據傳遞到Stream Parser中而不是喚醒用戶態程序進行收包處理,接著Stream Parser把數據包交由前述的BPF程序進行處理。如前面所說,類型為BPF_SK_SKB_STREAM_VERDICT的程序會決定SKB的轉發行為,經BPF程序處理后返回值為SK_REDIRECT的SKB會在內核態直接被轉發到目的sock中發送緩存中,最后調用sk_write_space通知目的sock有數據寫入。
機制2數據發送BYPASS協議棧
簡介
通常從用戶態空間中創建的TCP socket中發送數據可以使用send/sendto/sendmsg/write等系統調用,而這些系統調用最終都將由tcp_sendmsg和tcp_sendpage來處理,tcp_sendmsg和tcp_sendpage將用戶空間的數據復制到內核SKB中,并將其按照TCP數據段發送出去。
如圖3.1,對于位于同一臺主機的發送端和接收端來說,發送端用戶程序向發送側socket發送的數據經tcp_sendmsg和tcp_sendpage發送后經過網絡協議棧到達虛擬網卡Loopback后再經過一次網絡協議棧到達接收端的接受側socket,最后接收端的用戶程序從接收側socket中收取數據到用戶態空間。
很明顯,在這個過程中,數據經過了兩次網絡協議棧的處理。
因此,sockmap中又提供了一種加速機制,當發送端和接收端的socket都位于同一主機時,在發送數據時可以繞過網絡協議棧,直接將數據發送接收端的socket中,如圖3.2。
注:該機制下在Loopback網卡上將無法抓到網絡報文。
關聯的BPF程序
針對本機制,sockmap提供了一種BPF程序類型:
1. BPF_SK_MSG_VERDICT
該類型的BPF程序根據用戶的邏輯對用戶態發送的數據進行仲裁,其返回值(如下)決定了數據的轉發行為。
1.1 SK_PASS
該行為表示數據將重新有如圖3.1的方式發送到網絡協議棧中經Loopback轉發到接收端socket中。
1.2 SK_REDIRECT
該行為表示數據將按照圖3.2的方式直接發送到接收端socket中,不經過網絡協議棧和Loopback。
1.3 SK_DROP
該行為表示數據將被丟棄。
實現原理
為了實現本機制,sockmap直接替換了tcp_sendmsg和tcp_sendpage的實現為tcp_bpf_sendmsg和tcp_bpf_sendpage,如下。
在tcp_bpf_sendmsg和tcp_bpf_sendpage中會將數據的控制權交由前面提到的類型為BPF_SK_MSG_VERDICT的BPF程序仲裁決定該數據是否要經過該機制的加速。對于BPF程序返回值為SK_REDIRECT的數據將被直接放入接收側socket的接收緩存中,無需經過協議棧和Loopback。
總結
sockmap針對位于同一主機下socket的TCP數據轉發路徑的不同部分提供了兩種機制。
機制一由于BPF自身的限制(如只支持有限的循環)無法重寫較為復雜的用戶態邏輯,因此在實際應用上較為少見。
機制二目前已經可以在一些應用上看到了,比如社區的Cilium,以及我們團隊在service mesh的一些場景做的網絡優化。但是由于其只是BYPASS內核網絡協議棧,網絡轉發加速效果并沒有機制一那么明顯。
參考資料
https://github.com/torvalds/linux
https://lwn.net/Articles/731133/
https://lwn.net/Articles/768371/
https://www.spinics.net/lists/netdev/msg691409.html