0
0

[译]Go TCP Socket的实现

鸟窝 发表于 2017年11月29日 12:01 | Hits: 483
Tag: Go

原文:TCP Socket Implementation On Golangby 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调用同一个文件描述符,总是(可能)伴随着一个EAGAINerror。我记得这个错误,当文件描述符还没有准备(ready)某个操作的时候就会返回这个错,上面的例子中操作是read。问题是为什么Go会这样做呢?

我猜想这可能是epoll_wait的一个bug, 它为每一个文件描述符提供了错误的ready事件?每一个文件描述符? 看起来read事件是错误事件的两倍,为什么是两倍?

老实说,我的epoll知识很了了,程序只是一个简单的处理事件的socket handler(类似)。没有多线程,没有同步,非常简单。

通过Google我找到了一篇极棒的文章分析评论epoll,由Marek所写,。

这篇文章重要的摘要就是:在多线程中使用epoll, 不必要的唤醒(wake up)通常是不可避免的,因为我们想通知每个等待事件的worker。

这也正好解释了我们的futex 唤醒数。还是让我们看一个简化版本来好好理解怎么在基于事件的socket处理程序中使用epoll吧:

  1. Bindsocket listener到file descriptor, 我们称之为s_fd
  2. 使用epoll_create创建epoll file descriptor, 我们称之为e_fd
  3. 通过epol_ctlbinds_fd到e_fd, 处理特殊的事件(通常EPOLLIN|EPOLLOUT)
  4. 创建一个无限循环 (event loop), 它会在每次循环中调用epoll_wait得到已经ready连接
  5. 处理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

原文链接: http://colobu.com/2017/11/28/TCP-Socket-Implementation-On-Golang/

0     0

评价列表(0)