池的本质意义在于复用: 创建连接池时,很可能一次性创建了多个连接,大多数连接池考虑到性能,会在初始化的时候维护一定数量的最小连接(毕竟初始化连接池的过程一般是一次性的),可以直接使用。如果每次使用连接池都按需创建连接池,那么很可能你只用到一个连接,但是创建了N个连接。 连接池一般会有一些管理模块,即连接池的结构示意图中的绿色部分。 大多数的连接池都有闲置超时。连接池会检测连接的闲置时间,定期回收闲置的连接,把活跃连接数降到最低(闲置)连接的配置值,减轻服务端的压力。 一般闲置连接由独立线程管理,启动空闲检测的连接池相当于还会启动一个线程。 有些连接池还需独立线程负责连接保活功能。因此,启动一个连接池相当于启动了N个线程。
连接池不释放,还可能会引起线程泄露:
使用这个连接来请求一个会返回OK字符串的服务端接口:
访问该接口几次后执行jstack,可以看到有很多叫作Connection evictor的线程,且这些线程不会销毁
➜ ~ jstack 86194 | grep evictor
"Connection evictor" #120 daemon prio=5 os_prio=31 tid=0x00007fbe708ac000 nid=0xd207 waiting on condition [0x0000700009b90000]
"Connection evictor" #119 daemon prio=5 os_prio=31 tid=0x00007fbe70475000 nid=0x13713 waiting on condition [0x00007000066f4000]
"elasticBounded-evictor-1" #115 daemon prio=5 os_prio=31 tid=0x00007fbe6f090800 nid=0xd503 waiting on condition [0x000070000b0cf000]
"Connection evictor" #85 daemon prio=5 os_prio=31 tid=0x00007fbe6e5a4800 nid=0x14307 waiting on condition [0x0000700009c93000]
大部分都是HttpClient到Tomcat的连接:
➜ ~ lsof -nP -i4TCP:30666 | wc -l
8
有空闲连接回收策略
➜ ~ lsof -nP -i4TCP:30666
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 91607 apple 518u IPv6 0xe45224391c3078b1 0t0 TCP *:30666 (LISTEN)
java 91607 apple 526u IPv6 0xe4522438da39cf31 0t0 TCP 127.0.0.1:64404->127.0.0.1:30666 (ESTABLISHED)
java 91607 apple 528u IPv6 0xe4522438da3a0031 0t0 TCP 127.0.0.1:30666->127.0.0.1:64404 (ESTABLISHED)
java 91607 apple 529u IPv6 0xe4522438da39db71 0t0 TCP 127.0.0.1:64414->127.0.0.1:30666 (ESTABLISHED)
java 91607 apple 530u IPv6 0xe45224390b2aedd1 0t0 TCP 127.0.0.1:30666->127.0.0.1:64414 (ESTABLISHED)
java 91607 apple 531u IPv6 0xe45224390b2b0651 0t0 TCP 127.0.0.1:64422->127.0.0.1:30666 (ESTABLISHED)
java 91607 apple 532u IPv6 0xe45224390b2ad551 0t0 TCP 127.0.0.1:30666->127.0.0.1:64422 (ESTABLISHED)
# 60s后连接处于CLOSE_WAIT状态,最终彻底关闭。
➜ ~ lsof -nP -i4TCP:30666
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 91607 apple 518u IPv6 0xe45224391c3078b1 0t0 TCP *:30666 (LISTEN)
java 91607 apple 529u IPv6 0xe4522438da39db71 0t0 TCP 127.0.0.1:64414->127.0.0.1:30666 (CLOSE_WAIT)
以上结果表明CloseableHttpClient属内部带有连接池的API。
把CloseableHttpClient声明为static,只创建一次,并在JVM关闭前通过addShutdownHook钩子关闭连接池,在使用的时候直接使用CloseableHttpClient即可,无需每次都创建。
定义一个case2接口实现服务端接口调用:
定义个case3接口,修复之前按需创建CloseableHttpClient的代码,每次用完后确保连接池可关闭
使用wrk对case2和case3分别压测60s,可见两种使用方式的性能差异:
➜ ~ wrk -c1 -t1 -d 10s http://localhost:30666/httpclientnotreuse/case3
Running 10s test @ http://localhost:30666/httpclientnotreuse/case3
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.75ms 2.94ms 39.59ms 97.61%
Req/Sec 728.82 170.53 0.95k 69.00%
7260 requests in 10.00s, 816.67KB read
Requests/sec: 725.78
Transfer/sec: 81.64KB
➜ ~ wrk -c1 -t1 -d 10s http://localhost:30666/httpclientnotreuse/case2
Running 10s test @ http://localhost:30666/httpclientnotreuse/case2
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 411.81us 387.15us 11.83ms 97.72%
Req/Sec 2.53k 193.26 2.76k 83.17%
25428 requests in 10.10s, 2.79MB read
Requests/sec: 2517.61
Transfer/sec: 283.21KB
大的性能差距就在于TCP连接的复用。定义连接池时,我将最大连接数设置为1。所以,复用连接池方式复用的始终应该是同一个连接,而新建连接池方式应该是每次都会创建新的TCP连接。
若调用case3接口,每次创建新的连接池发起HTTP请求,可以看到每次请求服务端30666的客户端端口都是新的。这里我发起了三次请求,程序通过HttpClient访问服务端30666的客户端端口号,分别是63696、63697、63698
即每次都是新的TCP连接,去掉HTTP这个过滤条件,即可看到TCP握手、挥手过程
而复用连接池方式的接口case2的表现就完全不同了。可以看到,第二次HTTP请求的客户端端口56377和第一次连接的端口是一样的。