前言:

大家潜规则的会认为标准库里的http库肯定不好用,我先前也是这么考虑的,后来发现golang社区里的http client基本是围绕net/http造轮子。  那些go http client会像python requests那样好用,而不是在功能和底层上有提升。

既然大家都在用net/http,那么有必要深入测试下,别掉坑里。 先前使用python requests的时候就被坑了好几次,好在结合requests源代码找到了答案.  这篇主要说下测试net/http连接池的测试结果,下篇会聊聊go net/http连接池是怎么实现的,具体到源码方面的体现. 

net/http 长连接验证 ?

默认是长连接,毋庸置疑. 客户端发起的时候会在header里标记HTTP/1.1 。

net/http 连接复用 ?

连接可复用. 只要匹配到目标ip及端口就可以服用到该维度的连接池.

直接注释 response.Body.Close() 会出现什么?

各种循环测试,不仅长连接,而且连接还是会被复用. 社区里有人反映说close注释掉会出现连接不停创建的情况.

如果连接池中,某个主机的连接被占用,上层并发请求会发生什么?

net/http在池中找不到有用的连接,就会不断的重新new一个连接,不会阻塞等待一个连接。

// xiaorui.cc
 
req     18097 root  238u  IPv4 812087907      0t0     TCP 127.0.0.1:24691->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  239u  IPv4 812087908      0t0     TCP 127.0.0.1:24693->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  240u  IPv4 812087909      0t0     TCP 127.0.0.1:24695->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  241u  IPv4 812087917      0t0     TCP 127.0.0.1:24697->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  242u  IPv4 812087920      0t0     TCP 127.0.0.1:24699->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  243u  IPv4 812087923      0t0     TCP 127.0.0.1:24701->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  244u  IPv4 812087926      0t0     TCP 127.0.0.1:24703->127.0.0.1:8001 (ESTABLISHED)
req     18097 root  245u  IPv4 812087929      0t0     TCP 127.0.0.1:24705->127.0.0.1:8001 (ESTABLISHED)

对端关闭,上层代码如果不管不问的会出现什么?

go runtime 会在底层一直帮你epoll wait, 监听读事件的close报文 (空值报文) ,接着自动帮你做close fd相关, 然后在上层标记出网络连接是否发生关闭. 哪怕你的逻辑只是成功发起请求后,一直等待的sleep下去。

通过Linux strace系统调用、tcpdump能看到fin的过程,可以用tcpdump把包导出到wireshark查看.

// xiaorui.cc

# xiaorui.cc
 
epoll_wait(4, {{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=3298102768, u64=140491678354928}}}, 128, -1) = 1
clock_gettime(CLOCK_MONOTONIC, {11658405, 332899399}) = 0
futex(0x781cf8, FUTEX_WAKE, 1)          = 1
futex(0x781c30, FUTEX_WAKE, 1)          = 1
read(6, "", 4096)                       = 0
epoll_ctl(4, EPOLL_CTL_DEL, 6, {0, {u32=0, u64=0}}) = 0
close(6)                                = 0
futex(0xc42002b510, FUTEX_WAKE, 1)      = 1
futex(0x7824b0, FUTEX_WAIT, 0, NULL

附带说一下,像python requests那样,他无法在上层判断得知socket是否关闭,也没有golang runtime netpoll帮你做事件的监听及变更,只能每次去请求之前,需要先非阻塞的调用poll 指定的fd,看看是否有读事件,看看是否为空值,如果有空值的话,直接close该连接,然后重新建立连接。 另外,看了python requests及redis-py pool的代码, 他们默认都会重试一次connect的过程.

# xiaorui.cc
 
poll([{fd=3, events=POLLIN}], 1, 0)     = 1 ([{fd=3, revents=POLLIN}])
close(3)                                = 0
gettimeofday({1517632959, 600173}, NULL) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
fcntl(3, F_SETFL, O_RDWR)               = 0
connect(3, {sa_family=AF_INET, sin_port=htons(8001), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)
close(3)

下面是 连接被关闭后,net/http再次请求时发生的系统调用。 简单说,他也会重新建立一次连接。

// xiaorui.cc

[pid 20549] read(3, "", 4096)           = 0
[pid 20549] epoll_ctl(4, EPOLL_CTL_DEL, 3, {0, {u32=0, u64=0}}) = 0
[pid 20549] close(3)
[pid 20545] connect(3, {sa_family=AF_INET, sin_port=htons(8001), sin_addr=inet_addr("127.0.0.1")}, 16 
[pid 20545] <... connect resumed> )     = -1 EINPROGRESS (Operation now in progress)
[pid 20545] epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2487775088, u64=140697026457456}} 
[pid 20547] <... pselect6 resumed> )    = 0 (Timeout)
[pid 20545] close(3 

有不少人忽略的一点,只要内核的协议栈收到对方服务发起的fin请求,就会把连接的状态置为close_wait,至于什么时候去关闭释放连接要看你的httpclient库的代码实现。 time_wait的状态是谁发起关闭,那么谁的网络状态里就存在相应的time_wait连接。 短连接请求里大多是服务端根据你的keep-alive属性做是否关闭连接的逻辑,换句话说,多数是服务端发起关闭。

// xiaorui.cc

23:17:15.726467 IP localhost.8001 > localhost.48806: Flags [F.], seq 2505530211, ack 4042143564, win 512, length 0

总结:

net/http的连接池是默认全局共用的,你的后端主机虽然只有一百多台,如果我有100个协程,有概率会出现同时针对一主机并发访问,那么一个主机就有100个连接,100个后端主机就会产生10000个连接。 这问题的概率在我们生产环境中经常出现。100台没问题,那么更多呢?  单ip在主动连接可用的端口不到65535的…  所以,大家也要考虑到这问题。 

    我们当初的做法一开始是这样,进程的连接数做计数,当达到一定的阈值后,进行短连接请求,  但这样带来的问题是time wait过多,重复的建连效率也在下降。 golang net/http的MaxIdleConns也只是针对主机的维度,暂时没找到限制总连接数及主机连接池的控制入口。

     总的来说,golang net/http值得拥有,只需要简单封装下就可以顺溜溜的使用了。