ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot + Redis 삽질과 디버깅
    Server/Redis 2020. 10. 6. 23:53

    레디스가 만능은 아니다

     - Connection수를 최소화 하는 것이 중요하다.

     - Redis Set과 Redis List

     

     

    1) Redis에 Multi Thread로 Push를 때리는 것은 위험천만하다.

    WAS에서 특정 데이터를 REDIS에 저장할 때, REDIS와 WAS 사이의 통신 속도 만큼 지연이 발생한다. 가령 유저가 방문했을 때 방문 히스토리를 REDIS에 쌓는다고 할 때, REDIS와 WAS사이에 30ms 의 지연이 발생한다면, 별도의 Thread에서 처리를 하지 않고, Main Thread에서 Redis에 Push를 하게 된다면 그 만큼 응답 속도는 30ms 만큼 느려지게 된다.

     

    "어? 그럼 Sub Thread에서 Push 때리면 되는 거 아냐?"

     

    Redis를 죽일 수 있는 1방의 코드

    Thread thread = new Thread(()->{template.opsForSet().add(key, obj); }); thread.start();

    일단 Jedis의 경우 max-active 만큼의 connection을 사용하면 대기하게 된다. 무한 대기의 늪에 빠진다. 

     

    그리고 Lettuce 같은 경우에는 Max-Active 만큼 연결을 한 이후에는 연결을 강제로 끊어버린다!

    xception in thread "Thread-9992" Exception in thread "Thread-9983" org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
    	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1534)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1442)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnection.doGetAsyncDedicatedConnection(LettuceConnection.java:1026)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getOrCreateDedicatedConnection(LettuceConnection.java:1012)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getDedicatedConnection(LettuceConnection.java:981)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getDedicatedRedisCommands(LettuceConnection.java:1020)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnection.watch(LettuceConnection.java:758)
    	at org.springframework.data.redis.core.RedisTemplate.lambda$watch$23(RedisTemplate.java:1038)
    	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228)
    	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188)
    	at org.springframework.data.redis.core.RedisTemplate.watch(RedisTemplate.java:1037)
    	at com.joycity.jt.redis.services.RedisService.lambda$0(RedisService.java:50)
    	at java.base/java.lang.Thread.run(Thread.java:834)
    Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
    	at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78)
    	at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:56)
    	at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:242)
    	at io.lettuce.core.RedisClient.connect(RedisClient.java:206)
    	at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.lambda$getConnection$1(StandaloneConnectionProvider.java:115)
    	at java.base/java.util.Optional.orElseGet(Optional.java:369)
    	at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.getConnection(StandaloneConnectionProvider.java:115)
    	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1440)
    	... 11 more
    Caused by: java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다
    	at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
    	at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
    	at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:276)
    	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:233)
    	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:223)
    	at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:358)
    	at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253)
    	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1133)
    	at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350)
    	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
    	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
    	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
    	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
    	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    	... 1 more

     

     

    2) Redis Set과 Redis List

     

    요구사항은 단순 명료했다.

     

    • 레디스로 Queue를 구현할 것
    • 배열(혹은 리스트)를 레디스에 입력할 것
    • 배열(혹은 리스트)를 분할하여 불러와서 db에 insert-batch 돌릴 것

    그리고 Redis의 Set과 List 모두 위의 것을 만족시키지 못한다.

     

    (1) Redis List

     

    가장 먼저 알아본 것은 List형태였다. RPush와 LPOP을 이용한다면 선입선출의 Queue를 구현 가능 할 것 같았다. 하지만 그게 아니었다.

     

    stackoverflow.com/questions/34493730/is-there-any-way-to-pop-all-list-items-from-redis-list-at-once

     

    Is there any way to pop all list items from redis list at once?

    I want to pop all list items from redis list at once. I don't want to call lpop or rpop method by when the list is empty because it seems to be inefficient sending requests multi-time to redis-ser...

    stackoverflow.com

    가장 큰 문제점은 가져오는 것이 문제였다. PopRange만 있어도 이 모든 고통에서 해방될 터였다. 하지만 Redis의 Range는 Pop을 하지 않은 채 데이터만 읽어오는 형태였다. 그럼 하다 못해 범위로 데이터를 지울 수라도 있어야 하는데 그런 것 또한 없다.

     

    스택 오버플로우의 답변을 보면 알겠지만, list의 모든 item을 pop하는 방법은... range 후에 delete key를 하는 것이다! 

     

     

    (2) Redis Set

     

    Redis Set은 List형태로 한꺼번에 Push하고 Pop 하는 것이 가능하다. 근데, 문제는 Pop할 경우 Random하게 item을 가져온다는 것이다. (이미 선입선출의 queue가 깨지게 된다.)

     

    게다가 문제는 Set이다! 

     

    이것 때문에 부하테스트에서 데이터가 유실되는 초유의 사태를 겪었다.

     

    가령 Datetime과 ip를 기록하는 히스토리를 redis에 임시 보관한다고 하자. 엄밀히 따지면 ms 단위까지 봐야하지만, 편의상 yyyy-mm-dd HH:mm:ss 형태로 초까지만 구분한다고 하면, 같은 ip로 100TPS로 부하 테스트를 하면 history가 1초당 100건이 쌓이는 것이 아니라 1건만 쌓인다. 데이터가 날아가는 것이다!

     

    문제 원인은 매우 간단하다. "Set"이다. 같은 값은 중복 입력 안 되는 것이다!

     


    고통 끝에 임시 방편으로 Set에 넣을 데이터에 dummy 난수를 부여하여 중복이 되지 않게 했다. 

    그러나 이것은 궁극적인 해결책이 아니라고 생각한다.

     

    우선 Transaction까지 사용하는 가장 큰 이유는 같은 key값에 push와 pop이 일어나기 때문이다. 결국 key를 특정 조건에 따라 분리하여 입력하도록 하고, 입력이 끝난 키 값에 대해서만 불러와서 db에 넣는 로직으로 갈아야 보다 깔끔한 구조가 될 것이다. 

    'Server > Redis' 카테고리의 다른 글

    스프링 부트 + 레디스 설정/ 테스트  (0) 2020.09.29
    CentOS 8.2 Redis 설치  (0) 2020.09.29

    댓글

Designed by Tistory.