走進 mTCP
mTCP
是一款面向多核系統的使用者態網路協議棧
核心態協議棧的缺陷
網際網路的發展,使得使用者對網路應用的效能需求越來越高。人們不斷挖掘CPU處理能力加強,新增核的數量,但這並沒有使得網路裝置的吞吐率線性增加,其中一個原因是核心協議棧成為了限制網路效能提升的瓶頸。
互斥上鎖引起的開銷
互斥上鎖是多核平臺效能的第一殺手。現在的伺服器端應用為了儘可能的實現高併發,通常都是採用多執行緒的方式監聽客戶端對服務埠發起的連線請求。首先,這會造成多個執行緒之間對 accept
佇列的互斥訪問。其次,執行緒間對檔案描述符空間的互斥訪問也會造成效能下降。
報文造成的處理效率低下
核心中協議棧處理資料報文都是逐個處理的, 缺少批量處理的能力。
頻繁的系統呼叫引起的負擔
頻繁的短連線會引起大量的使用者態/核心態模式切換,頻繁的上下文切換會造成更多的 Cache Miss
使用者態協議棧的引入
使用者態協議棧-即是將原本由核心完成了協議棧功能上移至使用者態實現。
通過利用已有的高效能 Packet IO
庫 (以 DPDK
為例)旁路核心,使用者態協議棧可以直接收發網路報文,而沒有報文處理時使用者態/核心態的模式切換。除此之外,由於完全在使用者態實現,所以具有更好的可擴充套件性還是可移植性。
mTCP 介紹
mTCP
作為一種使用者態協議棧庫的實現,其在架構如下圖所示:
mTCP
以函式庫的形式連結到應用程序,底層使用其他使用者態的 Packet IO
庫。
總結起來, mTCP
具有以下特性:
- 良好的多核擴充套件性
- 批量報文處理機制
- 類
epoll
事件驅動系統 -
BSD
風格的socket API
- 支援多種使用者態
Packet IO
庫 - 傳輸層協議僅支援
TCP
多核擴充套件性
為了避免多執行緒訪問共享的資源帶來的開銷。 mTCP
將所有資源(如 flow pool
socket buffer
)都按核分配,即每個核都有自己獨有的一份。並且,這些資料結構都是 cache
對齊的。
從上面的架構圖可以看到, mTCP
需要為每一個使用者應用執行緒(如 Thread0
)建立一個額外的一個執行緒( mTCP thread0
)。這兩個執行緒都被繫結到同一個核(設定 CPU
親和力)以最大程度利用 CPU
的 Cache
。
批量報文處理機制
由於內部新增了執行緒,因此 mTCP
在將報文送給使用者執行緒時,不可避免地需要進行執行緒間的通訊,而一次執行緒間的通訊可比一次系統呼叫的代價高多了。因此 mTCP
採用的方法是批量進行報文處理,這樣平均下來每個報文的處理代價就小多了。
類 epoll
事件驅動系統
對於習慣了使用 epoll
程式設計的程式設計師來說, mTCP
太友好了,你需要做就是把 epoll_xxx()
換成 mtcp_epoll_xxx()
BSD 風格的 socket API
同樣的,應用程式只需要把 BSD
風格的 Socket API
前面加上 mtcp_
就足夠了,比如 mtcp_accept()
支援多種使用者態Packet IO庫
在 mTCP
中, Packet IO
庫也被稱為 IO engine
, 當前版本(v2.1) mTCP
支援 DPDK
(預設)、 netmap
、 onvm
、 psio
四種 IO engine
。
mTCP的一些實現細節
執行緒模型
如前所述 mTCP
需要會為每個使用者應用執行緒建立一個單獨的執行緒,而這實際上需要每個使用者應用執行緒顯示呼叫下面的介面完成。
mctx_t mtcp_create_context(int cpu);
這之後,每個 mTCP
執行緒會進入各自的 Main Loop
,每一對執行緒通過 mTCP
建立的緩衝區進行資料平面的通訊,通過一系列 Queue
進行控制平面的通訊
每一個 mTCP
執行緒都有一個負責管理資源的結構 struct mtcp_manager
, 線上程初始化時,它完成資源的建立,這些資源都是屬於這個核上的這個執行緒的,包括儲存連線四元組資訊的 flow table
,套接字資源池 socket pool
監聽套接字 listener hashtable
,傳送方向的控制結構 sender
等等
使用者態 Socket
既然是純使用者態協議棧,那麼所有套接字的操作都不是用 glibc
那一套了, mTCP
使用 socket_map
表示一個套接字,看上去是不是比核心的那一套簡單多了!
struct socket_map { int id; int socktype; uint32_t opts; struct sockaddr_in saddr; union { struct tcp_stream *stream; struct tcp_listener *listener; struct mtcp_epoll *ep; struct pipe *pp; }; uint32_t epoll;/* registered events */ uint32_t events;/* available events */ mtcp_epoll_data_t ep_data; TAILQ_ENTRY (socket_map) free_smap_link; };
其中的 socketype
表示這個套接字結構的型別,根據它的值,後面的聯合體中的指標也就可以解釋成不同的結構。注意在 mTCP
中,我們通常認為的檔案描述符底層也對應這樣一個 socket_map
enum socket_type { MTCP_SOCK_UNUSED, MTCP_SOCK_STREAM, MTCP_SOCK_PROXY, MTCP_SOCK_LISTENER, MTCP_SOCK_EPOLL, MTCP_SOCK_PIPE, };
使用者態 Epoll
mTCP
實現的 epoll
相對於核心版本也簡化地多,控制結構 struct mtcp_epoll
如下:
struct mtcp_epoll { struct event_queue *usr_queue; struct event_queue *usr_shadow_queue; struct event_queue *mtcp_queue; uint8_t waiting; struct mtcp_epoll_stat stat; pthread_cond_t epoll_cond; pthread_mutex_t epoll_lock; };
它內部儲存了三個佇列,分別儲存發生了三種類型的事件的套接字。
-
MTCP_EVENT_QUEUE
表示協議棧產生的事件,比如LISTEN
狀態的套接字accept
了,ESTABLISH
的套接字有資料可以讀取了 -
USR_EVENT_QUEUE
表示使用者應用的事件,現在就只有PIPE
; -
USR_SHADOW_EVENT_QUEUE
表示使用者態由於沒有處理完,而需要模擬產生的協議棧事件,比如ESTABLISH
上的套接字資料沒有讀取完.
TCP流
mTCP
使用 tcp_stream
表示一條端到端的 TCP
流,其中儲存了這條流的四元組資訊、 TCP
連線的狀態、協議引數和緩衝區位置。 tcp_stream
儲存在每執行緒的 flow table
中
typedef struct tcp_stream { socket_map_t socket; // code omitted... uint32_t saddr;/* in network order */ uint32_t daddr;/* in network order */ uint16_t sport;/* in network order */ uint16_t dport;/* in network order */ uint8_t state;/* tcp state */ struct tcp_recv_vars *rcvvar; struct tcp_send_vars *sndvar; // code omitted... } tcp_stream;
傳送控制器
mTCP
使用 struct mtcp_sender
完成傳送方向的管理,這個結構是每執行緒每介面的,如果有 2 個 mTCP
執行緒,且有 3 個網路介面,那麼一共就有 6 個傳送控制器
struct mtcp_sender { int ifidx; /* TCP layer send queues */ TAILQ_HEAD (control_head, tcp_stream) control_list; TAILQ_HEAD (send_head, tcp_stream) send_list; TAILQ_HEAD (ack_head, tcp_stream) ack_list; int control_list_cnt; int send_list_cnt; int ack_list_cnt; };
每個控制器內部包含了 3 個佇列,佇列中元素是 tcp_stream
-
Control
佇列:負責快取待發送的控制報文,比如SYN-ACK
報文 -
Send
佇列:負責快取帶傳送的資料報文 -
ACK
佇列:負責快取純ACK
報文
例子:服務端TCP連線建立流程
假設我們的服務端應用在某個應用執行緒建立了一個 epoll
套接字和一個監聽套接字,並且將這個監聽套接字加入 epoll
,應用程序阻塞在 mtcp_epoll_wait()
,而mTCP執行緒在自己的 main Loop
中迴圈
- 本機收到客戶端發起的連線,收到第一個
SYN
報文。mTCP
執行緒在main Loop
中讀取底層IO
收到該報文, 在嘗試在本執行緒的flow table
搜尋後,發現沒有此四元組標識的流資訊,於是新建一條tcp stream
, 此時,這條流的狀態為 TCP_ST_LISTEN - 將這條流寫入
Control
佇列,狀態切換為 TCP_ST_SYNRCVD ,表示已收到TCP
的第一次握手 -
mTCP
執行緒在main Loop
中讀取Control
佇列,發現其中有剛剛的這條流,於是將其取出,組裝SYN-ACK
報文,送到底層IO
-
mTCP
執行緒在main Loop
中讀取底層收到的對端發來這條流的ACK
握手資訊,將狀態改為 TCP_ST_ESTABLISHED (TCP的三次握手完成),然後將這條流塞入監聽套接字的accept
佇列 - 由於監聽套接字是加入了
epoll
的,因此mTCP
執行緒還會將一個MTCP_EVENT_QUEUE
事件塞入struct mtcp_epoll
的mtcp_queu
e佇列。 - 此時使用者執行緒在
mtcp_epoll_wait()
就能讀取到該事件,然後呼叫mtcp_epoll_accept()
從Control
佇列讀取到連線資訊,就能完成連線的建立。
參考資料
mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems