JedisConnectionException问题分析与处理

·  阅读 505

线上问题回顾

最近产线经常出现阶段性的服务不可用,查看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的报错。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改