BE-commerce 프로젝트
약 5분 분량 이벤트·메시징성능·용량

CDC가 139배 느리게 나왔다: 느렸던 것은 적지 않은 기본값이었다

목차

사용자 활동 로그를 CDC로 나르게 바꾸고 재 보니 lag p95가 아웃박스 방식의 139배(1,944ms 대 14ms)였습니다. 그대로 적었으면 CDC를 기각했을 것입니다. 원인은 CDC가 아니라 커넥터 설정을 기본값 그대로 둔 것이었고 지연에 관련된 값 셋을 명시하자 13ms로 아웃박스와 같아졌습니다. 그보다 앞선 측정은 반영률 0%에 오류 로그 0건이었는데 이벤트를 읽을 컨슈머가 아예 만들어지지 않은 상태였습니다.

결론부터

  • CDC 기본 설정은 lag p95 1,944ms로 아웃박스 14ms보다 139배 느렸지만, CDC 자체의 한계는 아니었습니다.
  • 지연에 영향을 주는 설정 세 가지를 명시하자 lag p95가 13ms로 내려갔습니다.
  • 반영률 0%였던 첫 측정은 성능 문제가 아니라 컨슈머 빈이 없었던 구성 오류였습니다.

CDC 도입 여부는 이름이나 첫 측정값으로 결정할 수 없습니다. 실제로 이벤트가 소비되는지, 기본값이 어떤 지연을 만드는지, 기존 정합성 요구와 같은 자리에 쓰는지를 따로 확인해야 합니다.

CDC를 어디에 쓸 것인가

CDC(Change Data Capture)는 DB가 남기는 변경 기록(MySQL binlog)을 읽어 메시지로 흘리는 방식입니다. 앱이 직접 메시지 브로커에 발행하지 않아도 DB에 쓴 것이 토픽으로 나갑니다. 이 글은 개인화에 쓰는 사용자 활동 로그를 CDC로 옮기면서 무엇을 기대했고 첫 측정이 왜 틀렸는지를 다룹니다.

측정은 로컬 단일 장비에서 합성 활동으로 했습니다(실 MySQL 8.4, Kafka, Debezium 3.0). 실 트래픽 값이 아닙니다.

느렸던 것은 CDC가 아니라 적지 않은 기본값이었다. 아웃박스 lag p95 14ms, CDC 기본값 1,944ms, 설정 셋을 명시한 CDC 13ms. 아래에는 상태만 보면 정상이던 조용한 실패 둘

왜 CDC를 들여왔나

이 저장소는 결제 이벤트를 트랜잭셔널 아웃박스로 발행합니다. 비즈니스 데이터와 “보낼 이벤트”를 같은 트랜잭션에 쓰고 릴레이가 주기적으로 읽어 브로커에 넣는 방식입니다. 아키텍처 문서는 릴레이 방식으로 폴링과 CDC 두 후보를 놓고 폴링을 골랐다고 적어 두었습니다. 그런데 이번에 CDC(Debezium)를 들였으니 읽는 사람은 그 결정이 뒤집혔는지 묻게 됩니다.

뒤집지 않았습니다. 두 자리는 푸는 문제가 다릅니다.

결제 아웃박스 릴레이활동 로그 전달
나르는 것트랜잭션이 이미 커밋한 사실사용자 활동 원본 행
지키는 것커밋된 것은 반드시 나간다결제·주문 경로의 격리
늦으면정합성 문제개인화 신선도 문제

아웃박스는 정합성 장치이고 폴링으로도 “커밋된 것은 반드시 나간다”가 성립합니다. 활동 로그에서 문제는 유실보다 결합입니다. 앱이 활동을 직접 발행하면 브로커가 흔들릴 때 그 여파가 주문·결제 경로로 들어옵니다. 그래서 결제 아웃박스는 폴링으로 두고 활동 로그만 CDC로 옮겼습니다. 기존 컨슈머가 같은 토픽을 그대로 읽으므로 소비 쪽 코드는 새로 만들지 않았습니다.

옮기면서 지킨 성질이 하나 있습니다. 기존 발행은 메시지 키를 userId로 잡아 같은 사용자의 활동이 같은 파티션에 들어가게 했고 그래서 컨슈머가 사용자별 갱신에 분산 락을 쓰지 않습니다. Debezium은 기본으로 테이블 PK를 키로 씁니다. 그대로 두면 이 전제가 조용히 깨지므로 변환 설정으로 키를 userId로 바꿨습니다. 파티션이 여럿일 때 사용자별 순서가 이 설정 하나에 걸려 있습니다.

