原文: 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
的结果:
|
|
在这个剖析结果中有很多有趣的东东,但本文中要特别指出的是read
的错误数和futex
调用的错误数。
一开始我没有深思futex的调用, 大部分情况它无非是一个唤醒调用(wake call)。既然这个程序会处理每秒几百个请求,它应该包含很多go routine。另一方面,它使用了channel,这也会导致很多block情况,所以有很多futex调用也很正常。 不过后来我发现这个数也包含来自其它的逻辑,后面再表。
Why you no read
有谁喜欢错误(error)?短短一分钟就有几百次的错误,太糟糕了, 这是我看到这个剖析结果后最初的印象。那么 read call
又是什么东东?
|
|
每次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
binds_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:
|
|
这意味着下面的过程应该是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,不过还是让我们用一个简单的程序来分析。
|
|
一个简单的请求后的strace
结果:
|
|
看到epoll_wait
有两个futex调用,我认为是worker执行以及一次 error read。
如果GOMAXPROCS
设置为1,在单worker情况下:
|
|
当使用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