pile·
백엔드·마켓컬리마켓컬리 Hello World·

컬리 검색이 카프카를 들여다본 이야기 1

컬리 검색팀이 색인 파이프라인을 개편하며 겪은 카프카 컨슈머 적체와 리밸런싱 문제를 설정 튜닝만으로 해결한 과정을 다룬다. max.poll 설정으로 불필요한 리밸런싱을 막고, 리스너를 Batch로 바꿔 색인 스루풋을 크게 끌어올렸다.

핵심 포인트
  • 색인에 부가 정보가 별도 토픽으로 추가되며 검색엔진 외부에서 여러 메시지를 조합해야 하는 구조로 변경
  • 컨슈머 처리 속도가 발행을 못 따라가 메시지가 적체되고, 컨슈머 그룹에 예기치 못한 리밸런싱이 반복
  • 레디스 지표는 정상이라 병목을 카프카로 좁힘
  • max-poll-records를 1로 줄여 5분 처리 초과로 인한 리밸런싱을 제거
  • 리스너를 Record에서 Batch로 바꿔 poll로 받은 전체 레코드를 한 번에 색인 요청, 스루풋 비약적 향상
상세 정리
  • AS-IS: 단일 토픽 메시지가 색인에 필요한 모든 정보를 담아 간단한 정제만으로 색인이 가능했다.
  • TO-BE: 요구사항 고도화로 부가 정보가 별도 토픽으로 유통돼, 복수 메시지를 하나로 조합해 인덱싱하는 설계가 필요해졌다.
  • 1차 구현: 색인을 이루는 상품정보들을 각각 레디스에 저장하고, 나머지 데이터가 레디스에 있으면 합쳐 색인을 요청하는 방식을 택했다.
  • 문제 발생: 상품 메시지를 일괄 발행하자 컨슈머가 못 따라가 적체됐고, 컨슈머 그룹 리밸런싱까지 겹쳐 색인 반영이 지연돼 정상 검색이 불가했다.
  • 원인 좁히기: 레디스 CPU 등 과부하 정황이 없어 병목을 카프카로 판단했다.
  • 리밸런싱 원인: 컨슈머가 기본 설정상 500개 레코드를 5분 내 처리하도록 돼 있는데, 계속 5분을 넘겨 그룹 내 리밸런싱이 발생하는 것으로 추정했다.
  • 해결 1: max-poll-records를 1로 극단적으로 줄이자 메시지 한 건 처리는 5분을 넘지 않아 리밸런싱 없이 색인을 구축할 수 있었다.
  • 근본 문제: 애초에 500건을 5분 안에 처리하면 리밸런싱이 없었을 것이므로 스루풋 개선이 본질이라 판단했다.
  • 리스너 구조: 기본 Record Listener(RecordMessagingMessageListenerAdapter)는 onMessage가 레코드 1건씩만 받아 처리한다.
  • 해결 2: listener.type을 batch로 바꾸면 BatchMessagingMessageListenerAdapter가 poll로 가져온 레코드 리스트를 한 번에 받는다.
  • 효과: 운영 중인 검색엔진이 Batch 색인 요청을 지원해, 건건이 요청하던 색인을 모아 요청하도록 바꾸자 소비·색인 속도가 비약적으로 올랐고 같은 설정에서도 리밸런싱이 사라졌다.
왜 읽나스프링 카프카 컨슈머에서 리밸런싱과 처리 적체로 고생하는 백엔드 개발자에게 max.poll·Batch Listener 튜닝의 원인 진단과 처방 레퍼런스.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2