應對百萬訪問量的 epoll 模式

 【導讀】golang 模型如何利用 epoll、爲什麼利用 epoll 提升併發性能?本文做了詳細解讀。

寫在前面

上一篇文章《併發模型:Actors 與 CSP》(https://jingwei.link/2018/07/08/actor-and-csp-model.html)簡單介紹了 Actors 和 CSP 兩種併發模型。如果認真推敲會發現,無論是 Actors 還是 CSP,直觀上來說其實都是內存模型,那麼高併發的 CPU 模型是怎麼樣的呢?

或者說:只有 8 顆 CPU 核的一臺主機,同一時間至多運行 8 個線程,如何實現一秒時間內對上萬個請求的響應?

select/poll 與 epoll

select/poll 模型工作機理

在 epoll 之前,存在兩種高併發模型:select 和 poll,大體的步驟是:

1# select和poll模式會專門對下面的連接鏈表進行輪詢,查看那個連接上有請求
2head->connetion1->connection2->connection3->connection4->...
3
4
  1. 創建連接鏈表。簡單講,同一時間來了一萬連接請求,給用戶 A 發來的連接請求創建一個專門的 connection1,用戶 B 發來的連接請求創建一個專門的 connection2;給用戶 C 發來的……

  2. 遍歷步驟 1 生成的連接鏈表,從 connection1 一個個地看直到 connection10000,查看哪個連接(connection)上面用戶發了新的請求,如果有發現新的請求,則想辦法通知負責當前連接的進程(比如我們自己的服務)去響應,然後繼續遍歷。

  3. 終於遍歷到了最後一個連接,繼續從頭開始遍歷。select/poll 模型的侷限

從上面的描述可以知道,select/poll 模型裏面存在一個遍歷查找過程。當鏈表的長度較短,且每個連接(connection)上的請求很頻繁時,select/poll 的模型工作的很好;但是一旦連接數增加,select/poll 模式遍歷查找的過程會消耗大量的 CPU 時間,而且連接數越多情況越惡化,因此限制了這種模式在高訪問量場景下的使用。

epoll 模型工作機理

既然遍歷連接(可以看做一小塊內存,是文件描述符的一種)限制了 select/poll 模型的天花板,那麼能不能不要再讓 CPU 遍歷那麼多連接了。

linux 說:可以。

1# 連接的列表,每個連接存在一個唯一的id
2[0]connection0 | [1]connection1 | [2]connection2 | ...
3
4# 發現connection1和connection10有請求
5# 把它們加入到一個特殊的鏈表
6head->connection1->connection10
7
8

大體的步驟如下:

  1. 創建連接數組列表。同一時間來了一萬連接請求,給用戶 A 發來的連接請求創建一個專門的 connection0,ID 爲 0;用戶 B 發來的連接請求創建一個專門的 connection1,ID 爲 1;給用戶 C 發來的……

  2. linux 內核和網卡驅動的約定:當某個連接上有新的請求時,網卡驅動把請求的內容和對應的連接 ID 一起發給內核。

  3. linux 內核拿到了帶連接 ID 的請求,找到對應的 connectionID 並把它加入到一個特殊鏈表。

  4. 遍歷這個特殊的鏈表,想辦法通知負責當前連接的進程(比如我們自己的服務)去響應,然後繼續遍歷

注意第 4 步,因爲 linux 內核已經把存在實際請求的連接揀出來了,因此不存在徒勞功,老老實實處理請求就好了。

epoll 的侷限

像上面所描述的,epoll 杜絕了無意義的遍歷,因此在高訪問量場景中有很大的發揮空間。但是不能不說,一切都是基於 web 請求計算量低請求低頻的場景。

試想,對於 epoll 中的 connection,如果網卡突然對 linux 內核說:哥,現在所有的連接都有請求。那麼特殊鏈表裏其實就是所有的連接實例了,這種場景下 epoll 反而不如 select/poll 模式,畢竟後者步驟少啊。

幸運的是,我們所說的百萬訪問量,都是人發起的,很契合 epoll 的使用場景。

golang 中的 epoll

參見 golang 源碼的 src/runtime/proc.go 文件,其在 main 函數啓動時,既開始在系統棧開始運行 sysmon 函數。

1func main() {
2//...
3 systemstack(func() {
4  newm(sysmon, nil)
5 })
6//...
7}
8
9

golang 源碼中的 sysmon 函數

通過查看 sysmon 函數可以知道,這個函數主要的是一個無窮的 for 循環,負責調整時序、GC(垃圾回收)以及 epoll 檢查等。

 1// sysmon
 2// Go runtime啓動時創建的,負責監控所有goroutine的狀態判斷是否需要GC,
 3// 進行netpoll等操作。sysmon函數中會調用retake函數進行搶佔式調度
 4func sysmon() {
 5//...
 6  for {
 7  if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
 8   //更新最後一次查詢G時間,爲了下一次做判斷
 9   atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
10   // 從網絡I/O查找已經就緒的G,不阻塞
11   gp := netpoll(false) // non-blocking - returns list of goroutines
12   //...
13  }
14  }
15}
16
17

進一步查看 netpoll 函數,能發現主要有下面幾個函數:

  1. epollcreate/epollcreate1 創建 epoll

  2. epollctl 設置 epoll 事件 3 epollwait 等待 epoll 事件 到這裏,golang 與 epoll 就算對接上了。因爲時間問題,細節暫時就不展開了,大家感興趣可以自己探索。

小結

本文簡單介紹了 epoll 模型。直觀上來講,併發模型中的 Actors 模型、CSP 模型等,側重的是內存的分配與信號的管理;但是,如何能充分發揮這些併發模型的優勢,滿足高併發的真實場景呢?

答案就是 epoll 模型。相比較於傳統的 select/poll 模型,epoll 能更充分地利用 cpu 的時間,把性能投入到有效的運算中去。

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