前言:
大家潜规则的会认为标准库里的http库肯定不好用,我先前也是这么考虑的,后来发现golang社区里的http client基本是围绕net/http造轮子。 那些go http client会像python requests那样好用,而不是在功能和底层上有提升。
既然大家都在用net/http,那么有必要深入测试下,别掉坑里。 先前使用python requests的时候就被坑了好几次,好在结合requests源代码找到了答案. 这篇主要说下测试net/http连接池的测试结果,下篇会聊聊go net/http连接池是怎么实现的,具体到源码方面的体现.
该文章后续仍在不断更新中, 请移步到原文地址 http://xiaorui.cc/?p=5056
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值得拥有,只需要简单封装下就可以顺溜溜的使用了。