Go 網絡庫 Gnet 解析

圖片拍攝於 2022 年 6 月 5 日 杭州  勇士總冠軍!!!

距離上次寫文章過了一月有餘,這段時間着實太躺了。以至於昨晚做了一個噩夢,醒來的時候狠狠的抽了自己兩巴掌,不能這麼躺了。

上面當然是個笑話。

開篇

上一篇 Go netpoll 大解析我們分析了 Go 原生網絡模型以及部分源碼,絕大部分場景下 (99%),使用原生 netpoll 已經足夠了。

但是在一些海量併發連接下,原生 netpoll 會爲每一個連接都開啓一個 goroutine 處理,也就是 1 千萬的連接就會創建一千萬個 goroutine。

這就給了這些特殊場景下的優化空間,這也是像 gnet 和 cloudwego/netpoll 誕生的原因之一吧。

本質上他們的底層核心都是一樣的,都是基於 epoll(linux) 實現的。只是事件發生後,每個庫的處理方式會有所不同。

本篇文章主要分析 gnet 的。至於使用姿勢就不發了,gnet 有對應的 demo 庫,可以自行體驗。

架構

直接引用 gnet 官網的一張圖

gnet 採用的是『主從多 Reactors』。也就是一個主線程負責監聽端口連接,當一個客戶端連接到來時,就把這個連接根據負載均衡算法分配給其中一個 sub 線程,由對應的 sub 線程去處理這個連接的讀寫事件以及管理它的死亡。

下面這張圖就更清晰了。

核心結構

我們先解釋 gnet 的一些核心結構。

engine 就是程序最上層的結構了。

接着看 eventloop。

對應 conn 結構,

這裏面有幾個字段介紹下,

conn 相當於每個連接都會有自己獨立的緩存空間。這樣做是爲了減少集中式管理內存帶來的鎖問題。使用 Ring buffer 是爲了增加空間的複用性。

整體結構就這些。

核心邏輯

當程序啓動時,

會根據用戶設置的 options 明確 eventloop 循環的數量,也就是有多少個 sub 線程。再進一步說,在 linux 環境就是會創建多少個 epoll 對象。

那麼整個程序的 epoll 對象數量就是 count(sub)+1(main Listener)。

上圖就是我說的,會根據設置的數量創建對應的 eventloop, 把對應的 eventloop 註冊到負載均衡器中。

當新連接到來時,就可以根據一定的算法 (gnet 提供了輪詢、最少連接以及 hash) 挑選其中一個 eventloop 把連接分配給它。

我們先來看主線程,(由於我使用的是 mac, 所以後面關於 IO 多路複用,實現部分就是 kqueue 代碼了,當然原理是一樣的)

Polling 就是等待網絡事件到來,傳遞了一個閉包參數,更確切的說是一個事件到來時的回調函數,從名字可以看出,就是處理新連接的。

至於 Polling 函數,

邏輯很簡單,一個 for 循環等待事件到來,然後處理事件。

主線程的事件分兩種,

一種是正常的 fd 發生網絡連接事件,

一種是通過 NOTE_TRIGGER 立即激活的事件。

通過 NOTE_TRIGGER 觸發告訴你隊列裏有 task 任務,去執行 task 任務。

如果是正常的網絡事件到來,就處理閉包函數,主線程處理的就是上面的 accept 連接函數。

accept 連接邏輯很簡單,拿到連接的 fd。設置 fd 非阻塞模式 (想想連接是阻塞的會咋麼樣?), 然後根據負載均衡算法選擇一個 sub 線程,通過 register 函數把此連接分配給它。

register 做了兩件事,首先需要把當前連接註冊到當前 sub 線程的 epoll or kqueue 對象中, 新增 read 的 flag。

接着就是把當前連接放入到 connections 的 map 結構中 fd->conn。

這樣當對應的 sub 線程事件到來時,可以通過事件的 fd 找到是哪個連接,進行相應的處理。

如果是可讀事件,

到這裏分析差不多就結束了。

總結

在 gnet 裏面,你可以看到,基本上所有的操作都無鎖的。

那是因爲事件到來時,採取的都是非阻塞的操作,且是串行處理對應的每個 fd(conn)。每個 conn 操作的都是自身持有的緩存空間。同時處理完一輪觸發的所有事件纔會循環進入下一次等待,在此層面上解決了併發問題。

當然這樣用戶在使用的時候也需要注意一些問題,比如用戶在自定義 EventHandler 中,如果要異步處理邏輯,就不能像下面這樣開一個 g 然後在裏面獲取本次數據,

而應該先拿到數據,再異步處理。

issues 上有提到,連接是使用 map[int]*conn 存儲的。gnet 本身的場景就是海量併發連接,內存會很大。進而 big map 存指針會對 GC 造成很大的負擔,畢竟它不像數組一樣,是連續內存空間,易於 GC 掃描。

還有一點,在處理 buffer 數據的時候,就像上面看到的,本質上是將 buffer 數據 copy 給用戶一份,那麼就存在大量 copy 開銷, 在這一點上,字節的 netpoll 實現了 Nocopy Buffer,改天研究一下。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aBdvYvoIO2FTMTPDY_IFYQ