측정 전에 적은 예상

“CDC가 아웃박스보다 조금 나쁠 것이다. binlog를 거치는 만큼 한 단계가 더 있다. 차이는 수십 ms 수준이고 대기 100ms를 주면 둘 다 반영률이 높을 것이다.”

같은 토픽을 같은 컨슈머가 읽게 두고 “누가 토픽에 넣는가”만 바꿔 쟀습니다. 도착률 초당 8건, 40초, 3파티션입니다. 반영률은 홈 요청 시점에 직전 활동이 컨텍스트에 들어와 있던 비율이고 waitMs는 요청이 최신 활동을 기다려 주는 상한입니다.

전달waitMs반영률e2e p95lag p95
아웃박스100100.0%39.0ms14ms
CDC (기본값)10019.5%134.0ms1,944ms
CDC (세 값 명시)10098.5%117.0ms13ms
아웃박스·CDC 공통00.0~2.5%23~27ms1,849~1,883ms

예상은 틀렸습니다. 첫 측정에서 CDC는 139배 나빴습니다.

느렸던 것은 CDC가 아니었다

커넥터 설정을 다시 보니 36개 항목 중 지연에 관련된 것을 하나도 적지 않았습니다. Debezium의 기본 poll.interval.ms는 500ms이고 배치가 차기를 기다립니다. 세 값을 명시했습니다.

설정기본값준 값
poll.interval.ms500100
max.batch.size2048256
max.queue.size81921024

lag p95가 1,944ms에서 13ms로, 반영률이 19.5%에서 98.5%로 바뀌었습니다. 이 조건에서는 토픽에 넣는 주체를 앱에서 CDC로 바꾼 것이 신선도를 나쁘게 만들지 않았습니다. CDC로 얻은 것은 결합을 끊은 것이고 신선도는 같은 수준을 유지했습니다.

waitMs=0은 전달 방식과 무관하게 반영률이 0~2.5%였습니다. 기다리지 않으면 소비가 못 따라온다는 점은 앞선 실험에서 찾은 결론(대기 50ms가 무릎)과 같고 전달 방식을 바꿔도 그 성질은 그대로였습니다.

그 전에 반영률 0%인 측정이 있었다

위 표보다 앞선 첫 CDC 측정은 반영률 0.0%였고 그것도 틀린 값이었습니다. 커넥터는 정상이었고 토픽에 메시지 103건이 실제로 쌓였고 앱 로그에 오류가 0건이었습니다. 원인은 컨슈머 쪽에 있었습니다. 활동을 읽는 컨슈머 빈이 “전달 방식이 KAFKA일 때”만 만들어지는 조건이 걸려 있었고 CDC 모드에서는 그 조건이 거짓이라 읽는 쪽이 아예 없었습니다.

같은 작업에서 조용한 실패가 한 번 더 있었습니다. 커넥터 상태는 RUNNING인데 안에서는 MySQL 8.4와 맞지 않아 재시작만 반복하고 있었습니다. 두 경우 모두 상태만 보면 정상이었고 끝단에 실제로 도착한 건수를 세어야 드러났습니다.

대가와 남은 것

  • 운영 부품이 늘었습니다. Debezium Connect를 띄우고 커넥터 설정, 스키마 히스토리 토픽, 복제 클라이언트 id를 관리해야 합니다.
  • binlog를 ROW + FULL로 켜야 합니다. 운영이면 보존 기간과 디스크를 같이 정해야 합니다.
  • DB 스키마가 메시지 계약이 됩니다. 컬럼 이름이 곧 메시지 필드라서 표를 바꾸면 소비자가 깨질 수 있습니다. 아웃박스는 중간에 이벤트 레코드를 두어 이것을 막아 줍니다. 여기서는 필드 이름을 바꾸는 변환 설정이 얇은 완충재 역할을 합니다.
  • poll.interval.ms=100의 비용은 아직 재지 않았습니다. 더 자주 폴링하면 DB와 커넥터 부하가 늘어납니다. 지금 값은 신선도만 보고 골랐습니다.
  • 커넥터가 죽는 것을 감시하는 수단이 아직 없습니다. CDC는 발행 보장의 근거를 아웃박스 테이블에서 binlog와 커넥터의 건강으로 옮깁니다. 이 측정은 커넥터가 살아 있을 때의 값입니다.

이어서 읽기

이 글에서 이어진 작업은 따로 적었습니다.

참고

프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.