连接池剖析及连接泄漏定位思路
背景最近连续解决两起连接泄漏的问题,期间阅读了大量开源源码,发现开源软件中设计的连接池,用的也都是一些常规手段,本文为大家揭开这层神秘的面纱。概述大多数应用中应该都是用的TCP协议,TCP连接在建立阶段会经过3次握手,销毁阶段会经过4次握手。标准网络是分7层的,每一层都有各自的协议头,应用层拿到的有效数据,在整个报文中占比没这么大。TCP握手阶段的报文,对应用层来讲都是额外的负担。所以很多客户端都
背景
最近连续解决两起连接泄漏的问题,期间阅读了大量开源源码,发现开源软件中设计的连接池,用的也都是一些常规手段,本文为大家揭开这层神秘的面纱。
概述
大多数应用中应该都是用的TCP协议,TCP连接在建立阶段会经过3次握手,销毁阶段会经过4次握手。标准网络是分7层的,每一层都有各自的协议头,应用层拿到的有效数据,在整个报文中占比没这么大。TCP握手阶段的报文,对应用层来讲都是额外的负担。所以很多客户端都会设计连接池,来复用已建立的连接。减少创建连接带来的消耗。
连接池本身不复杂,如果不想做的通用,少量的代码就能够实现。下图是一个连接池中必要的组件。
上图各组件含义:
- Client-客户端,屏蔽底层连接细节,使用它可以方便的与远端交互;比如RedisTemplate、HttpClient、KafkaProducer等。
- Connection Manager-连接管理器,Client内部在与远端交互时需要获得连接,连接既然是被复用的肯定是需要被管理起来,因此衍生出连接管理器的概念。
- Pool-缓冲池,用于存储连接的;里面分idle、in use。
- Validator-校验器,当应用负载较低的时候,连接池中的连接可能很久后才会被重新使用。有时候服务端会配置超时时间,连接空闲一段时间后会被服务端主动关闭,因此连接池中的连接可能已经无效了。获取空闲连接后,需要使用校验器进行校验,防止无效的空闲连接返回到上层。
- Cleaner-清理器,连接池一般会配置两个参数,分别为MinConnectionCount和MaxConnectionCount,类似java线程池中的核心线程数和最大线程数,用于应对闲时和忙时,减少资源浪费。经历过忙时后,不需要这么多连接资源了,因此需要有机制去清理闲置资源。
- Connection Factory-连接工厂,Connection Manager仅仅只负责管理职责,为了方便扩展,需要抽象出连接工厂这个概念,当连接池的资源不足,需要创建实际的连接时,由这个对象负责。
重点对象分析
connection wrapper
连接池的设计目标就是让使用者无感,如果底层的connection不进行包装,直接返回给上层使用,上层使用完后调用close方法就会销毁这个对象,使之无法得到复用。因此需要使用wrapper包装connection,并覆写close等方法进行拦截,让框架有机会去进行资源回收。
下面以Jedis的源码为例。
Jedis基于Apache的commons pool来构建连接池。
JedisConnectionFactory担任的是Connection Manager的职责。
JedisFactory同时担任Connection Factory和Validator的职责。
从源码中可以看到,创建出的Jedis会被JedisConnection 包装。不过严格来讲,JedisConnection并不是为了去拦截close操作而对Jedis进行包装的。Jedis自己就持有Pool,自己就对close方法做了拦截,并进行连接回收。细节部分大家可以自己顺着代码往下看。
org.springframework.data.redis.connection.jedis.JedisConnectionFactory#getConnection
public RedisConnection getConnection() {
Jedis jedis = fetchJedisConnector();
# 对Jedis进行包装
JedisConnection connection = (getUsePool() ? new JedisConnection(jedis, pool, getDatabase(), getClientName())
: new JedisConnection(jedis, null, getDatabase(), getClientName()));
connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
return postProcessConnection(connection);
}
Cleaner
本着“粒粒皆辛苦的原则”,大多数应用在设计时,肯定会考虑资源闲置的问题。系统忙时和闲时对资源要求不一样,因此连接数也有两个配置,最大数量和最小数量。资源一旦创建出来,不主动回收,就会一直闲置在这里。因此需要有个cleaner的角色对闲置资源进行回收。
回收一般也只有两种方法:
- 设置异步线程,定时去扫描闲置资源,并进行释放
- 在获取连接时,先执行一次清理逻辑。
异步线程回收资源
目前大部分应该都是采用这种方案,采用这种方案时要小心,清理逻辑里面要考虑掉各种异常并捕获。不然有未捕获的异常导致线程终止,那就永远失去了回收资源的能力。以AWS-S3的客户端组件为例:
AWS-S3存储和获取对象时使用的是HTTP协议,该客户端基于httpcomponents。
跟着代码最终可以定位到ApacheHttpClientFactory的create方法。
com.amazonaws.AmazonWebServiceClient#client(字段)
com.amazonaws.http.apache.client.impl.ApacheHttpClientFactory#create
# 创建HttpClient
public ConnectionManagerAwareHttpClient create(HttpClientSettings settings) {
HttpClientBuilder builder = HttpClients.custom();
HttpClientConnectionManager cm = (HttpClientConnectionManager)this.cmFactory.create(settings);
builder.setRequestExecutor(new SdkHttpRequestExecutor()).setKeepAliveStrategy(this.buildKeepAliveStrategy(settings)).disableRedirectHandling().disableAutomaticRetries().setConnectionManager(ClientConnectionManagerFactory.wrap(cm));
ConnectionManagerAwareHttpClient httpClient = new SdkHttpClient(builder.build(), cm);
# 注册连接池清理器
if (settings.useReaper()) {
IdleConnectionReaper.registerConnectionManager(cm, settings.getMaxIdleConnectionTime());
}
return httpClient;
}
下面两个为IdleConnectionReaper的源码
public static boolean registerConnectionManager(HttpClientConnectionManager connectionManager, long maxIdleInMs) {
if (instance == null) {
synchronized (IdleConnectionReaper.class) {
if (instance == null) {
instance = new IdleConnectionReaper();
instance.start();
}
}
}
return connectionManagers.put(connectionManager, maxIdleInMs) == null;
}
IdleConnectionReaper是Thread子类,调用其start方法就会启动一个线程去执行run方法。从run方法逻辑可以看到,就是调用HttpClientConnectionManager去清理闲置链接。清理方法就是判断线程的闲置时间是否超过设定的阈值。
public void run() {
while (!shuttingDown) {
for (Map.Entry<HttpClientConnectionManager, Long> entry : connectionManagers.entrySet())
entry.getKey().closeIdleConnections(entry.getValue(), TimeUnit.MILLISECONDS);
}
Thread.sleep(PERIOD_MILLISECONDS);
}
}
获取连接时同步进行回收
目前没有看到有那个组件采用这种方式,像Jedis甚至没有把清理机制加进去。
common pool是有设计去清理空闲连接的-org.apache.commons.pool2.impl.GenericObjectPool#evict。只是Jedis封装pool后,没有去调用这个逻辑。
validator
如上所述,validator用于验证从连接池获取的连接是否有效,因为连接池长期闲置的连接因为服务端主动关闭导致不可用。一般validator的功能是放在连接工厂里面的,common pool给PooledObjectFactory定义了一下方法。
public interface PooledObjectFactory<T> {
# 创建连接
PooledObject<T> makeObject() throws Exception;
# 销毁连接
void destroyObject(PooledObject<T> p) throws Exception;
# 校验连接是否有效
boolean validateObject(PooledObject<T> p);
# 激活连接,应该只是修改一下状态
void activateObject(PooledObject<T> p) throws Exception;
# 无效连接,应该只是修改一下状态
void passivateObject(PooledObject<T> p) throws Exception;
}
来看一下Jedis如何实现有效性校验的redis.clients.jedis.JedisFactory#validateObject
Jedis主要校验主机名和端口是否一致(这个主要是考虑哨兵模式,哨兵模式会发生主从切换,老连接清理没那么及时),然后使用“Ping”命令判断连接是否仍然联通。
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();
try {
HostAndPort hostAndPort = (HostAndPort)this.hostAndPort.get();
String connectionHost = jedis.getClient().getHost();
int connectionPort = jedis.getClient().getPort();
# 校验逻辑
return hostAndPort.getHost().equals(connectionHost) && hostAndPort.getPort() == connectionPort && jedis.isConnected() && jedis.ping().equals("PONG");
} catch (Exception var6) {
return false;
}
}
重点:像Redis这种长连接,而且也设计了连通性检测命令,能够很方便的设计失效校验逻辑。像HTTP协议就得精心去设计这个逻辑了。因为HTTP是单次单向通信,后面为了提高性能是HTTP1.1中加入了keep alive特性。对于协议本身来说没有设计保活检测机制,只能依赖socket的检测机制。socket远端是否断开连接有2种方法:
- socket类中有一个方法sendUrgentData,它会往输出流发送一个字节的数据,只要对方Socket的SO_OOBINLINE属性没有打开,就会自动舍弃这个字节(在Java 中是抛出异常),而SO_OOBINLINE属性默认情况下就是关闭的。
缺点:不可靠,得远端开启SO_OOBINLINE。 - 调用socket的read方法,看返回值是不是-1.
HttpClient采用的是方法2,方法2也有一个问题要解决,如果read方法没有返回-1,而且还有字节返回。返回的字节是有效数据,不能随意丢弃。而且此时的检测逻辑与业务无关,还没办法把读出来的字节返回给上层。
HttpClient采用了巧妙的设计,通过包装socket的inputstream。类似bufferedInputstream包装inputstream,为inputstream建立缓存机制,如果检测时读取到的状态是-1,他们认为连接已失效,进行销毁;如果读取到了有效字节,则先放到缓存区中,待上层应用使用。
追踪连接获取的过程,
org.apache.http.impl.conn.PoolingHttpClientConnectionManager#requestConnection
org.apache.http.impl.conn.PoolingHttpClientConnectionManager#leaseConnection
org.apache.http.pool.AbstractConnPool#lease(T, java.lang.Object, org.apache.http.concurrent.FutureCallback)
代码有删减
public E get(final long timeout, final TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
for (;;) {
synchronized (this) {
try {
final E entry = entryRef.get();
final E leasedEntry = getPoolEntryBlocking(route, state, timeout, timeUnit, this);
if (validateAfterInactivity > 0) {
if (leasedEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis()) {
# 校验连接的有效性
if (!validate(leasedEntry)) {
leasedEntry.close();
release(leasedEntry, false);
continue;
}
}
}
}
}
}
}
连接有效性校验最终追溯到org.apache.http.impl.BHttpConnectionBase#isStale,可以看到是通过读取socket,并看返回值是否-1来判断连接是否失效。
public boolean isStale() {
try {
# 从socket里面读取字节并存储到buffer中并返回实际读取的字节数,如果返回-1说明socket已经被远端关闭
final int bytesRead = fillInputBuffer(1);
return bytesRead < 0;
}
}
private int fillInputBuffer(final int timeout) throws IOException {
try {
return this.inBuffer.fillBuffer();
}
}
public int fillBuffer() throws IOException {
# 类似java NIO中的buffer,有pos和limit。需要把未读的字节移到buffer的起始位置,方便空出位置来容纳新的字节
if (this.bufferPos > 0) {
final int len = this.bufferLen - this.bufferPos;
if (len > 0) {
System.arraycopy(this.buffer, this.bufferPos, this.buffer, 0, len);
}
this.bufferPos = 0;
this.bufferLen = len;
}
final int readLen;
final int off = this.bufferLen;
final int len = this.buffer.length - off;
# 读取socket里面的字节并,存储到buffer。buffer作为socket的缓存提供给上层使用。
readLen = streamRead(this.buffer, off, len);
if (readLen == -1) {
return -1;
}
this.bufferLen = off + readLen;
return readLen;
}
附-AWS-S3连接泄漏定位
接到一个case,某个功能从S3上传下载文件时抛了异常,com.amazonaws.SdkClientException: Unable to execute HTTP request: Timeout waiting for connection from pool。
之前使用HttpClient,在并发量较大的情况下也报了这个错,后面发现是HttpClient开启连接池的情况下,默认最大连接数是5,当达到上限后,需要等待其他线程释放连接,而且这个等待时间也有上限,默认好像是60s。超时仍然无法获得连接就会报上面这个错。
因此第一反应进后台看看连接情况。
从图中看到连接数已经达到上线50(AWS客户端默认配置上线是50)。当看到状态全部为close_wait状态第一反应就是有泄漏。因为连接池有清理机制。
- 如果这些是正在使用的连接,那正在读取数据,状态不可能为close_wait.
- 如果这些连接已经被回收,而且空闲时间又较长,理论上应该会被清理掉。
基于上面判断,等待一段时间后发现查出来的数据不变,而且也咨询过项目组的人,目前没有人测试这个功能,因此断定是连接泄漏。
下一步就是像之前定位Redis连接数不断增长的问题一样;DUMP内存,然后看连接池中的连接是否状态是in use,并且除了连接池本身没有其他对象持有他们。如下图印证自己的猜测,剩的就是排查该功能的代码,发现调用s3时没有关闭资源的地方。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)