
최근에 운영중인 백엔드 서비스의 메세지 큐 제공자를 Redis 에서 Pulsar로 교체한 기념으로 Pulsar에 대한 글을 써볼까 한다.
k8s Stateful 인프라 서비스를 프로덕션 적용해보는건 처음이기 때문에, 6개월 정도 지나고 나면 더 할 말이 많을 것 같다.
프로듀서 – 컨슈머 모델

백엔드 서비스를 운영하다보면 처리 시간이 오래 걸리는 작업들을 구현해야 할 때가 있는데(e.g. 동영상 인코딩), 이럴 때 유용한 패턴이 프로듀서-컨슈머 모델이다.
- 작업의 생산자
- 작업을 쌓아둘 버퍼
- 작업의 처리자(=소비자)
로 역할을 분리하면 생산자와 소비자는 불필요한 대기시간 없이 자신의 역량을 최대한 발휘할 수 있다.
프로듀서 – 컨슈머 문제와 이에 대한 해결법은 1965년 다이스트라 형님의 “Cooperating Sequential Processes” 에서 처음 등장한 역사가 오래된 문제해결 방식이다.(링크: Bounded Buffer)
60년의 세월이 지난 지금도 여전히 유효하다. 구조가 간단하며 다양한 사례에 쉽게 적용이 가능하다는 장점이 있다.
커피숍에서 일하는 초짜 알바생을 떠올려보자.
이들은 초짜이기 때문에 분업을 할 줄 모른다. 즉, 주문을 받은 알바생은 요령이 없어 본인이 직접 주문받은 음료를 만들어야만 한다.
하지만 음료를 만드는 작업은 시간이 걸리는 일이다. 모든 알바생들이 음료를 만드느라 시간이 걸리다보니 주문을 받아줄 알바생이 없다.
때문에 카운터는 음료를 기다리는 손님들과 주문을 넣지 못한 손님들로 북적이게 될 것이다.

베테랑 알바생이 투입되면 이야기가 달라진다.
베테랑 알바생은 “주문 전담” 과 “음료 제조” 로 업무를 나눈다.
주문 전담 알바생이 하는 일은 오직 주문서 포스트잇을 주방으로 전달하는 것이다.
이로써 주문서가 쌓이는 일은 있어도 손님들이 주문을 넣지 못해 줄을 서게 되는 일은 없어진다.
손님이 줄을 서있는 대신, 주문서가 쌓이게 되므로 커피숍의 응답성은 높아지게 된다.

Back-Pressure Control
이러한 프로듀서 – 컨슈머 모델에서 ‘주문서를 쌓아둘 버퍼'(=메세지 큐)를 백엔드 서비스에서 직접 구현할 때 주의해야 할 것들이 몇 가지 있는데, 그 중 하나가 Back-Pressure Control 이다.
Back-Pressure 는 과거 rxjava의 Flowable에 대한 글, [rxJava] Flowable 과 Observable 의 차이 에서 다룬 바 있으며(벌써 7년 전이네?), ReactiveX 같은 다양한 Pub/Sub 유즈케이스를 구현한 라이브러리를 사용한다면 굳이 이런 고민을 할 필요가 없기는 하다.
어쨋든, 아마도 한 번 쯤 Exception Handling을 잘해야 한다는 소리를 들어본 적 있을 것이다.
Exception을 잘 처리해야 하는 이유는 우리가 미처 대비하지 못했던 ‘정의되지 않은 동작’이 언젠가 한 번은 일어나기 때문이다.
Back-Pressure를 제어해야 하는 이유는 우리가 아무리 시스템에 장애가 발생하지 않도록 다양한 대비를 해두었어도 우리가 미처 생각지 못했던 사유로 버퍼가 가득차는 일이 발생하기 때문이다.
Pulsar를 포함한 현대의 메세지 브로커 서비스는 Back-Pressure Control 기능 정도는 기본 제공한다. 하지만 Redis로 직접 MQ를 구현한다면 어플리케이션 레이어에서 직접 Back-Pressure 제어를 고려한 개발을 해야 할 것이다.
ReactiveX같은 라이브러리를 사용하여도 좋고, 단순한 유즈케이스를 원한다면 직접 만들어도 무방하다.
Race Condition & Starvation

Race Condition 과 Starvation 은 “리소스의 효율적인 배분”에 대한 이야기다. MQ의 목적이 자원의 작업을 효율적으로 배분하여 최적의 처리를 하는 것이기 때문에, Producer와 Consumer를 적절히 배치하고 Race Condition이나 Starvation이 발생하지 않도록 관리하는 것은 중요하다.

