上周提到了 tun/tap 轉發框架的數據通道結構和優化 tun/tap 轉發性能優化,涉及 RingBuffer,packetization 等核心話題。我也給出了一定的數據結構以及處理邏輯,但竟然沒有高尚的 epoll,本文說說它,因為它不適合。
epoll 作為 select,poll 的升級替代,它的優勢在于 “大量描述符場景,主動通知 IO 事件而無需遍歷查找 IO 事件”,這意味著在少量描述符場景,epoll 并無優勢,反而增加復雜性,但復雜性并沒什么大不了的,本文主要強調,epoll 本質上是在串行處理 I 和 O,這導致雙向流量的嚴重性能問題。
萬事皆有因,異步多路復用機制提供的能力是 “將對描述符的 I 和 O 同時復用到同一個線程中”,select,poll,epoll 本質上都是一回事,它們作為一個整體,適合做什么,不適合做什么,要搞清楚,而不能將它們看做不同的東西,因為這樣一來,你很容易陷入 epoll 降維打擊 select,進而萬能無敵的陷阱。
epoll 擅長業務消息的分揀處理,僅分揀消息后交由專門的線程,或直接處理短消息,但對于 tun/tap 等構建的隧道上的持續流量,同一個 socket 的 recv,send 在同一個循環體中會導致半雙工問題,且同一 socket 的 recv 和 send 間,以及不同 socket 的 recv/send 間串行會導致饑餓,這需要引入一個復雜的公平調度機制來解決。總而言之,這種非 “多路復用”問題,epoll 很難應對。
比如,epoll_wait 循環體中處理 POLLIN,POLLOUT 的 if 判斷的位置會直接影響公平性,同時涉及 ET,LT,編碼調度,若非如此,持續的 I 會餓死 O,反之亦然,而對于持續流量,將調度交給系統調度器何樂而不為。
對于 I 和 O,一心不可二用,持續的雙向隧道流量需要的恰恰是解復用,即將同一個描述符的 I 和 O 解復用到不同線程,而不是復用,所以選型時第一要務就應該排除掉 select,poll,epoll,libevent 等異步多路復用技術,而為每一個 socket 簡單地創建兩個線程分別作阻塞 I 和 O 幾乎是唯一選擇,但這由于太簡單而顯得 low,展示不出自己運用復雜技術的能力,進而選擇 epoll 等多路復用的錯誤技術,然后再陷入持續優化的深淵,早干嘛去了。
編程者使用 epoll 處理隧道倒不是都為了炫技,有些也屬于拿著錘子找釘子。受大環境教化對產線工人產出效率的倡導,大多數編程者更熟悉高級框架和高級庫的相關 API,底層的 thread_create 則早就拋到九霄云外去了,從不知或已遺忘了返樸歸真的方法論。
對于高級 API,我的態度還是度量時間尺度,平衡你編碼調試的時間和代碼運行的時間,但前提是你一定要深入理解這個高級 API 的底層,它解決了什么問題,適合做什么,不適合做什么。調用高級 API 肯定增加了程序運行時間,多一個指令就多一個指令的時延,但直接使用底層 API 卻對編程者有極高的要求,否則就會延遲代碼發布和上線時間,同時增加維護和 bugfix 時間,要平衡這兩者。
言歸正傳,既然不使用 epoll,選擇了簡單創建兩個線程,就又涉及線程相關的高級技巧,可謂到處都是坑。
都知道線程比進程更輕量,鞋城更輕量,但為引出線程庫,協程這些高級概念,線程創建,銷毀的管理開銷必須要被詬病,這似乎是引出一個新技術的慣例,于是就在編程者中形成一種新范式,即涉及服務器端的多線程,一定要用線程庫,協程,就像涉及多個 socket 的 IO 一定要用 epoll 一樣。進程,線程,協程的糾纏,與 select,poll,epoll 幾乎無異。
但線程庫,鞋城同樣不適合 tun/tap 隧道。理由和態度依然是度量并平衡時間尺度。
類似 web 服務器 mpm,若采用多線程,為每一個簡單的 request/response 花 80us 創建一個線程并隨后在 100us 后花 30us 銷毀,確實是一筆很昂貴的開銷,線程池被提出解決該問題,資源池化的典型套路,但對于隧道,持續的雙向流哪怕僅存活 1s,創建,銷毀線程不到 200us 的開銷都顯得微不足道,為什么引入復雜性呢,多花幾小時甚至更久的時間調試線程池,再算上維護和 bugfix 時間,創建多少個線程才能償還,而你的代碼在線上的生命周期甚至熬不到那么久。
總結一下,對于隧道,很簡單,不用 epoll,線程池,協程,這些高尚貨,要純性能就別高尚,讓高尚去訴諸軟件工程和項目。
隧道場景遠比服務器場景更簡單,它只是在兩個方向的固定分發,所以只要下面就夠了:
void readtun_thread(void *arg)
{while(1) {block = dequeue(pool);len = read_tun(block);enqueue(tun2socket, block);}
}
void writesocket_thread(void *arg)
{while(1) {block = dequeue(tun2socket);len = write_socket(block); // 后續再 batch 優化enqueue(pool, block);}
}
void readsocket_thread(void *arg) {...}
void writetun_thread(void *arg) {...}
浙江溫州皮鞋濕,下雨進水不會胖。