Debezium을 Rust로 다시 만들기보다 Embedded Engine을 최적화한 경험

지난 몇 년 동안, 우리는 커뮤니티, 포럼, 그리고 고객들로부터 같은 질문을 자주 들어왔습니다.
“당신들의 시스템이 Rust로 작성되었다면, 왜 Debezium을 Rust로 다시 작성하지 않았나요?”
RisingWave는 실시간 이벤트 스트리밍 플랫폼으로, 실시간 이벤트 데이터를 가장 간단하고 비용 효율적인 방식으로 처리·분석·관리할 수 있도록 설계되었습니다. 또한 Apache Iceberg™ 오픈 테이블 포맷을 기본 지원합니다. Postgres와 호환되는 SQL 인터페이스와 DataFrame 스타일의 Python 인터페이스를 모두 제공합니다.
RisingWave는 초당 수백만 건의 이벤트를 수집하고, 실시간 스트림과 과거 데이터를 지속적으로 조인·분석하며, 저지연으로 애드혹 쿼리를 처리하고, 최신의 일관된 결과를 Apache Iceberg™ 또는 다른 다운스트림 시스템에 저장할 수 있습니다.
실제로 이런 시도를 한 오픈 소스 프로젝트들이 있었습니다. 어떤 프로젝트들은 Rust로 Change Data Capture (CDC) 엔진을 처음부터 구현하려 했고, 다른 프로젝트들은 Postgres WAL이나 MySQL binlog를 직접 파싱하려고 시도했습니다. 3년 전, 우리가 CDC 모듈을 설계하기 시작했을 때 이 아이디어를 진지하게 고려했고 여러 번의 실험을 진행했습니다. 그러나 여러 차례의 시도(그리고 실패) 끝에, 우리는 결국 Debezium을 Rust로 다시 작성하지 않기로 결정했습니다. 대신 Debezium Embedded Engine 위에 구축하고 깊이 있는 커스터마이징을 하기로 선택했습니다.
그 이유는 다음과 같습니다.
왜 Rust로 다시 작성하지 않았는가
먼저, Rust가 성능, 안정성, 동시성 제어 측면에서 부인할 수 없는 장점을 제공한다는 점을 인정해야 합니다. 우리가 자체적인 핵심 컴퓨트 및 스토리지 엔진을 Rust로 작성한 이유도 바로 그것입니다. 그러나 CDC는 생태계 의존성이 매우 높은 영역이며, Debezium은 이미 사실상 업계 표준입니다. Postgres, MySQL, SQL Server, 혹은 덜 알려진 데이터베이스까지도 Debezium은 이미 성숙한 커넥터, 잘 검증된 로그 파서, 그리고 활발한 커뮤니티를 보유하고 있습니다.
만약 우리가 이를 Rust로 다시 작성한다면, 다음과 같은 일이 벌어집니다:
프로토콜 파싱을 처음부터 구현해야 함
각 데이터베이스의 로그 포맷은 모두 다릅니다. Postgres WAL과 MySQL binlog는 고유한 구조, 필드 인코딩, 예외 케이스를 가지고 있어 광범위한 적응과 디버깅이 필요합니다. Debezium이 이미 해결한 모든 파싱 문제들을 우리 스스로 다시 해결해야 합니다.매우 긴 호환성 테스트 주기
데이터베이스 버전 차이, 설정, 문자셋, 타임존, DDL 동작—이 모든 것이 CDC 파싱 정확도에 영향을 미칩니다. Debezium 커뮤니티는 거의 매일 버그 수정 PR을 제출합니다. 우리가 별도의 Rust 버전을 유지한다면, 장기적인 유지보수 비용을 전적으로 우리가 떠안아야 합니다.데이터베이스 기능 업데이트에 뒤처짐
데이터베이스는 매년 새로운 기능을 추가합니다. 예를 들어 Postgres의 논리적 디코딩 플러그인 업데이트나 MySQL GTID 동작 변경 등이 있습니다. Debezium 커뮤니티는 이러한 변화를 적극적으로 모니터링하고 빠르게 대응합니다. 우리가 이를 다시 작성한다면, 이런 변화만 따라가기 위해 전담 팀을 운영해야 합니다.
따라서 설령 우리가 이론적으로 Debezium보다 성능을 더 끌어낼 수 있다고 해도, 이를 포기하는 것은 쉽게 10년에 걸친 엔지니어링 프로젝트에 뛰어드는 것과 같습니다. RisingWave의 핵심 기능을 빠르게 반복 개발해야 하는 지금 단계에서, 이는 결코 좋은 선택이 아닙니다.
왜 Debezium Server가 아닌가
우리가 다시 작성하지 않는다면, 왜 Debezium Server 대신 Embedded Engine을 선택했을까요?
주요 차이는 다음과 같습니다:
Debezium Server는 독립 실행 프로세스입니다. 설정 파일을 통해 소스 데이터베이스와 다운스트림 타겟(Kafka, Pulsar 등)을 구성하면, 그것이 계속 실행됩니다. 즉, 일반적인 CDC 포워딩 도구라고 생각하면 됩니다.
Embedded Engine은 Java 라이브러리로, 여러분이 직접 애플리케이션 프로세스에 포함시킵니다. API를 통해 CDC 이벤트를 받고, 이를 어떻게 소비, 캐싱, 전달, 오류 복구할지를 직접 제어할 수 있습니다.
우리가 Embedded Engine을 선택한 세 가지 주요 이유는 다음과 같습니다:
세밀한 제어 가능성
Server 모드는 블랙박스와 같아서 데이터 처리, 메모리 관리, 백프레셔, 오류 복구에 대한 기본 동작을 수정하려면 트릭이 필요합니다. Embedded 모드에서는 우리가 코드로 직접 엔진을 구동하기 때문에 원하는 어느 지점이든 개입할 수 있습니다.컴퓨트 엔진과의 깊은 통합
RisingWave의 컴퓨트 노드는 자체적인 스케줄링, 체크포인트, DAG 토폴로지를 가지고 있습니다. CDC 데이터는 이러한 메커니즘과 긴밀히 통합되어야 합니다. Embedded 모드에서는 CDC 진입 지점에서 바로 로직을 삽입할 수 있지만, Server 모드에서는 불가능합니다.운영 단순성
Server 모드를 사용한다면 별도의 Debezium 프로세스 클러스터를 배포, 모니터링, 업그레이드해야 하며, 장애 조치와 설정 관리도 담당해야 합니다. Embedded 모드는 CDC를 동일한 배포 내에서 실행하기 때문에 모니터링, 로깅, 알림 시스템을 그대로 공유할 수 있습니다.
우리가 Debezium Embedded Engine에 적용한 핵심 업그레이드
Debezium을 그대로 사용하는 것은 아닙니다. 우리의 실시간 컴퓨트 엔진에 완벽히 맞추기 위해, Server 모드에서는 불가능한 방식으로 Embedded Engine을 깊게 커스터마이징했습니다.
우리는 Debezium Embedded Engine을 커스터마이징하여 RisingWave의 스트리밍 인제션을 강화했으며, Apache DataFusion 을 사용해 Apache Iceberg 로 데이터를 싱크할 때 컴팩션을 수행합니다. 자세한 내용은 여기에서 확인할 수 있습니다.
1. 락 없는 스냅샷 + 병렬 증분 스티칭
Debezium의 기본 스냅샷은 테이블을 잠글 수 있어 소스 DB의 운영에 영향을 미칩니다. 우리의 접근 방식은 먼저 스냅샷 경계(high-watermark LSN 또는 binlog 위치)를 기록한 후, 스냅샷과 WAL/binlog 이벤트를 동시에 스트리밍하는 것입니다. 다운스트림에서 중복 제거와 덮어쓰기를 수행하여 긴 락 없이도 일관성을 보장하고 훨씬 빠르게 실시간 동기화를 달성합니다.
2. 병렬 백필(Backfill)
Debezium의 기본 초기 스냅샷은 단일 테이블을 단일 스레드로 처리하기 때문에 느릴 수 있습니다. 2.2 버전부터 snapshot.max.threads 설정을 통해 여러 테이블을 병렬로 스냅샷할 수 있지만, 단일 대규모 테이블의 백필 속도는 여전히 개선되지 않았습니다. 우리는 대규모 테이블을 기본 키 범위로 분할하고 여러 스레드를 이용해 병렬로 백필을 수행했습니다. 또한 소스 DB 부하에 따라 스레드 수를 동적으로 조정하여 성능을 크게 높이는 동시에 과도한 부하를 피했습니다.
3. 크로스 클라우드 스키마 히스토리
Debezium의 기본 스키마 히스토리 저장소는 Kafka, 로컬 파일, S3, Azure Blob 등을 지원하지만, 우리가 필요한 모든 스토리지를 커버하지는 못합니다(예: GCS, Alibaba OSS 미지원). RisingWave에서는 S3, GCS, Azure Blob, OSS 등을 지원하는 플러그형 스키마 히스토리 계층을 구현했습니다. 이를 통해 멀티 클라우드 배포 시에도 이식성과 일관된 스키마 히스토리를 보장합니다.
4. 스키마 히스토리의 메모리 및 백프레셔 제어
Debezium의 기본 스키마 히스토리는 전체 파일을 한 번에 메모리에 로드합니다. 시간이 지나고 DDL 변경이 잦아지면 메모리 사용량이 과도하게 늘어나거나 심한 경우 OOM이 발생할 수 있습니다.
우리는 히스토리를 시간 또는 크기 기반 세그먼트로 분할해, 현재 작업에 필요한 부분만 로드하도록 변경했습니다. 이를 통해 메모리 피크를 줄이고 대규모 장기 실행 멀티 클라우드 환경에서도 안정성을 높였습니다.
5. TOAST 컬럼 값 보정
PostgreSQL에서 비-TOAST 컬럼을 업데이트할 때, TOAST 컬럼이 변경되지 않았다면 Debezium은 그 TOAST 컬럼에 대해 전체 값 대신 플레이스홀더(__debezium_unavailable_value)를 보냅니다. 우리의 CDC 인제션 지점에서는 과거 값을 조회해 이 플레이스홀더를 실제 데이터로 대체하고 새로운 이벤트와 병합합니다. 이를 통해 다운스트림 시스템은 항상 완전하고 사용 가능한 레코드를 받게 됩니다.
6. PostgreSQL 스키마 변경 감지
기본 Debezium은 스키마 진화 처리가 제한적입니다. 특히 Embedded Engine 모드에서는 주기적으로 메타데이터를 폴링해 컬럼 추가/삭제를 감지하는 방식에 의존하는데, 이는 지연이 크고 업스트림 DB에 부하를 줍니다.
우리는 PostgreSQL 복제 프로토콜의 R(Relation) 메시지를 활용해 스키마 변경을 직접 감지합니다. R 메시지가 논리적 복제 스트림에 나타나면 해당 테이블의 스키마가 변경되었음을 의미합니다(예: ADD COLUMN 또는 DROP COLUMN). RisingWave는 이를 수신하자마자 스키마 업데이트 로직을 즉시 실행하여 컬럼 추가/삭제를 자동으로 완료하고 업데이트된 스키마 메타데이터를 다운스트림에 동기화합니다. 이로써 수동 개입이 필요 없고 데이터 불일치 위험이 크게 줄어듭니다.
이러한 수정은 우리가 Embedded Engine 모드로 실행하기 때문에 가능한 것입니다. Server 모드에서는 이러한 중요한 지점에서 데이터를 가로채고 변환할 방법이 거의 없습니다.
결론
Debezium을 Rust로 다시 작성하는 것은 흥미롭게 들릴 수 있지만, 실제로는 생태계 성숙도, 안정성, 버전 호환성, 테스트 깊이를 단기간에 따라잡을 수 있는 것이 아닙니다. 우리가 필요한 것은 RisingWave를 위한 안정적이고 제어 가능하며 깊이 통합된 CDC 진입 지점이지, 수년간의 재작성 프로젝트가 아닙니다.
Debezium Embedded Engine을 선택함으로써 우리는 성숙한 생태계 위에서 우리의 워크로드에 가장 중요한 부분을 최적화할 수 있었습니다. 이는 엔지니어링 효율성, 안정성, 유지보수성 사이에서 현재 우리가 취할 수 있는 최선의 균형입니다.
