프로젝트에서 Redis를 사용하면서 겪게 되었던 문제와 해결방안에 대해서 정리하고자한다.

 

가끔씩 Spring boot으로 개발한 API 서버에서 응답을 지나치게 늦게 주는 경우가 있다. 

 

Redis가 API 서버와 무슨 관련이 있을까? 하고 이야기할 수 있다.

API Server의 세션관리를 Redis로 하고 있었고, 이때 Redis client(lettuce)가 정상 동작하지 않았다.

 

관련 로그는 아래와 같다.

 

RedisCommandTimeoutException

io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)
at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51)
at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:114)
at io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:69)
at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80)
at com.sun.proxy.$Proxy94.set(Unknown Source)

위 예외에 대해서 lettuce 공식 사이트의 Frequently Asked Questions에 아래와 같이 설명하고 있다.

 

Diagnosis:

  1. Check the debug log (log level DEBUG or TRACE for the logger io.lettuce.core.protocol)
  2. Take a Thread dump to investigate Thread activity

Cause:

Command timeouts are caused by the fact that a command was not completed within the configured timeout. Timeouts may be caused for various reasons:

  1. Redis server has crashed/network partition happened and your Redis service didn’t recover within the configured timeout
  2. Command was not finished in time. This can happen if your Redis server is overloaded or if the connection is blocked by a command (e.g. BLPOP 0, long-running Lua script). See also blpop(Duration.ZERO, …) gives RedisCommandTimeoutException.
  3. Configured timeout does not match Redis’s performance.
  4. If you block the EventLoop (e.g. calling blocking methods in a RedisFuture callback or in a Reactive pipeline). That can easily happen when calling Redis commands in a Pub/Sub listener or a RedisConnectionStateListener.

Action:

Check for the causes above. If the configured timeout does not match your Redis latency characteristics, consider increasing the timeout. Never block the EventLoop from your code.

 

여러 원인중에서 1번 Redis server의 네트워크 상태가 불안정할 때 발생하다는 부분에 눈길이 갔다.

 

그 이유는 Hikari를 사용하며 db pool 관리를 할 때 방화벽에서 일정시간이 지난 idle DB 연결을 끊었고, maxLifetime으로 커넥션 갱신해줌으로써 이슈를 해결했던 것이 떠올랐기 때문이다.

 

가끔 클라우드 업체의 NAT을 사용하여 외부 솔루션 서비스를 사용하다보면 이런 경험을 하게된다.

 

내가 경험한 이슈의 원인은 이렇다.

 

서버 - 방화벽 혹은 NAT - 서버  구조에서 

 

방화벽 혹은 NAT에서 다양한 이유로 유휴 커넥션을 drop 시킨다.

 

그렇기 때문에 커넥션을 지속적으로 갱신을 하거나 커넥션 검사 후 validation을 하여 사용가능 하지 않으면 커넥션을 재요청을 해야한다.

  

lettuce는 아무런 설정을 하지 않으면 Redis server와 커넥션 1개를 생성하고 여러 스레드가 공유하도록 한다.

 

어차피 Redis server는 single thread로 동작을하여 lettuce connection pool을 만들 필요가 없다고 한다. 단, 트랜잭션 처리를 위해서 pool을 형성할 필요성이 있다고 한다.

 

7.10.1. Is connection pooling necessary?

Lettuce is thread-safe by design which is sufficient for most cases. All Redis user operations are executed single-threaded. Using multiple connections does not impact the performance of an application in a positive way. The use of blocking operations usually goes hand in hand with worker threads that get their dedicated connection. The use of Redis Transactions is the typical use case for dynamic connection pooling as the number of threads requiring a dedicated connection tends to be dynamic. That said, the requirement for dynamic connection pooling is limited. Connection pooling always comes with a cost of complexity and maintenance.

 

 

사실 lettuce와 Redis server와의 커넥션을 끊어지는 현상이 없다면 다음과 같은 시도를 하지 않았텐데 한번 시도해보기로 하였다.

spring.redis.lettuce.pool.max-active #기본값 8 
spring.redis.lettuce.pool.max-idle #기본값 8 
spring.redis.lettuce.pool.max-wait #기본값 -1ms, 풀에서 커넥션 얻을때까지 대기 시간, 음수면 무기한 
spring.redis.lettuce.pool.min-idle #기본값 0, time-between-eviction-runs있을때만 유효 
spring.redis.lettuce.pool.time-between-eviction-runs #유휴 커넥션을 제거하는 스레드의 실행 간격
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600 * 12)
@EnableRedisRepositories
public class RedisSessionConfig {
	@Value("${spring.redis.host}")
	private String redisHost;
	@Value("${spring.redis.port}")
	private int redisPort;
	@Value("${spring.redis.lettuce.pool.max-active}")
	private int maxActive;
	@Value("${spring.redis.lettuce.pool.max-idle}")
	private int maxIdle;
	@Value("${spring.redis.lettuce.pool.min-idle}")
	private int minIdle;
	@Value("${spring.redis.lettuce.pool.max-wait}")
	private int maxWait;
	@Value("${spring.redis.lettuce.pool.time-between-eviction-runs}")
	private int timeBetweenEvictionRuns;

	@Bean
	public LettuceConnectionFactory connectionFactory() {
		RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost.trim(), redisPort);
		GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
		poolConfig.setMaxTotal(maxActive);
		poolConfig.setMaxIdle(maxIdle);
		poolConfig.setMinIdle(minIdle);
		poolConfig.setMaxWaitMillis(maxWait);
		poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRuns);
		LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().poolConfig(poolConfig)
				.build();

		LettuceConnectionFactory factory = new LettuceConnectionFactory(config, clientConfig); // Single Mode
		factory.setShareNativeConnection(Boolean.FALSE);
		return factory;
	}

	@Bean
	public RedisTemplate<String, String> redisSessionTemplate() {
		RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
		redisTemplate.setConnectionFactory(connectionFactory());
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new StringRedisSerializer());
		return redisTemplate;
	}

}

 

유휴 커넥션을 지속적으로 갱신하기 위해 min-idle(5), time-between-eviction-runs(10초) 옵션을 활용했다.

 

한달동안 모니터링하였고, 결과적으로 해당 이슈를 해결되었다. 

아무리 좋은 기능도 인프라 상황에 따라서 사용할 수 없고, 우회 방법을 선택하는 경우였다.

 

이런 인프라 상황에는 차라리 Jedis를 활용하는 것이 더 맞을 것 같다는 생각이 든다


출처

- https://github.com/redis/jedis/issues/2112

- https://jronin.tistory.com/126

- https://lettuce.io/core/snapshot/reference/#faq.timeout

 

'프로그래밍 > Spring & Maven' 카테고리의 다른 글

Spring Transactional  (0) 2022.02.28
hibernateLazyInitializer  (0) 2022.01.14
프로그래밍/Spring & Maven/Scheduled debugging  (0) 2021.08.18

+ Recent posts