應對百萬訪問量的 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
-
創建連接鏈表。簡單講,同一時間來了一萬連接請求,給用戶 A 發來的連接請求創建一個專門的 connection1,用戶 B 發來的連接請求創建一個專門的 connection2;給用戶 C 發來的……
-
遍歷步驟 1 生成的連接鏈表,從 connection1 一個個地看直到 connection10000,查看哪個連接(connection)上面用戶發了新的請求,如果有發現新的請求,則想辦法通知負責當前連接的進程(比如我們自己的服務)去響應,然後繼續遍歷。
-
終於遍歷到了最後一個連接,繼續從頭開始遍歷。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
大體的步驟如下:
-
創建連接數組列表。同一時間來了一萬連接請求,給用戶 A 發來的連接請求創建一個專門的 connection0,ID 爲 0;用戶 B 發來的連接請求創建一個專門的 connection1,ID 爲 1;給用戶 C 發來的……
-
linux 內核和網卡驅動的約定:當某個連接上有新的請求時,網卡驅動把請求的內容和對應的連接 ID 一起發給內核。
-
linux 內核拿到了帶連接 ID 的請求,找到對應的 connectionID 並把它加入到一個特殊鏈表。
-
遍歷這個特殊的鏈表,想辦法通知負責當前連接的進程(比如我們自己的服務)去響應,然後繼續遍歷
注意第 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 函數,能發現主要有下面幾個函數:
-
epollcreate/epollcreate1 創建 epoll
-
epollctl 設置 epoll 事件 3 epollwait 等待 epoll 事件 到這裏,golang 與 epoll 就算對接上了。因爲時間問題,細節暫時就不展開了,大家感興趣可以自己探索。
小結
本文簡單介紹了 epoll 模型。直觀上來講,併發模型中的 Actors 模型、CSP 模型等,側重的是內存的分配與信號的管理;但是,如何能充分發揮這些併發模型的優勢,滿足高併發的真實場景呢?
答案就是 epoll 模型。相比較於傳統的 select/poll 模型,epoll 能更充分地利用 cpu 的時間,把性能投入到有效的運算中去。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KrchRoCPd0d65QtZlPM5QA