《Go元件設計與實現》-netpoll的總結

aaayi發表於2022-01-13

主要針對位元組跳動的netpoll網路庫進行總結。netpoll網路庫相比於go本身的net標準庫更適合高併發場景。

基礎知識

netpoll與go.net庫一樣使用epoll這種IO多路複用機制處理網路請求。

基本理解

我們知道linux萬物皆檔案,每個檔案有個檔案識別符號fd,我們可以想象linux提供給我們的socket fd就是作業系統將傳輸層及以下的協議進行封裝抽象化的一個介面。我們可以簡單把socket理解成對應的一次tcp連線。 那麼網路操作根本上也是針對網路卡的IO操作,我們需要讀取資料/寫入資料,那麼如何更加高效地處理資料呢?目前大多數網路庫都使用IO多路複用機制,在linux系統中最先進的io多路複用就是epoll機制。

epoll工作方式

1642059597458-57fb017e-7e9a-4bcf-b78b-e06c239b3b6c.png

  • 事件通知機制
  • epoll_ctl/epoll_wait
  • ET(邊緣觸發)/LT(水平觸發)
事件通知機制
  • 註冊事件:epoll需要註冊一些可讀的事件
  • 監聽事件:監聽到可讀的資料
  • 觸發事件:通知資料可讀

主要還是有兩個系統呼叫:

  • epoll_ctl
  • epoll_wait
工作模式

epoll有兩種觸發工作模式:ET和LT

  1. ET也叫邊緣觸發,註冊的事件滿足條件之後,epoll只會觸發一次通知。就算你這一次的讀寫事件的資料沒有處理完,下一次epoll_wait也不會再觸發通知。
  2. LT也叫水平觸發,註冊的事件滿足條件之後,不管資料是否讀寫完成,每一次epoll_wait都會通知當前監聽的fd事件。

BIO/NIO

1642063229291-6b59adf6-3116-483c-ab02-b547b08152dc.png

  1. BIO:blocking I/O,阻塞I/O。就是當我們向一個socket發起read的時候,資料讀取完成之前一直是阻塞的。
  2. NIO:nonblocking I/O,非阻塞I/O。就是read資料的時候不阻塞,立即返回

那麼我們每次發現socket中有可讀資料的時候,我們就會開啟一個goroutine讀取資料。

Netpoll的優化點

go的net庫是BIO的,浪費了更多的goroutine在阻塞,並且難以對連線池中的連線進行探活。 netpoll採用了LT的觸發方式,這種觸發方式也就導致程式設計思路的不同

ET

1642064579869-64deb77c-4845-4103-b735-94a0eb19937b.png

LT

1642064598829-68afa0a4-1e15-4598-aca0-a4b924e1645c.png

netpoll採用LT的程式設計思路 由於netpoll想在 系統呼叫 和 buffer 上做優化,所以採用LT的形式。

優化系統呼叫

syscall這個方法其實有三步:

  1. enter_runtime
  2. raw_syscall
  3. exit_runtime

由於系統呼叫是一個耗時的阻塞操作,容易造成goroutine阻塞,所以需要加入一些runtime的排程流程。 但是,epoll_wait觸發的事件,保證不會被阻塞,所以netpoll直接採用RawSyscall方法做系統呼叫,跳過了runtime的一些邏輯。

優化排程

使用msec動態調參和runtime.Gosched主動讓出P

msec動態調參

epoll_wait的系統呼叫有個引數是,等待時間,設定成-1是無限等待事件到來,0是不等待。

1642066647357-64124af2-f093-4edd-bb41-d7117ce77b4d.png

這樣就有事件到來的時候下次迴圈的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

相關文章