Go TCP Socket的实现

原文: 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 的结果:

% 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 又是什么东东?

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 吧:

  1. Bind socket listenerfile descriptor , 我们称之为 s_fd
  2. 使用 epoll_create 创建 epoll file descriptor , 我们称之为 e_fd
  3. 通过 epol_ctl bind s_fde_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 :

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,不过还是让我们用一个简单的 程序 来分析。

package main

import "net/http"

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/test", handler)
	http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {

}

一个简单的请求后的 strace 结果:

epoll_wait(4, [{EPOLLIN|EPOLLOUT, {u32=2186919600, u64=140542106779312}}], 128, -1) = 1
futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1
futex(0x7c1b10, FUTEX_WAKE, 1)          = 1
read(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 4096) = 348
futex(0xc420060110, FUTEX_WAKE, 1)      = 1
write(5, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 116) = 116
futex(0xc420060110, FUTEX_WAKE, 1)      = 1
read(5, 0xc4200f6000, 4096)             = -1 EAGAIN (Resource temporarily unavailable)

看到 epoll_wait 有两个futex调用,我认为是worker执行以及一次 error read。

如果 GOMAXPROCS 设置为1,在单worker情况下:

epoll_wait(4,[{EPOLLIN, {u32=1969377136, u64=140245536493424}}], 128, -1) = 1
futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1
accept4(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) = 6
epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1969376752, u64=140245536493040}}) = 0
getsockname(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]) = 0
setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0
accept4(3, 0xc42004db78, 0xc42004db6c, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
read(6, "GET /test?kjhkjhkjh HTTP/1.1\r\nHo"..., 4096) = 92
write(6, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 139) = 139
read(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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章