HTTP keep-alive 也称为 HTTP 长连接。它通过重用一个 TCP 连接来发送/接收多个 HTTP请求,来减少创建/关闭多个 TCP 连接的开销。keep-alive 用不用?怎么用?
什么是 keep-alive?
keep-alive 是客户端和服务端的一个约定,如果开启 keep-alive,则服务端在返回 response 后不关闭 TCP 连接;同样的,在接收完响应报文后,客户端也不关闭连接,发送下一个 HTTP 请求时会重用该连接。
在 HTTP/1.0 协议中,如果请求头中包含:
Connection: keep-alive |
则代表开启 keep-alive,而服务端的返回报文头中,也会包含相同的内容。
在 HTTP/1.1 协议中,默认开启 keep-alive,除非显式地关闭它:
Connection: close |
用还是不用,这是个问题
keep-alive 技术创建的目的,就是能在多次 HTTP 之间重用同一个 TCP 连接,从而减少创建/关闭多个 TCP 连接的开销(包括响应时间、CPU 资源、减少拥堵等),参考如下示意图(来源:维基百科):
然而天下没有免费的午餐,如果客户端在接收完所有的信息之后还没有关闭连接,则服务端相应的资源还在被占用(尽管已经没用了)。例如 Tomcat 的 BIO 实现中,未关闭的连接会占用对应的处理线程,如果一个长连接实际上已经处理完毕,但关闭的超时时间未到,则该线程会一直被占用(使用 NIO 的实现没有该问题)。
显然,如果客户端和服务端的确需要进行多次通信,则开启 keep-alive 是更好的选择,例如在微服务架构中,通常微服务的使用方和提供方会长期有交流,此时最好开启 keep-alive。
在一些 TPS/QPS 很高的 REST 服务中,如果使用的是短连接(即没有开启keep-alive),则很可能发生客户端端口被占满的情形。这是由于短时间内会创建大量TCP 连接,而在 TCP 四次挥手结束后,客户端的端口会处于 TIME_WAIT一段时间(2*MSL),这期间端口不会被释放,从而导致端口被占满。这种情况下最好使用长连接。
客户端如何开启?
现在我们用到的几乎所有工具都是默认开启长连接的:
- 对于浏览器而言,几乎你现在用的浏览器(包括 IE6)都默认使用 keep-alive 了。
- Java8 中的
HttpURLConnection
默认开启长连接,但是默认连接池中只保留 5 个长连接1,如果同时超过 5 个线程在使用,则会创建新的连接,结束后多于 5 个的部分会被客户端主动关闭。 - Apache
HttpClient
默认为每个地址保留 2 个长连接,连接池中最多共保留 20 个连接2。 - Python requests 如果使用 session 则会默认开启长连接。
下面是一些代码备忘:
Feign 使用 HttpClient 连接池示例
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); |
服务端如何实现
不同的服务端对 keep-alive 的实现方式不同,就连 tomcat 不同的工作模式下,处理的方式也不同。这里大致说下 NIO 模式(tomcat 9.0.22)下的处理逻辑:
-
在
NioEndpoint#SocketProcessor
类中,只会关闭内部状态为CLOSED
的端口:if (state == SocketState.CLOSED) {
poller.cancelledKey(key, this.socketWrapper);
} -
而在
Http11Processor#service
方法中,如果是 keep-alive 的连接,最终的内部状态会是OPEN
} else if (this.openSocket) {
return this.readComplete ? SocketState.OPEN : SocketState.LONG;
} else { -
被保留的连接,超时时间之后,会在
NioEndpoint#Poller#timeout
方法中被关闭:} else if (!NioEndpoint.this.processSocket(socketWrapper, SocketEvent.ERROR, true)) {
this.cancelledKey(key, socketWrapper);
}
另外,如果使用 spring boot,可以通过 server.connection-timeout
配置项来调整
keep-alive 连接的保留时间,如果不设置则为每个 server 自己的默认配置,Tomcat 默认为 60s3。
抓包实验
抓包之下,再复杂的逻辑也将显露无疑。我们使用 Wireshark 抓包,看到使用了 keep-alive 的请求如下:
- 第二次 Http 请求时,并没有创建的连接的过程(没有
SYN
),而是重用之前的连接 - 在默认 60s 超时后,由服务端发送
FIN
报文关闭连接。
而不开启 keep-alive 的请求过程如下:
可以看到,与 keep-alive 不同,每次请求结束时都关闭当前连接,之后重新创建新的连接。
小结
文章比较杂,可能真正有用的就是:如果使用基于 Http 的微服务,可以使用长连接来解决一些问题。
参考
- HTTP Keepalive Connections and Web Performance Nginx 谈长连接与服务器性能的关系(文章比较老)
- 复习:TCP 三次握手、四次挥手 本站的文章,复习 TCP 连接的状态转换,尝试理解 TIME_WAIT 的影响
- https://techblog.bozho.net/caveats-of-httpurlconnection/ 使用 HttpURLConnection 的一些坑
- Appendix A. Common application properties Spring Boot 通用配置项