這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文: TCP Socket Implementation On Golang by Gian Giovani.
譯者注: 作者並沒有從原始碼層級去分析Go socket的實現,而是利用strace
工具來反推Go Socket的行為。這一方法可以擴充我們分析代碼的手段。
原始碼層級的分析可以看其實現: net poll,以及一些分析文章:The Go netpoller, The Go netpoller and timeout
Go語言是我寫web程式的首選, 它隱藏了很多細節,但仍然不失靈活性。最新我用strace工具分析了一下一個http程式,純屬手賤但還是發現了一些有趣的事情。
下面是strace
的結果:
1234567891011121314151617181920 |
% time seconds usecs/call calls errors syscall------ ----------- ----------- --------- --------- ---------------- 91.24 0.397615 336 1185 29 futex 4.13 0.018009 3 7115 clock_gettime 2.92 0.012735 19 654 epoll_wait 1.31 0.005701 6 911 write 0.20 0.000878 3 335 epoll_ctl 0.12 0.000525 1 915 457 read 0.02 0.000106 2 59 select 0.01 0.000059 0 170 close 0.01 0.000053 0 791 setsockopt 0.01 0.000035 0 158 getpeername 0.01 0.000034 0 170 socket 0.01 0.000029 0 160 getsockname 0.01 0.000026 0 159 getsockopt 0.00 0.000000 0 7 sched_yield 0.00 0.000000 0 166 166 connect 0.00 0.000000 0 3 1 accept4------ ----------- ----------- --------- --------- ----------------100.00 0.435805 12958 653 total |
在這個剖析結果中有很多有趣的東東,但本文中要特別指出的是read
的錯誤數和futex
調用的錯誤數。
一開始我沒有深思futex的調用, 大部分情況它無非是一個喚醒調用(wake call)。既然這個程式會處理每秒幾百個請求,它應該包含很多go routine。另一方面,它使用了channel,這也會導致很多block情況,所以有很多futex調用也很正常。 不過後來我發現這個數也包含來自其它的邏輯,後面再表。
Why you no read
有誰喜歡錯誤(error)?短短一分鐘就有幾百次的錯誤,太糟糕了, 這是我看到這個剖析結果後最初的印象。那麼 read call
又是什麼東東?
123 |
read(36, "GET /xxx/v3?q=xx%20ch&d"..., 4096) = 520...read(36, 0xc422aa4291, 1) = -1 EAGAIN (Resource temporarily unavailable) |
每次read調用同一個檔案描述符,總是(可能)伴隨著一個 EAGAIN
error。我記得這個錯誤,當檔案描述符還沒有準備(ready)某個操作的時候就會返回這個錯,上面的例子中操作是read
。問題是為什麼Go會這樣做呢?
我猜想這可能是epoll_wait
的一個bug, 它為每一個檔案描述符提供了錯誤的ready
事件?每一個檔案描述符? 看起來read事件是錯誤事件的兩倍,為什麼是兩倍?
老實說,我的epoll
知識很了了,程式只是一個簡單的處理事件的socket handler(類似)。沒有多線程,沒有同步,非常簡單。
通過Google我找到了一篇極棒的文章分析評論epoll
,由Marek所寫,。
這篇文章重要的摘要就是:在多線程中使用epoll
, 不必要的喚醒(wake up)通常是不可避免的,因為我們想通知每個等待事件的worker。
這也正好解釋了我們的futex 喚醒數。還是讓我們看一個簡化版本來好好理解怎麼在基於事件的socket處理常式中使用epoll
吧:
- Bind
socket listener
到 file descriptor
, 我們稱之為 s_fd
- 使用
epoll_create
建立 epoll file descriptor
, 我們稱之為 e_fd
- 通過
epol_ctl
bind s_fd
到 e_fd
, 處理特殊的事件(通常EPOLLIN|EPOLLOUT
)
- 建立一個無限迴圈 (event loop), 它會在每次迴圈中調用
epoll_wait
得到已經ready串連
- 處理ready的串連, 在多worker實現中會通知每一個worker
Using strace I found that golang using edge triggered epoll
使用strace
我發現 golang使用 edge triggered epoll:
1 |
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2490298448, u64=140490870550608}}) = 0 |
這意味著下面的過程應該是go socket的實現:
1、Kernel: 收到一個新串連.
2、Kernel: 通知等待的線程 threads A 和 B. 由於level-triggered 通知的"驚群"(“thundering herd”)行為,kernel必須喚醒這兩個線程.
3、Thread A: 完成 epoll_wait().
4、Thread B: 完成 epoll_wait().
5、Thread A: 執行 accept(), 成功.
6、Thread B: 執行 accept(), 失敗, EAGAIN錯誤.
現在我有八成把握就是這個case,不過還是讓我們用一個簡單的程式來分析。
12345678910111213 |
package mainimport "net/http"func main() {http.HandleFunc("/", handler)http.HandleFunc("/test", handler)http.ListenAndServe(":8080", nil)}func handler(w http.ResponseWriter, r *http.Request) {} |
一個簡單的請求後的strace
結果:
12345678 |
epoll_wait(4, [{EPOLLIN|EPOLLOUT, {u32=2186919600, u64=140542106779312}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1) = 1futex(0x7c1b10, FUTEX_WAKE, 1) = 1read(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 4096) = 348futex(0xc420060110, FUTEX_WAKE, 1) = 1write(5, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 116) = 116futex(0xc420060110, FUTEX_WAKE, 1) = 1read(5, 0xc4200f6000, 4096) = -1 EAGAIN (Resource temporarily unavailable) |
看到epoll_wait
有兩個futex調用,我認為是worker執行以及一次 error read。
如果GOMAXPROCS
設定為1,在單worker情況下:
12345678910111213 |
epoll_wait(4,[{EPOLLIN, {u32=1969377136, u64=140245536493424}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1) = 1accept4(3, {sa_family=AF_INET6, sin6_port=htons(54400), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1969376752, u64=140245536493040}}) = 0getsockname(6, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0accept4(3, 0xc42004db78, 0xc42004db6c, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)read(6, "GET /test?kjhkjhkjh HTTP/1.1\r\nHo"..., 4096) = 92write(6, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 139) = 139read(6, "", 4096) |
當使用1個worker,epoll_wait之後只有一次futex喚醒,並沒有error read。然而我發現並不總是這樣, 有時候我依然可以得到read error和兩次futex 喚醒。
And then what to do?
在Marek的文章中他談到Linux 4.5之後可以使用EPOLLEXCLUSIVE
。我的Linux版本是4.8,為什麼問題還是出現?或許Go並沒有使用這個標誌,我希望將來的版本可以使用這個標誌。
從中我學到了很多知識,希望你也是。
[0] https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
[1] https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/
[2] https://gist.github.com/wejick/2cef1f8799361318a62a59f6801eade8