主要針對位元組跳動的netpoll網路庫進行總結。netpoll網路庫相比於go本身的net標準庫更適合高併發場景。
基礎知識
netpoll與go.net庫一樣使用epoll這種IO多路複用機制處理網路請求。
基本理解
我們知道linux萬物皆檔案,每個檔案有個檔案識別符號fd,我們可以想象linux提供給我們的socket fd就是作業系統將傳輸層及以下的協議進行封裝抽象化的一個介面。我們可以簡單把socket理解成對應的一次tcp連線。 那麼網路操作根本上也是針對網路卡的IO操作,我們需要讀取資料/寫入資料,那麼如何更加高效地處理資料呢?目前大多數網路庫都使用IO多路複用機制,在linux系統中最先進的io多路複用就是epoll機制。
epoll工作方式
- 事件通知機制
- epoll_ctl/epoll_wait
- ET(邊緣觸發)/LT(水平觸發)
事件通知機制
- 註冊事件:epoll需要註冊一些可讀的事件
- 監聽事件:監聽到可讀的資料
- 觸發事件:通知資料可讀
主要還是有兩個系統呼叫:
- epoll_ctl
- epoll_wait
工作模式
epoll有兩種觸發工作模式:ET和LT
- ET也叫邊緣觸發,註冊的事件滿足條件之後,epoll只會觸發一次通知。就算你這一次的讀寫事件的資料沒有處理完,下一次epoll_wait也不會再觸發通知。
- LT也叫水平觸發,註冊的事件滿足條件之後,不管資料是否讀寫完成,每一次epoll_wait都會通知當前監聽的fd事件。
BIO/NIO
- BIO:blocking I/O,阻塞I/O。就是當我們向一個socket發起read的時候,資料讀取完成之前一直是阻塞的。
- NIO:nonblocking I/O,非阻塞I/O。就是read資料的時候不阻塞,立即返回。
那麼我們每次發現socket中有可讀資料的時候,我們就會開啟一個goroutine讀取資料。
Netpoll的優化點
go的net庫是BIO的,浪費了更多的goroutine在阻塞,並且難以對連線池中的連線進行探活。 netpoll採用了LT的觸發方式,這種觸發方式也就導致程式設計思路的不同
ET
LT
netpoll採用LT的程式設計思路 由於netpoll想在 系統呼叫 和 buffer 上做優化,所以採用LT的形式。
優化系統呼叫
syscall這個方法其實有三步:
- enter_runtime
- raw_syscall
- exit_runtime
由於系統呼叫是一個耗時的阻塞操作,容易造成goroutine阻塞,所以需要加入一些runtime的排程流程。 但是,epoll_wait觸發的事件,保證不會被阻塞,所以netpoll直接採用RawSyscall方法做系統呼叫,跳過了runtime的一些邏輯。
優化排程
使用msec動態調參和runtime.Gosched主動讓出P
msec動態調參
epoll_wait的系統呼叫有個引數是,等待時間,設定成-1是無限等待事件到來,0是不等待。
這樣就有事件到來的時候下次迴圈的epoll_wait採用立即返回,沒有事件就一直阻塞,減少反覆無用的呼叫。
runtime.Gosched主動讓出P
如果msec為-1的話會立即進入下一次迴圈,開啟新的epoll_wait呼叫,那麼呼叫就阻塞在這裡,goroutine阻塞時間長了之後會被runtime切換掉,只能等到下一次執行這個goroutine才行,導致時間浪費。 netpoll呼叫runtime.Gosched方法主動將GMP中的P讓出,減少runtime的排程過程。
優化buffer
我們在讀取和寫入資料的時候需要使用到buffer。 多數框架使用環形buffer,可以做到流式讀寫。但是環形buffer容量是死的,需要擴容的話,需要重新copy陣列,引入了很多的併發問題。
LinkBuffer
netpoll使用的buffer實現包括:
- 連結串列解決擴容copy問題
- sync.Pool複用連結串列節點
- atomic訪問size,規避data race和鎖競爭
還有一些nocopy方面的優化,減少了write和read的次數,從而提高了讀取和傳送的時候的編解碼效率。
更多資訊看:https://www.cloudwego.io/zh/blog/2021/10/09/位元組跳動在-go-網路庫上的實踐/#nocopy-buffer