Redis는 빠르다면서요

Minseok Jeon·2023년 4월 14일
3
post-thumbnail

배경)
Redis를 사용해 요청 횟수를 제한해 보자 의 포스팅을 보고 Rate limiting 기능을 구현하고 테스트까지 맞췄습니다. 라이브에 반영하고 문제 없이 잘 사용하던중 application 에서 응답 지연이 발생했습니다. "Redis를 통해 요청 횟수를 카운팅 하고 일정량 이상이면 429 상태 코드를 응답하는데 어떻게 응답 지연이 있을 수 있지?" 해당 기능은 Redis incr command의 결과에 따른 판단(429 or pass)이 로직의 전부였기 때문에 Redis의 성능 문제다! 라고 섣불리 판단 해버리고 더 많은 Connection을 이용해 Redis 서버와 통신하기 위해 Connection Pool을 찾아보고 적용했습니다. 그리고 다시 테스트. 결과는 동일. "Redis TPS가 10만이라고 하던데 내껀 왜이러지?" 라는 의문으로 Redis 성능을 의심하고 테스트를 시작했습니다.

결론)
내용이 아주아주 많이 길어질 거 같아 '배경-내용-결론' 순서를 지키지 않고 결론부터 먼저 말하면 Redis 성능은 Network Latency, Redis 서버의 구성 상태등을 고려해서 이야기해야 하지만 intel cpu 2 core, 1.5GB 메모리 network latency가 5ms 라고 했을 때 Lettuce library를 사용해 single client약 30,000 r/s(request/second) 정도의 성능을 가집니다.

정확하게 "TPS 30,000 정도야" 라고 말하기는 환경과 설정, 구현 방식이 제각기 다르기 때문에 딱 어떤 숫자로 정량화가 어렵지만 이렇게 전달하는 목적은 "의심할 여지 없이 빠르다" 를 전달하기 위함입니다. 'Redis 빠르데~' 를 확실하게 이해하려면 아래 내용까지 꼭 읽어주세요!!

내용)
배경에서 언급했던 내용중에 잘못된 부분을 먼저 찾아보겠습니다. 과도한 요청을 막기 위해 Redis로 Rate Limit 기능을 구현했는데 응답지연!! 여기서 생각을 Rate Limit 기능의 성능이 안나왔구나 -> Redis에 incr 명령밖에 없는데? -> Redis 성능이 별론데? 이렇게 되어 Redis를 무작정 파보기 시작했지만 여기서부터 문제 파악이 잘못됐습니다. 정확한 원인을 파악하기 위해 먼저 HTTP 요청이 처리되는 과정을 다시 정리했습니다.

대략적으로만 나타내도 응답 지연이 될 수 있는 원인이 3가지로 보여집니다.
1. Servlet Container의 I/O 처리량
2. HTTP 요청으로 실행되는 내부 Servlet의 여러 조건, 계산 관련 성능 문제
3. 외부 I/O(Redis, DB 등)

성능의 원인이 될 수 있는 부분이 여러곳이기 때문에 3번(Redis랑 통신하는 부분)을 정확하게 확인하기 위해서는 나머지 조건이 일정하게 될 수 있게 제한해야합니다.

1번 조건은 I/O를 처리하는 Thread를 한개만 동작할 수 있게끔 통제했습니다.(spring boot 에서는 yml 설정에서 쉽게 Servlet Container의 max thread 수를 설정할 수 있습니다)
2번 조건은 Redis의 incr 명령어를 제외한 나머지 로직은 제거했습니다.(log 포함. Slf4j를 사용한 로그 사용 여부에 따라 성능이 2,000TPS 정도 차이 납니다)

이렇게 제한된 조건하에 Lettuce Client 사용해 테스트를 진행했습니다.

테스트는 Apache JMeter 통해 진행했으며, 테스트를 진행한 PC의 사양은 아래와 같습니다.

  • 프로세서 : 12th Gen Intel(R) Core(TM) i7-12700 2.10 GHz
  • 설치된 RAM : 32.0GB(31.7GB 사용 가능)
  • 시스템 종류 : 64비트 운영 체제, x64 기반 프로세서

테스트를 진행하는 PC와 Redis를 설치한 서버간 network latency 6ms 입니다. 어떻게 측정했는지는 뒤에서 자세하게 설명하겠습니다.

아래는 4가지 테스트를 진행한 결과입니다.(Lettuce를 이용해 redis command를 사용하는 예제 코드는 이미 많이 있기 때문에 생략하겠습니다.)

  1. 단일 클라이언트 SyncCommand를 사용할때(168.4/sec)

  2. 200개 connection pool을 SyncCommand로 사용할때(246.8/sec)

  3. 단일 클라이언트 AsyncCommand를 사용할때(205.7/sec)

  4. 200개 connection pool을 AsyncCommand로 사용할때(202.6/sec)

분명 나름엔 connection pool도 사용하고 Async한 처리도 했는데 왜 처리량은 다 비슷한 걸까요? Lettuce client 사용했으니 내부 코드를 확인해보겠습니다.
Lettuce 는 Batcher를 통해 명령을 큐에 넣고 StatefulConnection 클래스에 dispatch 메소드를 통해 그 명령을 처리합니다.

