Lua를 활용한 Redis 큐: 공유 워커 풀에서 파티션별 순서 보장
고부하 시스템에서는 수십만 개의 키에 걸쳐 논리적 파티션 내에서 메시지 처리 순서를 엄격히 유지해야 합니다. 제한된 워커 풀이 무거운 파티션에 의해 차단되지 않으면서도 부하를 균등하게 분배해야 합니다. 재시도 및 지연 작업 지원은 smart-redis-queue 라이브러리에서 Redis와 Lua 스크립트를 통해 구현됩니다.
이 솔루션은 Lua의 원자성을 활용해 ZSET, HASH, STRING에 대한 작업을 경쟁 조건 없이 수행합니다. 키는 queue:{queueName}: 접두사로 구성됩니다. 순서가 보장되는 파티션(접두사 ! 포함 코드)은 락을 사용해 독점적으로 처리되고, 일반 파티션은 병렬로 처리됩니다.
Kafka 및 RabbitMQ와의 비교
Kafka는 토픽 파티션 내에서 순서를 보장하며 컨슈머 그룹으로 확장되지만, 파티션 할당을 컨슈머에 고정합니다. 준비된 모든 파티션에서 작업을 선택하는 동적 워커 풀은 다른 의미 체계가 필요합니다. Kafka의 TTL은 특정 시간 T까지의 정밀한 스케줄링을 대체하지 못합니다.
RabbitMQ는 TTL과 DLQ를 지원하지만, 다중 컨슈머 환경에서의 엄격한 순서는 라운드 로빈에 의해 깨집니다. 수백만 개의 키에 대한 별도 큐는 확장성이 떨어집니다.
Redis + Lua는 가벼운 대안을 제공합니다: 원자적 작업당 한 번의 왕복으로, 클러스터나 복잡한 모니터링이 필요 없습니다.
키 구조
| 키 | 구조 | 목적 |
|------|-----------|------------|
| queue:{name}:partitions | SET | 활성 파티션 |
| queue:{name}:partition:{code}:{priority} | ZSET | 우선순위별 작업 큐, 점수 = 가용 시간(ms) |
| queue:{name}:partition:{code}:priorities | ZSET | 우선순위 수준 |
| queue:{name}:payload:{taskId} | STRING | 페이로드 |
| queue:{name}:partition:{taskId} | STRING | 파티션 코드 |
| queue:{name}:consumer:{id}:tasks | HASH | 처리 중인 작업 |
| queue:{name}:partition:{code}:lock | STRING | 순서 보장 파티션용 락 |
| queue:{name}:partition:{code}:block | STRING (TTL) | 임시 차단(속도 제한) |
작업 생명주기
- 발행:
SET NX를 사용한 멱등 추가, 점수 = 예약 시간(ms)으로 ZSET에 기록. 파티션은 SET에 추가됩니다.
local ok = redis.call('SET', payloadKey, payload, 'NX')
if ok then
redis.call('ZADD', partitionQueueKey, scheduled, taskId)
redis.call('SADD', partitionsKey, partitionCode)
end
- 가져오기: 하트비트, 파티션 순회, 순서 보장 파티션의 락/차단 확인. 내림차순 우선순위(
ZREVRANGE)로 선택, 예약 시간 <= 현재 시간인 ZSET의 첫 작업. 컨슈머의 HASH로 이동.
- 확인: HASH, 페이로드, 키에서 제거. 순서 보장 파티션 카운터 = 0이면 락 해제.
- 거부: 새로운 점수로 ZSET에 반환. 순서 보장 파티션의 경우 우선순위 +1, tie-break를 위한 reject_seq. 순서 보장 파티션의 첫 작업이 거부되면 해당 파티션의 전체 배치가 거부됩니다.
성능 벤치마크
Go 1.22+, Redis 5, Intel i7-13700H에서 테스트.
발행 (작업/초):
- 1개 작업: 17–23k
- 10개 배치: 80–92k
- 100개 배치: 100–117k
배치 처리는 단일 Lua 호출을 통해 처리량을 증가시킵니다.
단일 워커 소비 (작업/초):
| 프리페치 | ns/op | 작업/초 |
|----------|--------|---------|
| 1 | 80–103 µs | 9.7–12.5k |
| 5 | 57–67 µs | 15–17.5k |
병렬 처리:
- 2개 워커: 28–30k/s
- 8개 워커: 23–26k/s
Redis 경합으로 인해 확장성은 비선형적입니다.
장애 처리
- TTL ~120초의 하트비트.
- 활성 컨슈머가 분산 락을 통해 죽은 컨슈머를 스캔.
- 죽은 컨슈머의 HASH에서 작업을 큐로 반환(순서 보장 파티션은 +우선순위).
속도 제한: RejectWithDelay는 TTL이 있는 차단을 설정합니다.
권장: 거부 시 증가와의 충돌을 피하기 위해 우선순위 단계를 100+로 설정.
핵심 요약
- Lua 원자성은 ZSET과 HASH 간 작업 이동 시 경쟁 조건을 방지합니다.
- 순서 보장 파티션은 독점 락과 거부 로직으로 순서를 유지합니다.
- 발행/가져오기에서 배치 처리는 처리량을 100k+ 작업/초로 향상시킵니다.
- 작업 손실 없이 워커 장애 후 자동 복구.
- 클러스터 오버헤드 없이 수백만 개의 파티션 시나리오에 적합합니다.
— Editorial Team
아직 댓글이 없습니다.