pile·
백엔드·channel-talk채널톡·

급증하는 트래픽 안정적으로 처리하기: 개선편(2) 논리적 파티셔닝

앞 글의 동적 스케일링으로도 남아 있던 두 문제, 즉 단일 Queue를 대량 Job이 점유해 다른 요청이 밀리는 HOL Blocking과 인스턴스 단위 스케일링의 비효율을 논리적 파티셔닝과 Coordinator 기반 Partition별 독립 스케일링으로 해결한 기록이다. Redis 인스턴스를 물리적으로 나누는 대신 같은 Redis 안에서 키 접두사만 분리해 독립 Queue를 만들었고, 10만 건 기준 처리 시간을 25분에서 4분으로 84% 줄였다.

핵심 포인트
  • 문제는 Redis의 get/set 성능이 아니라 모든 컴포넌트가 단일 Ready Queue에 의존하는 구조였다. Redis CPU는 5일간 15% 미만으로 여유가 있었다.
  • 인스턴스 단위 스케일링은 Worker가 10개만 더 필요해도 고정 32 Worker짜리 인스턴스를 통째로 추가해 32 → 64 → 96처럼 배수로 점프한다.
  • 파티션 키를 고객사 ID가 아니라 벌크액션 요청 자체로 잡았다. 격리하고 싶은 대상이 특정 고객사가 아니라 특정 대규모 요청이었기 때문이다.
  • Coordinator는 관찰과 결정만 하고 실제 실행은 각 Consumer가 하며, 지시 전달에 별도 RPC나 etcd 대신 이미 있는 Job 파이프라인을 재사용했다.
  • 기존 코드 재사용 덕에 Coordinator를 1,000줄 이내로 구현했다.
상세 정리
  • 파티셔닝을 택한 두 이유: 장기적으로 쉽게 횡적 확장할 수 있는 구조가 필요했고, 동적 스케일링으로 인한 Worker 수 변화의 예측 가능성을 높이기 위해서였다.
  • 복잡도 절충: 파티셔닝은 보통 멤버십 프로토콜이나 리더 선출 같은 분산 조율 메커니즘을 요구하는데, 그만큼의 정밀함이나 안정성이 요구되는 상황은 아니었고 TPS도 머신 한 대가 버티기 충분한 수준이었다.
  • 그래서 Redis 머신 자체를 늘릴 만큼의 트래픽은 아니라고 보고, 추후 대비해 Partition-aware 구조를 확립하고 Consumer를 분산 배치하는 데 집중했다.
  • 파티션 키 후보 1 고객사 단위: 장애 추적이 쉽고 같은 고객사 안에서 우선순위 비교도 자연스럽지만, 대형 고객사의 여러 벌크액션이 한 Partition에 집중되면 같은 Partition의 다른 고객사까지 영향을 받는다.
  • 후보 2 Job 종류별: 특정 유형이 유독 부하를 주는 경우엔 유용하지만, 같은 유형 안에서도 10건짜리와 100만 건짜리의 규모 차이가 커 격리 효과가 제한적이었다.
  • 상위 트래픽 요청을 격리해 더 좋은 머신에 두는 멀티 레벨 전략도 Discord나 X에서 볼 수 있는 일반적 패턴이지만, 관리 복잡도를 줄이기 위해 단순한 쪽을 택했다.
  • 후보 3 채택: 각 벌크액션 요청을 독립적으로 분배하면 고객사 단위보다 세밀해 부하가 고르게 분산되고, 대형 고객사가 여러 요청을 동시에 보내도 서로 다른 Partition으로 흩어진다.
  • 팀의 기본 관행은 고객사 ID 기준 파티셔닝이었다. 권한 제한 단위가 Channel이라 직관적이지만, 벌크액션을 자주 쓰는 고객사와 같은 Partition에 있다는 이유만으로 손해를 보는 구조가 되고 한 Partition으로만 요청이 몰리는 것도 바람직하지 않다고 판단했다.
  • 논리적 파티셔닝의 실체: Redis Cluster로 물리 분리하는 대신 키 접두사에 partition id를 넣어 Partition 수만큼 독립된 큐를 만든다.
  • 각 요청은 파티션 키에 의해 항상 같은 Partition으로 라우팅되고 Partition마다 독립된 Consumer와 Worker 파이프라인을 가진다. 한 요청이 여러 Partition에 흩어지지 않으므로 우선순위 관리와 상태 추적도 자연스럽다.
  • 스케일링 구조 변경: Partition당 Consumer 수를 1개로 고정하고 프로세스 내부의 Worker goroutine 수만 동적으로 조절한다. Worker 수 조절 API가 이미 있어 필요한 만큼만 늘리고 줄일 수 있었고, 인스턴스 수는 변하지 않아 Partition 간 간섭이 없다.
  • Coordinator 역할: 모든 Partition을 주기적으로 순회하며 부하 상태를 독립적으로 관찰하고, 필요하다고 판단되면 해당 Partition에 스케일링 커맨드를 보낸다. Consumer는 평소처럼 폴링하다 커맨드를 받으면 Worker 수를 조절한다.
  • 재시작 대응: 동적 스케일링 중 Consumer가 재시작되면 이전 Worker 수를 알 수 없어 기본값에서 다시 수렴해야 한다. 그래서 Worker 수를 주기적으로 Redis에 checkpoint하고 재시작 시 읽어 복구하며, Partition별로 키가 분리돼 해당 Partition의 checkpoint만 읽으면 된다.
  • 물리 파티셔닝 준비: Keyspace가 이미 완전히 분리돼 Partition 간 cross-key 의존이 없고 각 Consumer가 하나의 Partition만 처리하므로, 나중에 Redis 인스턴스를 나눌 때 연결 설정 한 곳만 바꾸면 되고 비즈니스 로직은 수정이 필요 없다.
  • 배포 전략: 파티셔닝을 켜면 Keyspace가 달라져 기존 Queue에 남은 Job 처리가 과제였는데, 인프라 단 트래픽 라우팅으로 새 요청은 새 Producer로만 보내고 기존 서버는 레거시 Keyspace의 Job을 소진한 뒤 자연스럽게 종료하게 했다. 별도 데이터 마이그레이션이나 Drain용 Consumer 배포가 필요 없었다.
  • Coordinator 설계 영감: Kafka의 KRaft 구조에서 가져왔다. Kafka는 오래 ZooKeeper에 메타데이터 관리를 위임해 운영할 분산 시스템이 둘이 되는 복잡도를 안았는데, KRaft는 이미 가지고 있는 도구를 재사용하자는 발상으로 메타데이터를 내부 토픽에 이벤트로 기록하고 Controller들이 Raft 합의로 복제하게 했다.
  • 같은 발상으로 Coordinator는 각 Partition의 Queue에 스케일링 지시 Job을 발행하고 Consumer는 기존과 동일한 방식으로 폴링해 처리한다. 별도 조율 시스템을 도입하지 않았다.
  • 검증 결과: 10만 건 부하 테스트에서 처리 시간이 25분에서 4분으로 줄었고, 특정 Partition에 대량 Job이 몰려도 다른 Partition 처리에 영향이 없어 HOL Blocking 해소가 확인됐다.
  • 스케일링도 인스턴스 단위 점프 대신 Partition별로 필요한 만큼만 증감하는 것을 확인했고, 4단계 전환은 서비스 중단 없이 완료됐다. 부하 상황에서 개발자가 직접 Worker 수를 조정하던 일도 사라졌다.
  • 남은 한계 둘: Partition 수가 설정 시점에 결정돼 운영 중 변경하려면 재배포가 필요하고, 해시 기반 라우팅이 대체로 균등하지만 극단적으로 큰 요청들이 특정 Partition에 겹치면 부하가 몰릴 수 있다. 후자는 Partition을 더 잘게 나누고 Shuffle Sharding을 도입하는 방향을 고려한다고 밝힌다.