먼저, SyncCommands와 AsyncCommands의 성능이 비슷한 이유는 RedisClient 객체에 connect() 메소드를 통해 StatefulRedisConnection 를 리턴받아 sync(), async() 메소드로 commands 객체를 가져와 다른거 같지만, StatefulRedisConnection 의 구현체 StatefulRedisConnectionImpl의 생성자를 확인해보면 사용되는 command 형태가 같은걸 확인 할 수 있습니다.

AsyncCommand 를 사용하고 syncHandler를 통해 sync 방식으로 리턴

결국 Lettuce 내부적으로 처리되는 속도는 SyncCommand나 AsyncCommand나 같을 수 밖에 없습니다. (물론 syncHandler 에서 처리하는 시간이 조금 더 들긴 하겠지만 무시할 수 있을 만큼 빠릅니다)

두번째로, connection pool 을 사용할때와 single connection 을 사용할때 성능이 거의 비슷한 이유에 대해 알아보겠습니다. Connection pool의 사용 목적은 command를 보내고(Request) command에 대한 응답(Response)을 받을 때 발생하는 network latency를 줄이기 위해 미리 여러개의 connection 을 만들어 두고 한번에 여러 요청을 전달해 하나의 network latency 시간 동안 여러 command를 처리하기 위함이었을 것입니다. 하지만, Redis에서는 Pipeline이란 기능을 통해 command를 보내고(Request) command에 대한 응답(Response)을 받는 즉, RTT(Request/Response protocols and round-trip time) 시간을 개선할 수 있도록 합니다. 때문에 Lettuce library에서는 기본적으로 이 파이프 라인 수를 Integer Max개 만큼으로 설정해 single client도 여러개의 command를 redis 서버에 한번에 전달하고 응답을 받습니다. 때문에 하나의 connection만으로도 무수히 많은 command를 빠르게 redis 서버로 요청하고 그에 대한 응답을 받을 수 있습니다.

SyncCommand, AsyncCommand, Single connection, Multiple Connection(Connection pool) 은 모두 비슷한 성능을 가진다고 확인을 했으니 이제 Redis 사용 방법을 고정 시키고 제한했던 다른 조건을 변경시켜보겠습니다. 2번 조건은 redis 이외의 다른 로직이 들어가면 복잡해지니 그대로 두고, 1번 조건을 변경해보겠습니다. Servlet Container 의 IO를 핸들링하는 Thread 갯수는 application.yml(application.properties)에서 쉽게 변경할 수 있습니다.

단일 클라이언트 SyncCommand를 사용할때만, 다시 성능 확인을 해보겠습니다.(위에서 4가지 방법 모두 성능이 비슷함을 확인)

약 초당 27,000 개를 처리합니다. 즉, 앞선 성능 측정에서 tps가 안나왔던 이유는 redis 의 처리량 문제가 아니라 Servlet container(Tomcat)의 처리량이 적어서 였던 것이었습니다.

자 그러면 이제 현재 구성된 Redis 스펙에서 최대 성능을 알아보겠습니다. 테스트 PC - Redis(AWS 환경)간 처리량을 확인은 redis-benchmark 명령어를 통해 알 수 있습니다. 아래는 redis-benchmark 명령을 통해 확인한 결과입니다. 총 1,000,000개의 incr 명령어를 테스트 했습니다.

Latency 부분을 확인해보면 절반의 데이터가 4.9ms 가 소요 됐고, 남은 데이터의 25%는 6.8ms 가 소요 됐습니다. 이 결과로 대략적으로 6ms 정도의 Network Latency 있음을 확인할 수 있습니다.

1,000,000개의 request가 28.80초가 걸려 완료 됐으므로 tps는 약 34,000 정도라 짐작할 수 있습니다. 해당 테스트는 다른 어플리케이션을 이용하지 않고 redis server 와 직접 통신하는 테스트 였으니, Spring Framework 내에서 Lettuce(redis client)를 사용했을때는 아무래도 내부 어플리케이션에서 동작하기 때문에 조금의 성능 손해를 감안해야합니다.

계속 Servlet Container 의 thread 수를 늘려 테스트를 해봐도 결과는 비슷합니다. JMC(Java Mission Control), VisualVM을 사용해서 보면 요청이 많을 수록 thread 수가 많아 지는것 확인할 수 있지만 그 수가 계속해서 늘어나진 않습니다.

마지막으로, I/O thread를 좀 더 효율적으로 사용하기 위해 non blocking하게 동작하는 webflux framework를 사용하고 redis command 도 reactive commands로 사용해 보겠습니다.

위에 결과처럼 redis-benchmark 로 얻은 결과와 거의 근접한 처리량을 보여줬습니다.

여태까지의 내용을 다시 정리해보면 다음과 같습니다.

  1. Redis의 성능은 의심할 여지 없이 빠르다.
  2. 성능을 의심하는 구간이 있다면 의심하는 구간을 제외한 나머지 구간은 통제해야 정확히 알 수 있다.(Servlet Container의 request 처리량, 내부 business logic에서 외부 I/O 등에 따른 성능 문제인데 Redis를 오해)

참고 자료)

profile
개발 천재 밍코천

0개의 댓글