
💡 조회 성능 개선을 위한 Redis와 Redis Sentinel 적용기
이전 프로젝트를 진행하면서, 조회 성능 개선을 위해 Redis 캐싱을 사용해 본 적이 있었고, 큰 성능 향상을 체감하였다. 하지만, 당시 마감기한에 의해 시간적 여유가 부족했기 때문에, 이번 기회에 Redis에 대해서 더욱 자세히 알아보고, 추가적으로 Redis sentinel을 통한 장애 대응 및 모니터링 시스템도 구현해볼 생각이다.
실시간 채팅 메시지인 만큼 Outbox에 많은 Event들이 쌓일 것이고, ChatMessage와 Outbox 중에서 ChatMessage는 메시지 만의 고유 특성을 유지하기 위해서, Outbox에 idempotency Key(멱등성 키)를 넣어서, 조회를 통해 중복 메시지 발생을 유지한다.

Outbox 개수: 50
Number of Threads (users) : 3000
Ramp-up period (seconds): 10
Loop Count: 5

Average(평균 응답시간): 794ms(0.8초)
Throughput(처리량): 1262req/sec
위 기준으로 Jmeter를 통해 조회 성능을 체크해본 결과 위 와 같은 결과가 발생하였다. 대규모 트래픽을 예상해서, 스레스 수를 3000으로 잡다보니, 처리량은 괜팒지만 평균 응답속도가 0.8초 만큼의 측정되며 버벅임을 피할 수 없었다.
이로 인해, 대용량 트래픽이 예상되므로, Redis 캐싱을 사용한 조회 성능과 비교해보려고 한다.
우리 프로젝트는 MSA 기반 서비스일 뿐 아니라, 여러 서버에서 Redis를 사용하고, Redis Sentinel을 통해 모니터링과 장애 대응을 진행할 것이기 때문에 특성상, Global Cache를 사용햇다.
Redis Cache를 적용하기 전에, 생각해야하는 고려사항이 존재했다. 단순한 접근이 아니라, 운영적인 측면에서도 고민을 했다.
우선,Redis는 Docker에 띄워 외부DB와 비슷한 형태로 사용될 것이기 때문에 Redis와의 통신 중 네트워크 장애나 다른 장애들에 의해서 행위가 유실될 수 있다는 문제가 존재한다. 이는 결국 "원자성"을 위협하는 문제로 이어진다.
따라서, 단순히 Redis를 이용해서 캐싱을 적용하는 것 뿐 아니라, 운영적인 측면에서 장애를 대처해야 하며, 원자성을 유지하는 방식은 2가지가 존재한다.
1) Lua Script를 사용해서 다양한 Redis 명령어를 하나의 스크립트로 묶어 하나의 >트랜잭션 처리로 원자성을 보존
2) Redis Cache에서 1차적으로 원자성 처리(ex. AOF, RDB) + Outbox 테이블의 >Unique 처리를 통해 2차적으로 원자성 처리
위 방식 중, 우리 서비스의 실시간 채팅에서는 outbox에 저장되는 이벤트의 멱등성 키 중복 처리만 필요했기 때문에 1번을 사용하게 되었다.
실제로 Redis Cahce의 원자성 처리를 고려하여 여러가지 방식이 존재하였다.
내가 고민했던 방식은 3가지 였고, 현재 Redis Sentinel을 통해 failover 처리가 되어있으므로 해당 사항도 고려 사항에 포함시켜서 고민하였다.
1) 모든 노드(Master,Slave) AOF 적용,
2) Master에만 AOF(AOF‑only) 적용
3) AOF+RDB 혼용(Hybrid) 방식
음.. 하지만 찾아보고 공부할수록, AOF나 RDB를 통해서 따로 백업해둔다는게 결국 실시간 채팅 서비스에서 단순 멱등성 키 중복조회를 위한 캐시로 사용할 것이 때문에 너무 오버엔지니어링이라고 생각했다. 하지만 더욱 깊이 바라볼 수 있는 시간이였고, 향후 필요에 의해 다시 적용해볼 생각이다.
현재 프로젝트에서는 MSA 기반 서비스를 구성하고 있기 때문에, Redis와 Redis Sentinel 모두 Docker Compose를 통해 서버를 띄워서 사용했다.
# Redis & Redis Sentinel 설정
redis-master:
image: redis:latest
command: redis-server
container_name: "redis-master"
networks:
- infra
redis-slave-1:
image: redis:latest
command: redis-server --slaveof redis-master 6379
links:
- redis-master
container_name: "redis-slave1"
networks:
- infra
redis-slave-2:
image: redis:latest
command: redis-server --slaveof redis-master 6379
links:
- redis-master
container_name: "redis-slave2"
networks:
- infra
sentinel-1:
build: sentinel
ports:
- "5003:26379"
env_file:
- .env
depends_on:
- redis-master
- redis-slave-1
- redis-slave-2
container_name: "sentinel1"
networks:
- infra
sentinel-2:
build: sentinel
ports:
- "5004:26379"
env_file:
- .env
depends_on:
- redis-master
- redis-slave-1
- redis-slave-2
container_name: "sentinel2"
networks:
- infra
sentinel-3:
build: sentinel
ports:
- "5005:26379"
env_file:
- .env
depends_on:
- redis-master
- redis-slave-1
- redis-slave-2
container_name: "sentinel3"
networks:
- infra
docker-compose.yml파일에 위 설정을 추가해서 진행하였으며, Redis의 장애 사항 대응을 위해 Master 1개, Slave 2개로 설정하여 장애 상항을 대비하였다. ( 기존 Master가 장애로 down 되면, 투표에 걸쳐 Slave 1개가 Master로 승격 )
추가적으로 Sentinel은 홀수 개(예: 3개)로 구성하여, 과반수 투표(Quorum) 기준인 2를 만족시킬 수 있도록 설계하였다.