왜 읽나단일 큐를 공유하는 워커 시스템에서 대형 작업이 나머지를 막는 문제를 겪는 팀에게, Redis를 그대로 두고 Keyspace만 나누는 절충과 조율 컴포넌트를 기존 파이프라인으로 구현하는 방법을 준다.
channel-talk
채널톡 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·twilio-engTwilio Engineering·

    Programmable Messaging에서 Verify API로 마이그레이션하기

    Twilio의 Programmable Messaging API로 자체 OTP 솔루션을 운영하던 서비스가 Verify API로 전환하는 방법을 코드 예시와 함께 설명한다. Verify는 OTP 전송·검증을 위한 전용 API로, 전화번호 구매, 토큰 생성, DB 저장·만료 관리를 내부에서 처리해 개발자가 직접 구현할 코드를 크게 줄인다.

    요약 이어보기
    #authentication#twilio#sms+2
  2. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 2부

    포스타입 백엔드 엔지니어가 벡터 기반 개인화 추천 시스템을 실제 운영하며 맞닥뜨린 성능 장애와 용량 문제를 해결한 과정을 담은 2부다. 수백만 개의 벡터 KNN 검색이 피크 시간대에 전체 Elasticsearch 검색 성능을 흔드는 문제부터 클러스터 OOM 사태까지, 쿼리 최적화와 인프라 분리 두 가지 경로로 근본 해결에 이른다.

    요약 이어보기
    #elasticsearch#vector-search#recommendation-system+2
  3. 백엔드·포스타입포스타입·

    포스타입이 개인화 추천을 하는 방법 1부

    포스타입이 태그 기반 추천의 한계를 극복하고 벡터 임베딩 기반 개인화 추천 시스템을 구축한 과정을 담은 1부다. 유사한 콘텐츠가 다른 용어를 쓰거나 동일한 태그가 전혀 다른 톤의 콘텐츠를 가리키는 문제를 임베딩 벡터로 해결하고, OpenSearch의 HNSW ANN 검색으로 수백만 벡터를 실시간 검색하는 시스템을 구축해 구매율 15% 향상을 달성했다.

    요약 이어보기
    #opensearch#vector-search#recommendation-system+2