본문으로 건너뛰기

단일 파이프를 통한 WaterDrop 성능 50% 향상: 파일 디스크립터 기반 폴링 도입기

One Thread to Poll Them All: How a Single Pipe Made WaterDrop 50% Faster

작성자
발행일
2026년 02월 19일
https://mensfeld.pl/2026/02/waterdrop-fd-polling-50-percent-faster/

핵심 요약

  • 1 기존의 프로듀서별 개별 백그라운드 스레드 폴링 방식에서 발생하는 GVL 경합과 메모리 낭비 문제를 해결하기 위해 파일 디스크립터 기반의 통합 폴링 엔진으로 전환하였습니다.
  • 2 librdkafka의 전용 API와 OS 파이프를 활용하여 이벤트 발생 시에만 깨어나는 엣지 트리거 방식을 도입함으로써 불필요한 CPU 자원 소모를 방지하고 처리 효율을 극대화했습니다.
  • 3 벤치마크 결과 기존 스레드 모드 대비 약 50%의 성능 향상을 기록했으며 단일 폴러 스레드가 여러 프로듀서를 효율적으로 관리하여 대규모 배포 환경에서의 안정성을 높였습니다.

도입

Kafka 메시지 전송 라이브러리인 WaterDrop은 기존에 각 프로듀서 인스턴스마다 독립적인 백그라운드 스레드를 생성하여 이벤트를 폴링했습니다. 하지만 수백 개의 프로듀서가 실행되는 대규모 환경에서는 이러한 방식이 Ruby의 GVL(Global VM Lock) 경합을 유발하고 과도한 메모리를 점유하는 병목 현상을 초래했습니다. 본 글에서는 이러한 구조적 한계를 극복하기 위해 파일 디스크립터(FD)를 활용한 새로운 폴링 메커니즘을 도입하여 성능을 획기적으로 개선한 과정을 상세히 설명합니다.

기존 스레드 기반 폴링의 한계

WaterDrop 프로듀서가 생성될 때마다 rdkafka-ruby는 내부적으로 rd_kafka_poll을 반복 호출하는 백그라운드 스레드를 실행합니다. 이 방식은 다음과 같은 문제점을 안고 있습니다. - 자원 낭비: 각 스레드는 약 1MB의 스택 공간을 차지하며 프로듀서 수가 늘어날수록 메모리 사용량이 급증합니다. 25개의 프로듀서를 사용하면 25MB 이상의 메모리가 폴링 스레드에만 할당됩니다. - GVL 경합: rd_kafka_poll은 C 확장 함수 호출 시 GVL을 해제하고 반환 시 다시 획득하는 과정을 반복합니다. 수십 개의 스레드가 이 과정을 짧은 주기로 반복하면 Ruby 애플리케이션의 메인 로직과 경합이 발생하여 전체적인 성능이 저하됩니다. - 비효율적 대기: 이벤트가 없는 상황에서도 스레드들은 타임아웃까지 대기하며 불필요한 컨텍스트 스위칭을 유발합니다. 이는 특히 처리량이 적은 환경에서도 시스템 자원을 지속적으로 소모하게 만듭니다.

파일 디스크립터(FD)와 파이프를 활용한 혁신

이러한 문제를 해결하기 위해 librdkafka의 rd_kafka_queue_io_event_enable API를 도입했습니다. 이 API는 특정 큐에 이벤트가 발생할 때 파일 디스크립터로 신호를 보낼 수 있게 해줍니다. - 작동 원리: OS 파이프를 생성하여 쓰기 끝단을 librdkafka에 전달합니다. 이벤트 큐에 데이터가 쌓이면 librdkafka가 파이프에 1바이트를 기록하여 신호를 보냅니다. 이는 엣지 트리거 방식으로 작동하여 큐가 비어 있다가 채워질 때만 신호가 발생합니다. - IO.select 활용: Ruby의 IO.select를 사용하여 단일 스레드에서 여러 파이프를 동시에 모니터링합니다. 이는 시스템 레벨에서 효율적으로 관리되며 데이터가 있을 때만 스레드가 깨어나므로 CPU 사용률을 최적화할 수 있습니다. - Non-blocking Poll: 데이터 감지 시 poll_nb(0)를 호출합니다. 이 함수는 GVL을 해제하지 않고 실행되므로 기존 방식보다 약 1.6배 빠른 실행 속도를 보여줍니다. 10만 번의 반복 테스트에서 rd_kafka_poll은 초당 5.1M 호출을 처리한 반면 poll_nb는 8.1M 호출을 처리했습니다.

성능 벤치마크 결과

로컬 Kafka 브로커 환경에서 실시한 테스트 결과는 다음과 같습니다. - 단일 프로듀서: 초당 메시지 처리량이 27,300개에서 41,900개로 약 54% 증가했습니다. - 다중 프로듀서 (25개): 초당 메시지 처리량이 24,140개에서 36,110개로 약 50% 증가했습니다. 이러한 성능 향상은 즉각적인 이벤트 알림 GVL 오버헤드 제거 그리고 스레드 통합에 따른 경합 감소라는 세 가지 요소가 결합된 결과입니다. 특히 프로듀서 수가 많아질수록 단일 폴러 스레드의 효율성이 더욱 빛을 발합니다.

주의사항 및 고급 설정

FD 모드 사용 시 몇 가지 고려해야 할 점이 있습니다. - 콜백 실행 환경: 모든 프로듀서의 콜백이 단일 폴러 스레드에서 실행되므로 콜백 내부에서 시간이 오래 걸리는 작업을 수행하면 다른 프로듀서의 폴링이 지연될 수 있습니다. message.acknowledged나 statistics.emitted 콜백은 최대한 가볍게 유지해야 합니다. - 전용 폴러(Dedicated Poller): 특정 프로듀서의 콜백 부하가 크다면 config.polling.poller 설정을 통해 독립적인 폴러 스레드를 할당하여 격리할 수 있습니다. 이를 통해 중요도가 높은 프로듀서의 응답성을 보장할 수 있습니다. - 점진적 도입 계획: WaterDrop 2.8에서는 선택 사항(Opt-in)으로 시작하여 2.9에서는 기본값으로 설정하고 2.10에서는 기존 스레드 모드를 완전히 제거할 예정입니다. 이는 대규모 운영 환경에서의 안정적인 전환을 위함입니다.

결론

WaterDrop의 FD 기반 폴링 도입은 단순한 성능 향상을 넘어 Ruby 애플리케이션의 자원 효율성을 극대화하는 중요한 이정표가 되었습니다. 현재는 선택 사항으로 제공되지만 향후 버전에서는 기본 동작 방식으로 채택되어 모든 사용자에게 혜택이 돌아갈 예정입니다. 이러한 최적화 기법은 향후 Karafka 컨슈머 측면에도 적용될 계획이며 이는 Ruby 생태계 내에서 고성능 데이터 스트리밍 처리를 구현하는 데 있어 핵심적인 역할을 할 것으로 기대됩니다.

댓글0

댓글 작성

댓글 삭제 시 비밀번호가 필요합니다.

이미 계정이 있으신가요? 로그인 후 댓글을 작성하세요.

0/1000
정중하고 건설적인 댓글을 작성해 주세요.