일반적인 해결방식은 위 그림처럼 Worker들에게 Job을 분배하는 Scheduler를 두는 것이다.
Message Queue로부터 Job을 받아오는 Scheduler가 Consumer에게 일감을 뿌려주므로 Consumer끼리의 불필요한 경쟁이나, Job을 얻지 못해 낭비되는 리소스가 발생하지 않게 된다.
Round Robin 스케줄러 역시 구현이 간단하기 때문에 사용사례가 복잡하지 않다면 직접 구현하여도 무방하나,
Celery(링크: https://docs.celeryq.dev/en/stable/)와 같은 오픈소스를 사용한다면 많은 고민 요소를 대신 해결해줄 수 있다.
Redis Message Queue

우리 팀에서 운영중인 백엔드 서비스에 처음 Producer Consumer 모델을 도입하였던 이유는 작업의 처리시간이 꽤 길었기 때문이다.
현재는 처리방식이 많이 개선되어 요청 1 건을 처리하기까지 대략 1~2초 내외에서 해결이 되지만,
서비스 초기에는 짧게는 수 초 ~ 길게는 수십 초 가량이 걸렸다.
이렇게 오랜 시간이 소요되는 CPU Bound & IO Bound Job을 비동기적으로 처리하기 위한 몇 가지 선택지가 있었는데,
Redis 를 Message Queue로 채택하기까지 다음을 고려하였다.
- 소규모 팀이므로 인프라 관리 코스트를 최소화할 것
- 유즈케이스가 단순하므로 Producer/Consumer 구조를 어플리케이션 레이어에서 직접 구현하여도 무방
- k8s의 ScaleOut Metric으로 사용할 지표를 쉽게 구성할 수 있을 것
Redis MQ 는 훌륭히 제 역할을 수행해 주었다.
우리 서비스는 조직 내외적인 이슈로 추가적인 신규 인프라를 구성, 운영하기 쉽지 않은 환경이었던지라
이미 Cache로 사용중인 Redis에 새로운 역할을 부여하는 것은 상대적으로 쉽고 편한 결정이었다.
또한 CNCF의 graduated 프로젝트인 Event Driven Autoscaler – Keda 를 사용하면 손쉽게 Redis의 Queue Length를 ScaleOut Metric으로 적용할 수 있다.
링크
Keda: https://keda.sh/
Redis List Scaler: https://keda.sh/docs/2.13/scalers/redis-lists/
이렇게 잘 버텨주던 레디스에서 점점 문제가 생기기 시작하는데…
Redis to Pulsar
SPOF(Single Point Of Failure)
작년도에 발생한 여러 건의 장애는 레디스의 지분이 꽤 컸다.
인프라가 노후화되었다던가, 디스크 full로 인한 IO 병목이라던가, 최대 커넥션 한도를 초과하였다던가…
단순히 응답성을 향상시키는 목적의 캐시 스토리지 레디스는 문제가 생겨도 문제상황이 전파되지 않는다.
허나 MQ의 인프라로 레디스를 사용하면 더이상 워커들에게 태스크를 전달할 수단이 막히게 되어
레디스 장애 = 서비스 장애가 된다.
레디스 클러스터를 구성, Availability를 높이는 방법도 고려할 수 있었지만
- 구현부의 수정이 불가피하고
- 인프라 구성의 변경은 외부 조직의 양해와 협력이 요구되는데다
- 현 구성(Redis-Sentinel)으로도 장애 상황을 막을 수 없었기 때문에
우리는 레디스 대신 다른 MQ 서비스를 도입하여야겠다는 판단을 한다.
Pulsar Benchmark

Redis 대신 MQ 후보로 검토하였던 서비스는 RabbitMQ, Kafka, Pulsar 이렇게 세가지였다.
모두 업계에서 충분히 검증된 훌륭한 Message Broker 이지만 우리 서비스에선 다음의 기준에 따라 Pulsar를 선택했다.
- 큰 사이즈(4~5MB)의 메세지를 안정적으로 처리할 수 있는가
- k8s 클러스터 상에서 프로덕션 레벨의 운영이 가능한가
- 작은 팀에서 운영이 가능할 정도로 운영 장벽이 낮은가
- 추후 서비스를 확장하였을 때 다양한 요구사항 및 아키텍처 패턴 수용이 가능한가
Pulsar는 운영 장벽이 셋 중에서 가장 낮은데, 이유는 다음과 같다.
- Pulsar는 구조적으로 Stateless Layer(=Broker) 와 Persistent Layer(=Bookie)로 나눠져 있다.
- Broker 와 Bookie 의 책임이 명확히 분리되어 있기 때문에 운영도 명확히 분리시킬 수 있다.
- 이 말인즉, 스토리지의 교체, 업그레이드, 장애 등의 문제가 Message Broker에게 전파되지 않는다는 의미이다.
- 따라서 클러스터의 확장, 업그레이드, (스토리지)교체 등의 운영이 kafka나 RabbitMQ와 비교하였을 때 상대적으로 쉽다.
- 우리같이 작은 규모의 팀은 helm chart로 운영하여도 손색이 없을 정도다.
2편에서 계속…
다음 글에는 Pulsar의 아키텍처, 특징, 제공하는 기능 등을 Pub/Sub 모델과 함께 소개하겠다.





댓글 남기기