웹 애플리케이션을 대규모로 운영할 때 백그라운드 작업은 빼놓을 수 없는 핵심 요소입니다. 기본 개념은 단순합니다. 클라이언트가 웹 애플리케이션에 요청을 보내면, 앱은 요청을 처리하는 과정에서 시간이 오래 걸리는 여러 작업을 수행하게 됩니다. 클라이언트에게 더 빠르게 응답하기 위해 앱은 이러한 무거운 작업을 백그라운드 처리 시스템으로 큐잉하고, 연산이나 I/O 작업 같은 헤비 리프팅은 모두 백그라운드 프로세싱이 담당하도록 하는 것이죠. 백그라운드 잡을 효과적으로 활용하는 것은 웹 애플리케이션 확장의 가장 중요한 빌딩 블록 중 하나입니다.
Rails 개발자라면 선택할 수 있는 훌륭한 라이브러리가 여럿 있다는 점에서 운이 좋습니다. 각 라이브러리는 저마다 장단점과 사용하는 백엔드 데이터베이스가 다릅니다. 이런 도구들은 무거운 작업을 손쉽게 외부로 위임해 주기 때문에, 우리는 더 적은 리소스로 더 많은 사용자에게 더 빠른 응답 속도를 제공할 수 있습니다.
최근까지 Honeybadger는 사내 백그라운드 처리 작업의 대부분을 Sidekiq로 수행했습니다. Sidekiq는 폭발적으로 빠른 사용자 경험을 유지하고, 대량으로 유입되는 데이터를 처리하는 견고한 파이프라인을 구축하는 데 큰 역할을 해왔습니다.
그렇다면 Sidekiq의 한계는 무엇이었을까요?
안정적인 Redis에도 한계는 존재합니다. 최근까지 우리는 에러 추적 엔드포인트로 들어오는 데이터 수집(ingest) 작업을 포함해 모든 잡 처리를 Sidekiq로 진행했습니다. 에러 트래픽은 변동성이 큽니다. 큐에 쌓이는 잡 수가 순식간에 10배, 20배로 늘어나기 일쑤였고, 오토스케일링이 충분한 워커를 띄워 따라잡을 때까지 큰 적체가 발생했습니다. 하지만 더 큰 문제는 ElastiCache 클러스터의 메모리 고갈이었습니다.
ElastiCache 클러스터로 흘러가는 잡 트래픽이 상당히 많았기 때문에, 다운스트림 처리가 지연되면 클러스터 메모리가 바닥날 위험이 있었습니다. 큐가 아닌 데이터는 별도 클러스터에 저장하고 있었지만, 시간이 지나면서 비큐(non-queue) 데이터 일부가 기본 클러스터에도 섞여 들어갔고, 메모리 부족(OOM) 상황에서 이 데이터들이 강제로 제거될 수 있었습니다. 그러나 더 치명적인 문제는 클러스터 메모리가 가득 차면 새로운 잡 자체를 받아들일 수 없다는 점이었습니다.
또 하나의 문제는 Redis/ElastiCache가 기본값으로 volatile-lru 제거 정책을 사용한다는 점입니다. 그 결과 메모리 사용량이 높아지면 TTL이 설정된 데이터 중 가장 오랫동안 사용되지 않은 것부터 지우기 시작합니다. 실제로 Honeybadger에서도 메모리 사용량이 너무 높아져 의도치 않게 Redis가 캐시를 비우는 사건이 있었습니다. 다행히 제거된 데이터는 재현 가능한 것(TTL이 설정된 이유)이라 영구 데이터 손실은 없었습니다.
하지만 이 경험은 해결해야 할 질문 하나를 남겼습니다. 비슷한 상황이 발생했는데 이번에는 재구축할 수 없는 데이터가 사라진다면? 고객 데이터를 절대 잃지 않으려면 어떻게 해야 할까요? 고객의 에러 데이터를 다루는 것이 곧 우리 비즈니스의 핵심이기 때문에, 우리에게는 데이터 손실에 탄력적으로 대응할 수 있는 시스템이 필요했습니다.
데이터 수집을 위한 Kafka 도입
Kafka는 확장성과 복원력을 동시에 제공하는 분산형 이벤트 파이프라인입니다. 최근 Insights를 출시하며 이벤트 데이터를 처리하는 인프라로 Kafka를 구축한 경험이 이미 있었기 때문에, 에러 수집 데이터 처리에도 동일한 기술 스택을 활용하고자 했습니다. 목표는 Kafka를 통해 더 나은 확장성과 함께 이중화된 스토리지를 더 저렴한 비용으로 달성하는 것이었습니다.
Insights를 위해 AWS MSK 클러스터를 직접 운영하고 있었기 때문에 인프라와 오토스케일링 설정이 이미 마련되어 있었습니다. 덕분에 몇 가지 토픽만 만들고, 기존 Sidekiq 워커와 동일한 코드를 실행하는 컨슈머 몇 개만 추가하면 됐습니다. 개념이 꽤 단순했기에 남은 시간을 Kafka 컨슈머의 세부 튜닝에 집중할 수 있었습니다.
Sidekiq에서 Karafka로의 마이그레이션
Honeybadger는 '마제스틱 모놀리스(majestic monolith)' 아키텍처로 설계되어 있는데, Karafka는 이 구조를 그대로 유지하는 데 큰 도움이 되었습니다. 이미 Insights 데이터 일부를 Karafka로 처리하고 있었기 때문에, 새로운 컨슈머를 추가하는 일은 어렵지 않았습니다.
Karafka와 Sidekiq의 주요 차이점 중 하나는 잡을 가져오는 방식입니다. Karafka는 잡을 배치(batch)로 묶어 한 번의 컨슈머 실행으로 함께 처리합니다. 컨슈머 안에서는 메시지 배열을 순회하며 기존 Sidekiq 워커 로직을 인라인으로 실행할 수 있습니다:
class NoticeConsumer < ApplicationConsumer
def consume
messages.each do |message|
NoticeWorker.new.perform(message.payload)
end
end
end
고려해야 했던 또 다른 차이는 에러 처리 방식입니다. Sidekiq에서는 각 잡이 원자적으로 처리되기 때문에, 워커가 재시도와 실패 콜백을 통해 스스로 에러를 관리합니다. 반면 Kafka의 배치 특성상 에러를 다룰 수 있는 선택지가 더 많습니다. 특히 Karafka는 Dead Letter Queue(DLQ)라는 메커니즘을 제공해 배치 단위 또는 개별 메시지 단위로 에러 처리를 지정할 수 있습니다.
dead_letter_queue(
topic: "ingestion.errors.dead",
max_retries: 5,
independent: true
)
Karafka 컨슈머가 개별 메시지 처리에 실패하면 최대 5번까지 재처리를 시도합니다. 5번째 시도까지 실패하면 해당 메시지는 지정된 토픽으로 전송됩니다. independent: true 옵션은 배치 전체가 아니라 실패한 메시지만 DLQ로 보내도록 지시하는 역할을 합니다.
Karafka 모니터링과 스케일링
알고 보면 Karafka 컨슈머를 모니터링하고 스케일링하는 일은 꽤 복잡합니다. AWS/MSK와 Karafka 양쪽에서 추적할 수 있는 지표가 많고, 시스템을 조율할 수 있는 설정 값도 다양합니다. 코드가 실제로 무엇을 하고 있는지, 데이터 플로우가 어떻게 동작하는지 세심한 주의가 필요합니다.
AWS CloudWatch로 많은 지표를 모니터링하고 있는데, 그중 Kafka 관련해서 주목하는 지표는 다음과 같습니다:
- SumOffsetLag — 특정 토픽과 컨슈머 그룹에 대해, 모든 파티션의 오프셋 지연(offset lag)을 합산한 값입니다.
- EstimatedMaxTimeLag — 특정 토픽과 컨슈머 그룹에 대해, 모든 파티션을 현재 오프셋까지 따라잡는 데 걸릴 것으로 추정되는 시간입니다.
Karafka 자체도 훌륭한 계측(instrumentation) 기능을 제공하지만, 이 데이터를 직접 퍼블리시하고 저장해야 합니다:
- processing_lag — 소비된 모든 메시지 배치에서 확인할 수 있는 값으로, Karafka가 Kafka로부터 메시지를 가져와 처리를 시작하기까지 걸린 시간을 알려줍니다.
- consumption_lag — processing_lag와 비슷하지만, 배치의 마지막 메시지가 Kafka에 들어온 시점부터 컨슈머가 처리를 시작하기까지의 시간입니다.
- duration — 컨슈머가 배치 전체를 처리하는 데 걸리는 시간입니다.
실제로 Sidekiq 프로세스의 스케일링과 Karafka 컨슈머 프로세스의 스케일링은 상당히 다릅니다. Sidekiq는 Redis 인스턴스가 감당할 수 있는 범위 내에서 프로세스를 자유롭게 늘리면 됩니다. 반면 Kafka에서는 토픽의 파티션당 최대 1개 프로세스라는 제약이 있습니다. 경험칙으로는, Kafka 컨슈머가 여러 파티션에 할당될 수 있으므로 계획보다 파티션 수를 넉넉하게 잡는 것이 좋습니다.
또 하나 기억해야 할 점은 Kafka 컨슈머의 스케일 업·다운이 매우 긴 작업일 수 있다는 것입니다. 컨슈머 그룹에 컨슈머를 추가하거나 제거하려면 그룹이 리밸런싱(rebalancing)을 거쳐야 하며, 이는 필요에 따라 파티션을 재할당하는 과정을 의미합니다. 재할당 중에는 컨슈머가 메시지 처리를 멈춥니다. sticky-cooperative 할당 방식으로 이 문제를 어느 정도 완화할 수 있지만, 가능하다면 리소스를 미리 넉넉히 프로비저닝해 리밸런싱 자체를 피하는 것이 바람직합니다.
현재 우리는 SumOffsetLag를 스케일링 지표 중 하나로 모니터링하고 있습니다. 주의할 점은 리밸런싱 중에는 이 지표가 보고되지 않는다는 것입니다. 그래서 상상할 수 있듯이, 리밸런싱 기간 동안 이 지표는 리밸런싱이 끝날 때까지 급격히 증가합니다. 스케일링을 최소화해야 하는 또 다른 이유입니다.
Honeybadger의 Karafka, 앞으로의 계획은?
Kafka/Karafka 구현체를 100% 가동한 지 한 달이 넘었는데, 솔직히 꽤 만족스럽다고 말할 수 있습니다. 그리고 언제든 버튼 하나로 Sidekiq로 되돌아갈 수 있다는 점도 든든합니다. 덕분에 두 시스템 중 어느 쪽에서든 유지보수 작업이 필요할 때 훨씬 더 큰 복원력을 확보할 수 있습니다.
Sidekiq에서 Karafka로 마이그레이션하는 과정에서 Kafka와 Karafka를 다루는 방법에 대해 많은 것을 배웠습니다. 아직 최신 버전의 Honeybadger 젬(gem)으로 업데이트하지 않았다면 꼭 확인해 보세요! karafka 플러그인에 새로운 기능들을 추가했습니다. Insights가 활성화되어 있으면, 우리 젬이 Kafka 시스템의 전반적인 건강 상태를 한눈에 파악할 수 있도록 중요한 통계를 추적하기 시작합니다.
또한 이 데이터를 시각화하고 Kafka 컨슈머의 동작을 더 잘 이해할 수 있도록 돕는 Insights Karafka 대시보드도 새로 선보였습니다. Karafka 대시보드를 사용하려면 플러그인에서 metrics를 활성화해야 합니다. honeybadger.yml에 다음 설정을 추가하세요:
karafka:
insights:
metrics: true
대시보드의 모습은 다음과 같습니다:

고객들이 이 데이터를 활용해 자신들의 Kafka 시스템을 어떻게 개선해 나가는지 기대가 큽니다. Sidekiq에서 Karafka로의 마이그레이션이나 Kafka 활용에 대해 궁금한 점이 있다면 언제든 문의해 주세요!