Dockerfile 설정에서는 Sentinel 실행에 필요한 sentinel.conf파일을 이동시키고, 권한 부여 및 entrypoint 파일도 실행시킨다.
entrypoint 명령어를 통해 Sentinel이 빌드되고, 컨테이너 동작후 entrypoint 파일이 실행된다.

entrypoint 파일에서는 Sentinel 설정을 적용 및 실행

Sentinel 실행에 필요한 설정 파일이다. 핵심 부분은 sentinel resolve-hostnames yes를 설정해줘야 sentinel monitor 명령어에서 redis-master를 찾을 수 있다. (설정하지 않으면.. 연결 안됨🥲)

# redis-master 실행
$ docker exec -it redis-master redis-cli -p 6379
# replication 설정 확인
$ info replication
Master 노드를 통해 replication 정보를 조회해보면, Slave 노드로 설정한 2개의 주소가 조회되는 것을 보아 잘 적용된 것으로 보인다.
추가적으로, master-failover_state: no-failover 설정을 통해 sentinel을 통해 잘 모니터링 되고 있음을 의미한다.
사진을 통해 보이는 것 처럼, Redis Sentinel도 총 3개 설정되어있고, Master 노드와 Slave 갯수를 잘 가르키고 있어 잘 설정된 것을 확인할 수 있다.장애 사항시 Redis Sentinel이 잘 모니터링하고 있고 장애를 즉시 감지하는지 / failover를 처리하여 서비스 중단 없이 Slave를 Master로 승격 시키는지를 확인하기 위해서, 실행 중인 Master 노드를 중단하고 확인해 볼 생각이다.


위의 중단 전/후를 비교해보면, Master 노드인 redis-master가 중단되는 순간 failover 처리를 하는 것을 볼 수 있다
- Sentinel이 모니터링 중, redis-Master의 장애를 판별 (주관적 장애 감지)
- 다른 Sentinel들도 모니터링 후, 투표하여 장애 정확히 판별 (객관적 장애 감지)
- Failover 시작 (Slave 중 1개 Master 로 승격)
- 승격 대상 Slave 선택
- 선택된 Slave를 Master 노드로 전환 완료
위 과정을 통해, 중단 없이 Slave가 Master로 승격되어 중단없이 진행되는 것을 확인하였고, Sentinel이 빠르게 장애를 판단하여 처리하는 것을 확인할 수 있었다.



위를 비교해보면, 실제 Master 노드인 redis-master가 중단되고 나면, 투표를 거쳐 redis-slave1과 redis-slave2 중에 하나가 Master 노드로 승격하는 것을 확인 할 수 있다.
또한, redis-master도 다시 재 실행하면, Slave로 편입되어 지는 것을 볼 수 있다.(다시 Master로 돌아가지는 X)
이로써 Docker-compose를 통해 Redis와 Redis Sentinel을 모두 띄워서, 설정 확인과 failover 처리까지 모두 완성되었다. (+ 향후 Sentinel에서 문제 발생시 알림 보내주는 기능 추가 예정)