主要針對字節跳動的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