前言
本文更多地面向后端开发同学。虽然在后端开发中,服务间调用已经广泛采用 RPC 技术。但是由于一些原因,例如像 Dubbo 停止维护、各厂自己的 RPC 框架还存在这样那样的问题、Open API 场景的大量存在等原因,HTTP 协议在后端开发中依然被广泛采用。为了提高 HTTP 接口的调用效率,HTTP 长连接是常见的优化手段。因此本文选择介绍一下 HTTP 和 TCP 长连接这个老生常谈的话题。
本文介绍了 TCP 和 HTTP Keep Alive 的相关概念和知识,以及常用的 HTTP 客户端、服务器技术在 Keep Alive 方面相关的配置。目的在于帮助大家正确地理解相关概念,用好相关技术。
本文提到的示例代码和 TCPDUMP 文件可以在这里找到。
下文中的 Keep Alive 与长连接指同一概念,Keep Alive 是通用的称谓,但为了方便交流,有时会使用长连接
TCP 长连接 (Keep Alive)
TCP 长连接是一种保持 TCP 连接的机制。当一个 TCP 连接建立之后,启用 TCP Keep Alive 的一端便会启动一个计时器,当这个计时器到达 0 之后,一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包,但是其 Seq 与上一个包是重复的。
打个比喻,TCP Keep Alive 是这样的:
TCP 连接两端好比两个人,这两个人之间保持通信往来(建立 TCP 连接)。如果他俩经常通信(经常发送 TCP 数据),那这个 TCP 连接自然是建立着的。但如果两人只是偶尔通信。那么,其中一个人(或两人同时)想知道对方是否还在,就会定期发送一份邮件(Keep Alive 探测包),这个邮件没有实质内容,只是问对方是否还在,如果对方收到,就会回复说还在(对这个探测包的 ACK 回应)。
需要注意的是,keep alive 技术只是 TCP 技术中的一个可选项。因为不当的配置可能会引起诸如一个正在被使用的 TCP 连接被提前关闭这样的问题,所以默认是关闭的
HTTP 长连接 (Keep Alive)
在 HTTP 1.0 时期,每个 TCP 连接只会被一个 HTTP Transaction(请求加响应)使用。之后,这个 TCP 连接便会被关闭。当网页内容越来越复杂,包含大量图片、CSS 等资源之后,这种模式效率就显得太低了。所以,在 HTTP 1.1 中,引入了 HTTP persistent connection 的概念,也称为 HTTP keep-alive(后面统一称呼为 HTTP 长连接)。
HTTP 1.0 和 1.1 在 TCP 连接使用方面的差异可见下图:
当需要建立 HTTP 长连接时,HTTP 请求头将包含如下内容:
Connection: Keep-Alive
如果服务端同意建立长连接,HTTP 响应头也将包含如下内容:
Connection: Keep-Alive
当需要关闭连接时,HTTP 头中会包含如下内容:
Connection: Close
也打个比方:
两个人互相通信,一个人发信,并附上一句,我们保持联系好吗。另一个人在回信中写到,好的,我们继续保持联系。此后,他们每封信里都写上相同的内容。直到有一天,友谊的小船翻了,一个人在回信里写到,我们不要再联系了。另一个人信守承诺,回道:好的,我们不联系了。从此两人天各一方。。。
TCP Keep Alive 与 HTTP Keep Alive 的关系
如上文的解释,TCP Keep Alive 和 HTTP Keep Alive 是两个目的不同的技术,不存在谁依赖于谁的关系。TCP Keep Alive 用于探测对端是否存在,而 HTTP Keep Alive 用于协商以复用 TCP 连接。即便一个 TCP 连接未启用 Keep Alive 功能,也不妨碍 HTTP 层面开启长连接。
但如果 TCP Keep Alive 的 interval 数值设置果断,就可能导致 HTTP 无法重复利用已建立的 TCP 连接。
HTTP 客户端配置
Apache HTTP Client
Apache HTTP Client 存在两种版本,在 3.x 版本时被称为 Commons HttpClient,4.x 后 Apache 创建了一个新的独立项目:Apache HttpComponents。这里指的是后者。
话不多说,看代码:
HttpClient buildHttpClient() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(2, TimeUnit.MINUTES);
connectionManager.setMaxTotal(DEFAULT_MAX_PER_ROUTE * 2);
connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
HttpClientBuilder builder = HttpClients.custom()
.setConnectionManager(connectionManager);
return builder.build();
}
上面这段代码展示了如何创建一个使用连接池的 HttpClient
的方法。当使用连接池后,HttpClient
就不必为每个请求创建一个单独的 TCP 连接了。
OkHttp
OkHttp 是国外的互联网支付公司 Square 的产品,优点是比较轻量,主要面向 Android 开发 使用,也可以使用在服务端开发场景中。使用 OkHttp 设置 HTTP 长连接比较简单,默认就会开启。只需创建一个 OkHttpClient
即可。
OkHttpClient client = new OkHttpClient();
这个 client 应该是单例的,因为它有自己的连接池。
不同于 Apache HTTP Client 的连接池,OkHttp 的连接池无法设置开启最大的连接数。有多少个线程使用 OkHttpClient
发送请求,其就会创建多少个连接。
OkHttp 实现 HTTP 连接池的组件是 ConnectionPool
。虽然其不能设置最大连接数,但是可以设置最大空闲连接数和连接超时时间。
RestTemplate
RestTemplate 是 Spring 框架提供的 HTTP 客户端组件,适合调用 RESTful API 的场景。其并没有直接实现 HTTP 客户端功能,而是利用现有的组件。下面仅给出 RestTemplate 使用 Apache HTTP Client 时如何配置连接池:
@Profile("apache")
@Bean
public ClientHttpRequestFactory apacheHttpClientFactory() {
return new CustomHttpComponentsClientHttpRequestFactory();
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);
return restTemplate;
}
static class CustomHttpComponentsClientHttpRequestFactory extends
HttpComponentsClientHttpRequestFactory {
CustomHttpComponentsClientHttpRequestFactory() {
setHttpClient(buildHttpClient());
}
private HttpClient buildHttpClient() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(2, TimeUnit.MINUTES);
connectionManager.setMaxTotal(CONN_POOL_SIZE * 2);
connectionManager.setDefaultMaxPerRoute(CONN_POOL_SIZE);
HttpClientBuilder builder = HttpClients.custom()
.setConnectionManager(connectionManager);
return builder.build();
}
}
操作系统设置
因为服务器操作系统很少采用 Windows,所以这里之谈 Linux 的相关配置。
在 Linux 操作系统中,TCP Keep Alive 相关的配置可以在 /proc/sys/net/ipv4/
目录中找到,具体有下面三个(括号中的是默认值):
- tcp_keepalive_time (7200)
- tcp_keepalive_intvl (75)
- tcp_keepalive_probes (9)
tcp_keepalive_time 的含义是在空闲相应时间(单位是秒)之后,TCP 协议栈将发送 Keep Alive 探测包;tcp_keepalive_intvl 的含义是开始探测之后每个探测包所间隔的时长,单位同样是秒;tcp_keepalive_probes 是探测包的个数。
默认的配置通常不适合一般的 Web 服务器,实际参数会远小于上述默认值。
正如前文所说,TCP Keep Alive 是用来检测 TCP 连接有效性的一种机制,如果一个空闲的 TCP 连接如果失效,那究竟多久能发现?假设采用如下配置:
- tcp_keepalive_time = 60
- tcp_keepalive_intvl = 10
- tcp_keepalive_probes = 6
那将在 60 + 10 * 6 = 120s,即两分钟之后发现一个失效的 TCP 连接。
Nginx Keep Alive 配置
相较于有着丰富选择的 HTTP 客户端,在负载均衡服务器方面选择不是那么多,Nginx 是较为常见的选择。下面一段是启用了 HTTP 长连接的配置:
events {
use epoll;
worker_connections 102400;
}
http {
upstream keepAliveService {
server 10.10.131.149:8080;
keepalive 20;
}
server {
listen 80;
server_name keepAliveService;
location /keep-alive/hello {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://keepAliveService;
}
}
}
上面这段配置中,对于 HTTP 长连接来说至关重要的有这么几项:
- keepalive
- proxy_http_verion
- proxy_set_header
如果想要启用 HTTP 长连接,那这三项配置都是必须的。下面分别介绍。
keepalive
顾名思义,这个参数用于控制可连接整个 upstream servers 的 HTTP 长连接的个数,即控制总数。但需要注意的是,这个参数并不控制所有 upstream servers 连接的个数。
proxy_http_verion
这个用于控制代理后端链接时使用的 HTTP 版本,默认为 1.0。要想使用长连接,必须配置为 1.1。除了 location 以外,这项配置还可以放在 http 和 server 这两级中。
proxy_set_header
除了要将 HTTP 协议设置为 1.1 以外,还要清空 Connection header 中的值。如果不配置 proxy_set_header Connection ""
,则发往 upstream servers 的请求中,Connection header 的值将为 close
,导致无法建立长连接。
同 proxy_http_version
一样,这项配置也可以放在 http 和 server 这两级中。
应用服务器配置
因为本文的示例程序使用 Spring Boot(版本为 1.5.2)开发,所以介绍 Spring Boot 中的两种应用服务:Tomcat 和 Undertow。
Tomcat
Spring Boot 使用的 Tomcat 版本为 8.5.11,在使用默认配置情况下,每一个 HTTP 长连接会保持 10s 中,10s 之后,Tomcat 会主动断开连接。
从 HTTP 消息内容来看,虽然发往 Tomcat 的 HTTP 请求中带有 Connection: Keep-Alive
,但是 Tomcat 提供的相应中并没有带有 Connection: Keep-Alive
header。所以,严格来说,HTTP 长连接并没有建立成功(响应中没有包含 Connection: Keep-Alive
说明服务器端并没有同意建立长连接)。
但是,与 Tomcat 建立的连接确实被复用了,只是没有持续太长时间(10s)
Undertow
Undertow 是 JBoss 新推出的 Web 容器,特点在于高性能,有着远好于 Tomcat 的性能。
在对 HTTP 长连接支持方面,Undertow 的行为同 Tomcat 不同,不会主动关闭 HTTP 连接,也减少了建立、关闭 TCP 连接所带来的性能消耗。
长连接与短连接的性能对比
追求更好的性能通常是使用 HTTP 长连接的目的,那采用 HTTP 长连接之后,究竟有哪些性能提高?
接口响应时间
测试使用 JDK 8 编写的应用,应用服务器采用 Undertow,接口使用 Spring MVC 开发,接口耗时 100ms(Sleep 100ms),TPS 为 1000 左右,HTTP 客户端采用 Apache HTTP Client,配置 100 个连接的连接池,经过 Nginx 转发。非长连接模式是在 Nginx 配置中将 proxy_set_header Connection ""
去掉。
经过三次测试,在接口平均响应时间方面,使用长连接比不使用长连接少 1ms。
采用长连接:
c.github.yanglifan.KeepAliveApplication : Average time is 106
不采用长连接:
c.github.yanglifan.KeepAliveApplication : Average time is 107
可见,在平均响应时间方面,长连接和短连接差距很小。
服务器负载
虽然接口平均响应时间方面差距不大,但是在服务器负载方面,应用长连接确有实实在在的好处。以 Nginx 服务器为例
使用长连接时:
不使用长连接时:
平均来看,在上述负载的情况下,使用长连接时 Nginx 的 CPU 占用率为 5% 左右,不使用长连接时为 9% 左右,可见差距还是比较明显的。也说明了使用长连接的好处,就是通过避免服务器频繁建立连接,降低服务器的负载。
附:Linux 命令
netstat 命令
netstat 是常用的用来查看网络相关数据的命令,常用的参数有 anp。这三个参数比较常用,不在复述其用途。
不太常用的参数由 o,o 的意思是显示 TCP Timers。当增加 o 这个参数时,可能看到的结果是这样的:
keepalive (6176.47/0/0)
也可能是这样的
off (0.00/0/0)
当为 keepalive 时,表明 TCP 协议栈开启了 keep alive timers,后面括号中的值分别是 timer 所剩时长/重传次数/keepalive probe 次数。当为 off 时,表明没有开启 TCP keepalive timer。当然,正如前面所说,这个 timer 是否开启,和 HTTP keep alive 没有必然联系。
watch 命令
当查看服务器和客户端之间是否启用 HTTP keep alive 时,除了抓包以外,更为快速的方法是使用 netstat 命令看连接两次的端口是否不变。如果客户端端口一直不变,说明服务端和客户端之间一直保持着同一个连接。
但是,手工不停执行 netstat 命令不是个好办法,这里就要清楚 watch 命令。
watch 命令可以实现命令的反复执行,示例如下:
watch -n1 "netstat -anpo | grep :8080 | grep EST | grep 10.110.24.202"
引号内的便是需要执行的命令。-n 的参数用来指定重复执行的间隔,默认为 2秒。
总结
总结一下,首先 HTTP 和 TCP 的长连接(keep alive)是不同的机制,之间没有必然联系。而在 HTTP 方面,不同的客户端、服务器端产品的行为也是不同的:Apache HTTP Client 可以通过配置连接池、OkHttp 同样可以使用连接池,但是可配置的参数不多、RestTemplate 基于其它 HTTP Client 的实现;Nginx 默认不开启 HTTP 长连接,也需要配置,使用 HTTP 长连接可以降低 Nginx 负载;应用服务器对 HTTP 长连接的行为也有不同,需要分别对待。