线上问题回顾
最近产线经常出现阶段性的服务不可用,查看pinpoint日志发现阶段性的出现大量的Redis报错,报错信息如下
RedisConnectionFailureException
java.net.SocketTimeoutException: Read timed out;
nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
通过Redis的监控发现Redis的连接数并没有到达上限,网络也很正常,Redis服务器的内存也很充足,那么究竟是什么原因导致频繁的Redis报错呢?
Redis获取连接源码解析
从RedisTemplate的get方法调用过程分析一下报错原因,get方法源码如下所示
public V get(final Object key) {
return execute(new ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
}
该方法最终执行execute方法,execute方法中会通过RedisConnectionUtils获取一个链接Connection,然后通过这个链接和Reids服务器进行网络请求,
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
//省略代码若干
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
//省略代码若干
}
下面详细介绍一下,获取链接的过程和链接的一些基本特性,我们采用了JedisPool作为redis链接池,fetchJedisConnector方法首先会判断连接池非空,并且尝试从连接池中获取连接,如果无法获取到连接或抛出异常RedisConnectionFailureException,Cannot get Jedis connection,即无法获取连接的异常。
public RedisConnection getConnection() {
if (cluster != null) {
return getClusterConnection();
}
//获取链接
Jedis jedis = fetchJedisConnector();
JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex, clientName)
: new JedisConnection(jedis, null, dbIndex, clientName));
connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
return postProcessConnection(connection);
}
protected Jedis fetchJedisConnector() {
try {
if (usePool && pool != null) {
//从JedisPool中获取链接
return pool.getResource();
}
} catch (Exception ex) {
throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
}
}
所有池化资源的思想都是大同小异的,连接池会设置空闲连接数和最大连接数,当连接数未达到最大连接数时,当收到获取连接的请求时会创建新的连接,如果达到最大连接数后则不会创建新的连接,会被阻塞。
public T getResource() {
try {
return internalPool.borrowObject();
} catch (NoSuchElementException nse) {
throw new JedisException("Could not get a resource from the pool", nse);
} catch (Exception e) {
throw new JedisConnectionException("Could not get a resource from the pool", e);
}
}
通过borrowObject方法在连接池中获取连接,如果没有空闲连接,则通过create方法创建连接,
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
assertOpen();
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
PooledObject<T> p = null;
// Get local copy of current config so it is consistent for entire
// method execution
final boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
final long waitTime = System.currentTimeMillis();
while (p == null) {
create = false;
//获取空闲链接
p = idleObjects.pollFirst();
if (p == null) {
//没有空闲链接,创建链接
p = create();
if (p != null) {
create = true;
}
}
//省略代码若干
}
}
create方法实际调用makeObject方法来创建连接,这里有个比较重要的参数connectionTimeout,该方法中通过ip、端口和超时时间等来创建Jedis对象,然后调用Jedis的connect方法来建立与Redis服务器的连接。
public PooledObject<Jedis> makeObject() throws Exception {
final HostAndPort hostAndPort = this.hostAndPort.get();
final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
try {
jedis.connect();
if (password != null) {
jedis.auth(password);
}
if (database != 0) {
jedis.select(database);
}
if (clientName != null) {
jedis.clientSetname(clientName);
}
} catch (JedisException je) {
jedis.close();
throw je;
}
return new DefaultPooledObject<Jedis>(jedis);
}
connect方法中会创建一个socket,通过socket来保持与redis服务器的连接,socket.connect方法会传入一个超时参数connectionTimeout,前面所说的RedisConnectionFailureException异常与这个参数的设置息息相关,这个参数表示的是从redis客户端这边获取连接并请求redis服务器到客户端接收到数据这个过程中的超时时间,如果一个请求在connectionTimeout设置的时间内没有返回数据就会抛出RedisConnectionFailureException异常。
public void connect() {
if (!isConnected()) {
try {
socket = new Socket();
// ->@wjw_add
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
// valid
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
// ensure timely delivery of data
socket.setSoLinger(true, 0); // Control calls close () method,
//通过socket来保持与redis服务器的连接
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
socket.setSoTimeout(soTimeout);
if (ssl) {
if (null == sslSocketFactory) {
sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
}
socket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, true);
if (null != sslParameters) {
((SSLSocket) socket).setSSLParameters(sslParameters);
}
if ((null != hostnameVerifier) &&
(!hostnameVerifier.verify(host, ((SSLSocket) socket).getSession()))) {
String message = String.format(
"The connection to '%s' failed ssl/tls hostname verification.", host);
throw new JedisConnectionException(message);
}
}
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException("Failed connecting to host "
+ host + ":" + port, ex);
}
}
}
问题分析
问题分析到这里我们从源码层面分析异常抛出的原因,即Redis服务器在connectionTimeout时间内未返回数据,这个超时时间是在JedisConnectionFactory通过setTimeOut方法来设置的,如果客户端没有进行配置会采用默认值2000ms作为超时时间,按理说2s对于支持高并发高吞吐的Redis来说压力不大,但是考虑到Redis处理请求的,当遇到大量的网络请求,大数据量的网络IO时或者执行某些比较耗时的查询时,可能会超时。
问题解决
为了尽快的恢复线上用户的使用,我们首先将这个超时时间扩大到5s,设置后报错的次数减少,但是并没有彻底解决问题,要彻底解决需要定位耗时操作,分析业务和优化代码,我们首先通过查看Redis的慢查询日志,发现大量delete操作非常耗时,有的甚至超过10s,这些delete操作的数量较大,并且数据占用的内存也很大,随后分析了这部分业务和代码,发现大量没必要的delete混存操作,并且很多都是循环嵌套的,优化代码后,产线基本没有再次出现Redis的报错。
网友评论