CDC 작업을 수행할 때 Flink CDC 라이브러리를 사용하면 얻게되는 장점은 parallelism을 2이상으로 높일 수 있다는 것입니다.
반면 Debezium 기반 Kafka Connect는 단일 태스크로만 동작하기 때문에 병렬성(tasks.max)가 1로 고정되는 단점이 존재합니다.
그런데 문득 Flink CDC도 내부적으로 Debezium을 사용하는데 왜 Flink CDC는 병렬 소싱이 가능하고, Kafka Connect는 불가능한지 구조적인 차이가 궁금해 조사해보았습니다.
결론부터 말하면, 차이의 핵심은 Debezium 자체가 아니라 그 위에 올라가는 실행 프레임워크(Kafka Connect vs Flink)의 아키텍처에 있습니다.
Debezium 기반 Kafka Connect - 병렬성이 1인 이유
Kafka Connect의 태스크 모델
Kafka Connect는 Connector와 Task라는 두 가지 개념으로 동작합니다. Connector가 설정을 관리하고, 실제 데이터를 읽는 건 Task입니다. 일반적인 Source Connector(예: JDBC Source)는 tasks.max를 높여서 여러 태스크를 병렬로 실행할 수 있습니다.
그런데 Debezium의 CDC 커넥터는 tasks.max=1로 고정됩니다. MySQL, PostgreSQL, SQL Server 할 것 없이 모두 단일 태스크입니다.
단일 태스크일 수밖에 없는 이유
이유는 CDC의 근본적인 메커니즘에 있습니다.
| DB | CDC 메커니즘 | 제약 |
|---|---|---|
| MySQL | Binlog | 하나의 binlog 스트림은 순차적이며, 읽기 위치(offset)가 단일 |
| PostgreSQL | Replication Slot | 하나의 슬롯은 동시에 하나의 컨슈머만 사용 가능 |
| SQL Server | CT(Change Tracking) | 변경 캡처 인스턴스가 단일 읽기 지점을 유지 |
The MySQL connector always uses a single task and therefore does not use this value. — Confluent Documentation
핵심은 데이터베이스의 변경 로그(binlog, WAL 등)는 본질적으로 단일 스트림이라는 것입니다. 하나의 파일에 순서대로 기록되는 로그를 여러 태스크가 나눠 읽으면, 이벤트 순서가 보장되지 않습니다. Debezium은 이 순서 보장을 위해 단일 태스크를 강제합니다.
스냅샷 단계도 마찬가지
"변경 로그는 단일 스트림이니까 그렇다 치고, 스냅샷은 병렬로 할 수 있지 않을까?"라고 생각할 수 있습니다.
하지만 Debezium의 Kafka Connect 구현에서는 스냅샷과 binlog 읽기가 하나의 태스크 안에서 순차적으로 이루어집니다. 스냅샷 완료 후 binlog 읽기로 전환하는 과정에서 정확한 오프셋을 넘겨야 하기 때문에, 이 둘을 분리하기 어려운 구조입니다.
Debezium에서 snapshot.max.threads 설정을 통해 여러 테이블에 대한 병렬 스냅샷은 지원하지만, 하나의 테이블을 여러 태스크로 쪼개서 읽는 것은 불가능합니다.
Flink CDC: 같은 Debezium인데 어떻게 병렬로?
Flink CDC와 Debezium의 관계
Flink CDC는 Debezium을 라이브러리(Embedded Engine) 형태로 사용합니다.
Embedded Engine이란 Debezium이 제공하는 별도 API로 Kafka Connect 없이, 아무 Java 애플리케이션 안에서 Debezium의 CDC 기능만 꺼내 쓸 수 있는 옵션을 의미합니다.
따라서 Flink CDC의 내부 코드를 보면 단순히 Debezium 의존성을 추가한 것이 아니라, binlog 추출 같은 Debezium의 CDC 핵심 로직을 추출해 Flink 코드 위에 올린 형태로 이루어져 있습니다. 참고
FLIP-27 Source API: 병렬 읽기의 기반
Flink CDC 2.0부터는 Flink의 FLIP-27 Source API (FLINK-10740)를 기반으로 동작합니다. 이 API의 핵심은 두 가지 역할의 분리입니다:
| 역할 | 구현 클래스 | 하는 일 |
|---|---|---|
| SplitEnumerator | MySqlSourceEnumerator |
테이블을 chunk로 분할하고, 각 SourceReader에 할당 |
| SourceReader | MySqlSourceReader |
할당받은 chunk를 실제로 읽음 (병렬 실행) |
SplitEnumerator는 JobManager에서 실행되고, SourceReader는 TaskManager에서 parallelism 수만큼 실행됩니다. 이 분리 덕분에 "무엇을 읽을지 결정하는 것"과 "실제로 읽는 것"을 독립적으로 스케일링할 수 있습니다.
실제로 MySqlSource.java의 클래스 Javadoc을 보면, 이 세 가지 특성이 명시되어 있습니다:
The MySQL CDC Source based on FLIP-27 and Watermark Signal Algorithm which supports parallel reading snapshot of table and then continue to capture data change from binlog.
- The source supports parallel capturing table change.
- The source supports checkpoint in split level when read snapshot data.
- The source doesn't need apply any lock of MySQL.
이번에 조사하면서 알게된 사실인데 스냅샷 과정에서 chunk 단위로 체크포인트를 찍는다고 하는건 조금 새로웠습니다.
Incremental Snapshot: 테이블을 Chunk로 쪼개기
Flink CDC는 라이브러리 버전 2.0부터 Incremental Snapshot 알고리즘을 통해 병렬 소싱을 지원하게 되었습니다.
원리는 이전에 제 블로그 글에서 언급한 Netflix의 DBLog 논문 기반의 알고리즘인데요, 다음과 같이 동작합니다.
1단계: Chunk 분할
ChunkSplitter가 테이블의 Chunk Key 범위를 기준으로 데이터를 여러 chunk로 나눕니다. (Chunk Key는 PK가 될수도 있고, 따로 지정할 수도 있습니다)
이 인터페이스의 핵심 메서드는 아래과 같습니다.
/** Generates all snapshot splits (chunks) for the give data collection. */
Collection<SnapshotSplit> generateSplits(TableId tableId) throws Exception;
MySQL의 경우 MySqlChunkSplitter가 이를 구현합니다.
예시: users 테이블 (PK: id, 1000만 건)
Chunk 1: id [1, 100000)
Chunk 2: id [100000, 200000)
Chunk 3: id [200000, 300000)
...
Chunk 100: id [9900000, 10000000]
chunk 크기는 scan.incremental.snapshot.chunk.size (기본 8,096행)로 설정할 수 있습니다. PK가 auto increment인 경우 evenly로 판단되어 균등 분할이 되며, 그렇지 않은 경우엔 unevenly로 판단되어 자체적인 분할 방식으로 처리합니다. (unevenly인 경우 성능 차이가 꽤나 존재하여 조심해야 합니다)
2단계: 병렬 읽기
SnapshotSplitAssigner가 생성된 chunk들을 여러 SourceReader에 분배합니다. 내부적으로 비동기 스레드(snapshot-splitting)에서 테이블을 chunk로 분할하고, getNext() 호출 시 remainingSplits에서 하나씩 꺼내 각 SourceReader에 할당합니다. parallelism=4로 설정했다면, 4개의 SourceReader가 각각 다른 chunk를 동시에 읽습니다.
// 개념적 구조 (실제 코드 단순화)
// SourceReader 1: SELECT * FROM users WHERE id >= 1 AND id < 100000
// SourceReader 2: SELECT * FROM users WHERE id >= 100000 AND id < 200000
// SourceReader 3: SELECT * FROM users WHERE id >= 200000 AND id < 300000
// SourceReader 4: SELECT * FROM users WHERE id >= 300000 AND id < 400000
3단계: Binlog 전환
모든 chunk의 스냅샷이 완료되면, MySqlHybridSplitAssigner가 내부의 SnapshotSplitAssigner에서 BinlogSplitAssigner로 전환됩니다. 이후의 변경 이벤트는 단일 binlog reader가 처리합니다. 여기서 Debezium의 binlog 읽기 코드가 그대로 활용됩니다.
Watermark 기반 일관성 보장 (Lock-Free)
여기서 궁금한 점은 "스냅샷을 읽는 동안 데이터가 변경되면 어떡하지?" 라는 생각이 들 수 있습니다.
Flink CDC는 Watermark 알고리즘으로 이 문제를 해결합니다. 각 chunk를 읽을 때 다음 과정을 거칩니다.
- chunk 읽기 시작 전: 현재 binlog 위치를 Low Watermark로 기록
- chunk의
SELECT쿼리 실행 (스냅샷 데이터 읽기) - chunk 읽기 완료 후: 현재 binlog 위치를 High Watermark로 기록
- Low~High Watermark 사이의 binlog 이벤트 중 이 chunk 범위에 해당하는 변경을 추후에 보정(back-fill)
이 방식 덕분에 MySQL의 Global Read Lock이 필요 없습니다. Debezium의 기존 스냅샷 방식은 데이터 일관성을 위해 이 글로벌 락을 사용했는데, 이는 아무래도 운영 DB에 부하가 갈 수 있습니다.
위에서 언급했던 것처럼 Apache Flink CDC docs 중 scan.incremental.snapshot.enabled 부분에 해당 내용이 명확히 나와있습니다.
Compared to the old snapshot mechanism, the incremental snapshot has many advantages, including: (1) MySQL CDC Source can be parallel during snapshot reading (2) MySQL CDC Source can perform checkpoints in the chunk granularity during snapshot reading (3) MySQL CDC Source doesn't need to acquire global read lock before snapshot reading.
구조 비교: 한눈에 보기
| 항목 | Debezium + Kafka Connect | Flink CDC |
|---|---|---|
| Debezium 사용 방식 | Kafka Connect Source Connector | Embedded Engine (라이브러리) |
| 실행 프레임워크 | Kafka Connect | Apache Flink |
| 스냅샷 병렬성 | 단일 태스크 (테이블 간 병렬만 가능) | Chunk 기반 다중 SourceReader |
| 스냅샷 단위 | 테이블 전체 | Chunk (PK 범위 기반) |
| 글로벌 락 필요 | 필요 (FLUSH TABLES WITH READ LOCK) | 불필요 (Watermark 알고리즘) |
| 체크포인트 단위 | 테이블 단위 | Chunk 단위 |
| 장애 복구 | 처음부터 다시 스냅샷 | 마지막 미완료 chunk부터 재개 |
| 변경 이벤트 읽기 | 단일 태스크 | 단일 Binlog Reader (스냅샷 완료 후) |
그렇다면 Binlog Stream 단계는?
Flink CDC의 병렬성은 스냅샷 단계에서만 효과적입니다. 스냅샷이 끝나고 binlog stream로 전환되면, 단일 Binlog Reader가 변경 이벤트를 처리하기 때문에 Debezium과 동일하게 Parallelism이 1로 설정됩니다.
마무리
정리하면, Flink CDC가 Debezium 기반 Kafka Connect에 비해 스냅샷 단계에서 병렬성이 높은 이유는 Debezium 자체의 차이가 아니라, 실행 프레임워크의 아키텍처 차이입니다.
- Kafka Connect는 CDC 커넥터를 단일 태스크로 실행하며, 이는 binlog의 순차적 특성 때문에 불가피한 설계입니다.
- Flink CDC는 Source API의 SplitEnumerator/SourceReader 분리 모델을 활용하여, 스냅샷 단계를 chunk 단위로 분할하고 병렬 실행합니다. Debezium은 binlog 읽기와 데이터 변환에만 활용합니다.
'오픈소스' 카테고리의 다른 글
| LSM Tree in Flink (0) | 2026.03.28 |
|---|---|
| CASCADE DELETE, Debezium은 알고 있을까? (0) | 2026.03.21 |
| Flink CDC MySQL Snapshot은 정말 중복 없이 동작할까? (0) | 2026.03.01 |
| [flink-cdc] Iceberg sink connector에서의 default value 지원 (0) | 2026.02.14 |
| [flink-cdc] VARIANT 타입과 PARSE_JSON 함수 (0) | 2026.01.31 |