변경 데이터 캡처
고친 사람 github-actions[bot]
변경 데이터 캡처는 데이터베이스에 생긴 변경을 한 건씩 알아내 다른 시스템으로 넘기는 일입니다. 전체를 다시 읽어 견주는 대신 바뀐 것만 건넵니다. 검색 색인이나 분석용 저장소처럼 원본을 따라가야 하는 곳이 이 흐름을 받아 씁니다.
쉽고 빠른 이해
데이터베이스에서 방금 바뀐 것만 뽑아 다른 시스템에 넘기는 일입니다. 「customers 표 1번 행의 이름이 Anne Marie 로 바뀌었다」가 넘어가는 한 건입니다.
이게 없으면 원본을 전부 다시 읽어 견주거나, 애플리케이션이 두 곳에 각각 써야 합니다. 앞쪽은 원본이 지는 부담이 큽니다. 뒤쪽은 한쪽만 성공했을 때 어긋납니다.
- 원본에서 바뀐 것을 알아냅니다. 데이터베이스가 남겨 둔 변경 기록을 읽는 방법이 그중 하나입니다
- 변경을 한 건씩, 어디까지 넘겼는지 표시와 함께 내보냅니다
- 받는 쪽이 같은 변경을 자기 저장소에 적용합니다. 검색 색인이나 분석용 저장소가 그 받는 쪽입니다
대가는 받는 쪽이 늘 조금 뒤처진다는 것입니다. 쓰자마자 같은 값을 읽어야 하는 곳에는 안 맞습니다. 장애 뒤에는 같은 변경이 두 번 올 수 있어서 받는 쪽이 그것을 견뎌야 합니다.
상세
창고에 무엇이 남았는지 알고 싶을 때, 문을 열고 선반을 처음부터 세는 방법이 있습니다. 물건이 들어오고 나갈 때마다 쪽지를 한 장씩 붙여 두고 그 쪽지만 모아 보는 방법도 있습니다. 변경 데이터 캡처는 뒤쪽입니다.
변경 데이터 캡처는 줄여서 CDC(Change Data Capture)라고도 부릅니다. 영어 이름을 우리말로 옮긴 것입니다.
데이터베이스에서는 행을 넣고 고치고 지우는 일이 쉬지 않고 일어납니다. 변경 데이터 캡처는 그 한 건 한 건을 알아내 바깥으로 넘깁니다. 넘겨받은 쪽은 그것만 보고 같은 변경을 자기 저장소에 다시 해낼 수 있어야 합니다. 주문 표에서 1번 주문의 상태가 결제 대기에서 결제됨으로 바뀌면, 「1번 주문의 상태가 결제 대기에서 결제됨으로 바뀌었다」는 한 건이 검색 색인으로 넘어가는 식입니다.
변경이 처음 일어나는 데이터베이스가 원본입니다. 넘겨받은 변경을 자기 저장소에 적용하는 쪽은 받는 쪽입니다. 검색 색인 · 분석용 저장소 · 캐시처럼 원본을 따라가야 하는 저장소가 받는 쪽에 섭니다.
둘 사이에는 캡처기가 섭니다. 원본에서 변경을 알아내 한 건씩 내보내는 프로그램입니다. 어디까지 넘겼는지 기억해 두는 것도 캡처기의 일입니다.
flowchart TD
A["애플리케이션"] -->|쓴다| B["원본 데이터베이스"]
B --> C["캡처기"]
C --> D["검색 색인"]
C --> E["분석용 저장소"]
C --> F["캐시"]
그림에서 애플리케이션과 받는 쪽 사이에는 선이 없습니다. 애플리케이션은 원본에만 씁니다. 받는 쪽이 늘거나 줄어도 애플리케이션 코드는 손대지 않습니다.
데이터베이스는 자기가 한 변경을 순서대로 파일에 적어 둡니다. 장애가 나면 이 파일을 되짚어 끊긴 지점부터 되살립니다. 복제본에 같은 변경을 보낼 때도 이 파일을 씁니다. 이 파일을 변경 기록이라고 부릅니다.
캡처기가 변경을 알아내는 방법은 여럿입니다. 표를 질의로 주기마다 훑을 수 있습니다. 표가 바뀔 때마다 저절로 도는 트리거로 변경을 따로 적어 두게 할 수도 있습니다. 변경 기록을 읽을 수도 있습니다.
변경 한 건의 다섯 칸
넘어가는 한 건에는 받는 쪽이 같은 일을 다시 해낼 만큼의 정보가 실려야 합니다. 받는 쪽은 원본을 들여다볼 수 없습니다. 건네받은 한 건만 봅니다.
칸 하나가 빠지면 그만큼 다시 해낼 수 없는 것이 생깁니다.
| 칸 | 담는 것 | 빠지면 |
|---|---|---|
| 표와 키 | 어느 표의 어느 행인가 | 무엇을 고쳐야 할지 모릅니다 |
| 연산 | 넣었나 고쳤나 지웠나 | 무슨 일이 있었는지 모릅니다 |
| 바뀐 뒤 값 | 그 행이 지금 어떤 값인가 | 적용할 값이 없습니다 |
| 바뀌기 전 값 | 그 행이 직전에 어떤 값이었나 | 무엇이 달라졌는지 못 가립니다 |
| 위치 | 원본의 변경 순서에서 어디쯤인가 | 앞뒤 순서를 못 맞춥니다. 멈췄다 다시 뜰 때 어디부터 이을지 모릅니다 |
바뀌기 전 값은 늘 다 실리지는 않습니다. 원본이 그 값을 어디까지 남기느냐는 설정이 정합니다. 많이 남길수록 기록이 커집니다.
변경 한 건에는 행의 값이 실려 나갑니다. 개인 정보가 든 열도 걸러 내지 않으면 받는 쪽까지 따라갑니다. 어느 열을 내보내지 않을지는 붙이기 전에 정해 둡니다.
배경
검색 색인이나 분석용 저장소는 원본 데이터베이스를 따라가야 합니다. 가장 단순한 방법은 원본을 전부 다시 읽어 받는 쪽과 견주는 것입니다. 자주 돌릴수록 원본이 지는 부담이 커집니다. 간격을 벌리면 그만큼 받는 쪽이 뒤처집니다.
애플리케이션이 쓸 때마다 두 곳에 각각 쓰는 방법도 있습니다. 이것을 이중 쓰기라고 부릅니다. 한쪽만 성공하면 둘이 어긋난 채로 남습니다. 어긋났다는 것을 알려 주는 쪽은 아무 데도 없습니다.
그래서 쓰는 곳은 원본 하나로 두는 방법이 필요했습니다. 나머지 저장소는 바뀐 것만 받아 따라갑니다. 필요한 것은 받는 쪽의 표가 원본과 모양이 달라도 자기 모양에 맞춰 적용할 수 있는 변경의 흐름이었습니다. 분석용 저장소 가운데 운영 데이터베이스를 늘 따라가야 하는 것이 데이터 웨어하우스입니다. 이런 흐름을 제품 안에 갖춘 데이터 웨어하우스가 많았습니다. 바뀐 데이터를 잡아낸다는 뜻에서 이 일을 변경 데이터 캡처라고 부릅니다.
예시
변경 한 건이 실물로 어떻게 생겼는지 제품 넷에서 봅니다. 볼 것은 상세의 다섯 칸이 제품마다 어느 이름의 칸에 실리느냐입니다.
PostgreSQL 논리 디코딩의 출력
PostgreSQL 은 변경 기록을 WAL(Write-Ahead Log, 미리 쓰기 로그)에 적습니다. 논리 디코딩은 WAL 에
적힌 변경을 데이터베이스 내부 사정을 몰라도 읽을 수 있는 꼴로 풀어 내보내는 기능입니다. 쓰려면
먼저 wal_level 을 logical 로 둡니다.
변경은 복제 슬롯에서 꺼냅니다. 복제 슬롯은 원본에서 일어난 순서대로 변경을 다시 내줄 수 있는 흐름
하나입니다. 슬롯에서 꺼내는 꼴은 출력 플러그인이 정합니다. WAL 안의 내부 표현을 받는 쪽이 원하는
형식으로 바꿔 주는 것이 출력 플러그인입니다. 아래는 test_decoding 이라는 출력 플러그인으로 연
슬롯에서, 한 트랜잭션 안에 행 둘을 넣은 뒤 변경을 꺼낸 결과입니다.
postgres=# BEGIN;
postgres=*# INSERT INTO data(data) VALUES('1');
postgres=*# INSERT INTO data(data) VALUES('2');
postgres=*# COMMIT;
postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-------+---------------------------------------------------------
0/BA5A688 | 10298 | BEGIN 10298
0/BA5A6F0 | 10298 | table public.data: INSERT: id[integer]:1 data[text]:'1'
0/BA5A7F8 | 10298 | table public.data: INSERT: id[integer]:2 data[text]:'2'
0/BA5A8A8 | 10298 | COMMIT 10298
(4 rows)
가운데 두 줄이 변경 한 건씩입니다. table public.data 가 표, INSERT 가 연산입니다. id[integer]:1
이 키입니다. data[text]:'1' 이 바뀐 뒤 값입니다. 넣기라서 바뀌기 전 값은 없습니다.
맨 앞 lsn 칸이 위치입니다. LSN(Log Sequence Number, 로그 순번)은 WAL 안에서 그 변경이 적힌 곳을
가리키는 번호입니다. 가운데 xid 칸은 그 변경이 속한 트랜잭션의 번호입니다. 두 변경은 BEGIN 과
COMMIT 사이에 묶여 나옵니다. 한 번 꺼낸 변경은 다음
호출에서 다시 나오지 않습니다.
Debezium 이 내보낸 고치기 이벤트
Debezium 은 원본의 변경 기록을 읽어 변경 이벤트를 내보내는 캡처기입니다. 내보낸 이벤트는 Apache
Kafka 라는 메시지 브로커에 쌓입니다. 받는 쪽은 거기서 읽어 갑니다. 아래는 PostgreSQL 의 customers
표에서 한 행을 고쳤을 때 나온 이벤트입니다.
{
"schema": { ... },
"payload": {
"before": {
"id": 1
},
"after": {
"id": 1,
"first_name": "Anne Marie",
"last_name": "Kretchmar",
"email": "[email protected]"
},
"source": {
"version": "3.7.0.Final",
"connector": "postgresql",
"name": "PostgreSQL_server",
"ts_ms": 1559033904863,
"ts_us": 1559033904863769,
"ts_ns": 1559033904863769000,
"snapshot": false,
"db": "postgres",
"schema": "public",
"table": "customers",
"txId": 556,
"lsn": 24023128,
"xmin": null
},
"op": "u",
"ts_ms": 1465584025523,
"ts_us": 1465584025523514,
"ts_ns": 1465584025523514964,
}
}
op 의 u 가 연산입니다. 이 칸에 올 수 있는 값은 여섯입니다.
| 값 | 뜻 |
|---|---|
c |
만들기 |
u |
고치기 |
d |
지우기 |
r |
읽기. 스냅샷 때만 나옵니다 |
t |
잘라내기 |
m |
메시지 |
before 에는 id 하나만 있습니다. 이 표의 REPLICA IDENTITY 설정이 기본값 DEFAULT 이기
때문입니다. 이 설정에서는 바뀌기 전 값으로 기본 키 열만 남습니다. 행 전체의 이전 값을 받으려면 표를
REPLICA IDENTITY FULL 로 바꿉니다. after 는 고친 뒤의 행입니다. first_name 이 이제 Anne Marie 입니다.
source 는 이 변경이 원본 어디서 왔는지를 담습니다. table 이 표, txId 가 트랜잭션 번호, lsn
이 WAL 안의 위치입니다. 시각은 두 군데에 찍힙니다. source.ts_ms 는 데이터베이스에서 변경이 일어난
시각입니다. 바깥의 ts_ms 는 Debezium 쪽에 찍힌 시각입니다. 둘을 견주면 원본과 Debezium 사이의
지연이 나옵니다. 위 예시의 두 값은 서로 몇 해씩 벌어져 있어 이 값끼리는 지연이 안 나옵니다. 견주는
자리만 보면 됩니다.
MongoDB change stream 의 이벤트
MongoDB 는 행 대신 문서를 담는 데이터베이스입니다. 표에 해당하는 묶음이 컬렉션입니다. 변경 기록은 oplog(operations log, 작업 로그)입니다. change stream 은 애플리케이션이 oplog 를 직접 뒤지지 않고 변경을 구독하게 해 줍니다.
아래는 문서 하나를 넣었을 때 change stream 이 돌려준 이벤트입니다.
{
"_id": {
"_data": "82635019A0000000012B042C0100296E5A1004AB1154ACACD849A48C61756D70D3B21F463C6F7065726174696F6E54797065003C696E736572740046646F63756D656E744B65790046645F69640064635019A078BE67426D7CF4D2000004"
},
"operationType": "insert",
"clusterTime": Timestamp({ "t": 1666193824, "i": 1 }),
"collectionUUID": new UUID("ab1154ac-acd8-49a4-8c61-756d70d3b21f"),
"wallTime": ISODate("2022-10-19T15:37:04.604Z"),
"fullDocument": {
"_id": ObjectId("635019a078be67426d7cf4d2"'),
"name": "Giovanni Verga"
},
"ns": {
"db": "test",
"coll": "names"
},
"documentKey": {
"_id": ObjectId("635019a078be67426d7cf4d2")
}
}
operationType 이 연산입니다. ns 의 db · coll 이 어느 데이터베이스의 어느 컬렉션인지를
말합니다. documentKey 가 키입니다. fullDocument 는 넣은 뒤의 문서입니다. 맨 앞 _id 의 _data 는 재개
토큰입니다. 이 값을 넘기며 다시 열면 이 이벤트 다음부터 이어 받습니다.
고치기 이벤트는 기본으로 바뀐 칸만 싣습니다. 아래는 engineering 데이터베이스 users 컬렉션의
문서 하나를 고친 이벤트에서 바뀐 칸을 담는 부분만 떼어 온 것입니다.
"updateDescription": {
"updatedFields": {
"email": "[email protected]"
},
"removedFields": ["phoneNumber"],
"truncatedArrays": [ {
"field" : "vacation_time",
"newSize" : 36
} ]
}
email 칸이 새 값으로 바뀌었습니다. phoneNumber 칸은 지워졌습니다. vacation_time 배열은 길이가
36 으로 잘렸습니다. 고친 뒤의 문서 전체를 받으려면
change stream 을 그렇게 설정합니다. 바뀌기 전 문서는 MongoDB 6.0 부터 받을 수 있습니다.
DynamoDB Streams 의 스트림 레코드
DynamoDB 는 아마존이 운영하는 관리형 데이터베이스입니다. 행에 해당하는 것이 항목입니다. DynamoDB Streams 는 표의 항목 단위 변경을 시간 순서대로 최대 24시간 남겨 두는 기록입니다.
변경 한 건이 스트림 레코드 하나입니다. 레코드의 칸은 이렇습니다.
| 칸 | 담는 것 |
|---|---|
eventName |
연산. INSERT · MODIFY · REMOVE 중 하나 |
Keys |
바뀐 항목의 기본 키 |
NewImage |
바뀐 뒤의 항목 |
OldImage |
바뀌기 전의 항목 |
SequenceNumber |
스트림 안의 순번 |
StreamViewType |
무엇을 실었나. KEYS_ONLY · NEW_IMAGE · OLD_IMAGE · NEW_AND_OLD_IMAGES |
StreamViewType 은 스트림을 만든 뒤에는 못 바꿉니다. 바꾸려면 스트림을 끄고 새로 만듭니다. 항목의
값을 하나도 안 바꾼 PutItem · UpdateItem 은 레코드를 남기지 않습니다.
다섯 칸이 실리는 이름
네 실물을 상세의 다섯 칸에 맞춰 놓으면 이렇습니다.
| 칸 | PostgreSQL 출력 | Debezium | MongoDB | DynamoDB Streams |
|---|---|---|---|---|
| 표와 키 | table public.data · id |
source.table · before.id |
ns · documentKey |
Keys |
| 연산 | INSERT |
op |
operationType |
eventName |
| 바뀐 뒤 값 | data[text]:'1' |
after |
fullDocument · updatedFields |
NewImage |
| 바뀌기 전 값 | 넣기라 없음 | before |
6.0 부터 설정하면 | OldImage |
| 위치 | lsn |
source.lsn |
_id 의 재개 토큰 |
SequenceNumber |
다섯 칸이 가리키는 것은 제품이 바뀌어도 같습니다. 칸의 이름과 바뀌기 전 값을 기본으로 어디까지 싣느냐가 다릅니다. 위 표의 빈칸처럼 연산이나 설정에 따라 안 실리는 칸도 있습니다.
갈래
캡처기가 변경을 어디에서 알아내느냐가 축입니다. 표를 밖에서 질의로 훑을 수 있습니다. 쓰기 트랜잭션 안에서 트리거가 따로 적게 할 수도 있습니다. 데이터베이스가 이미 남긴 변경 기록을 읽을 수도 있습니다.
flowchart TD
subgraph SRC["원본 데이터베이스"]
W["쓰기 트랜잭션"] -->|고친다| TBL["표"]
W -->|적는다| LOG["변경 기록"]
W -->|불러낸다| TR["트리거"]
TR -->|적는다| CT["변경 표"]
end
Q["캡처기 · 질의로 훑기"] -->|읽는다| TBL
RT["캡처기 · 트리거"] -->|읽는다| CT
RD["캡처기 · 변경 기록 읽기"] -->|읽는다| LOG
질의로 훑기는 표를 밖에서 들여다봅니다. 트리거는 원본 안에서 쓰기 트랜잭션에 끼어듭니다. 트리거가 적어 둔 변경 표를 캡처기가 읽습니다. 변경 기록 읽기는 이미 적힌 기록을 옆에서 읽습니다.
질의로 훑기
주기마다 SQL(Structured Query Language) 질의를 돌려 새 행이나 고친 행만 골라 읽습니다. 무엇이 새것인지는 표의 열로 가립니다. Kafka Connect 는 Kafka 와 다른 시스템을 잇는 커넥터를 올려 돌리는 틀입니다. 그 위에서 도는 JDBC(Java Database Connectivity) 소스 커넥터가 이 방식을 씁니다. 어느 열로 가리느냐에 따라 잡는 것이 갈립니다.
| 가리는 열 | 잡는 것 | 놓치는 것 · 서야 하는 전제 |
|---|---|---|
| 행마다 커지는 번호 열 | 새 행 | 고친 행. 그래서 한 번 넣고 안 바꾸는 데이터에만 씁니다 |
| 마지막으로 고친 시각 열 | 지난번 뒤에 고친 행 | 시각이 같은 행이 여럿일 때 그중 하나만 처리한 뒤 장애가 나면 나머지를 놓칩니다 |
| 번호 열과 시각 열을 함께 | 고친 행. 장애로 중간에 멈춰도 복구 뒤 남은 것을 잡습니다 | 시각이 충분히 잘게 찍혀야 합니다. 그래야 고친 건마다 번호와 시각의 짝이 하나로 정해집니다 |
지운 행은 질의 결과에 안 나오므로 안 잡힙니다. 주기 사이에 여러 번 바뀐 행도 마지막 값 하나만 보입니다. 중간 상태는 잡히지 않습니다.
번호 열만 쓸 때는 커밋 순서가 어긋나면 행을 잃습니다. 동시에 넣는 트랜잭션이 많으면 번호 101 이 100 보다 몇 밀리초 먼저 커밋될 수 있습니다. 그 사이에 질의가 돌면 캡처기는 101 까지 읽었다고 적습니다. 다음 질의는 101 보다 큰 번호만 묻기 때문에 뒤늦게 커밋된 100 을 건너뜁니다.
sequenceDiagram
participant 쓰기 as 쓰기 트랜잭션
participant 표
participant 캡처기
쓰기->>표: 101 번 행을 커밋한다
캡처기->>표: 적어 둔 번호 뒤를 묻는다
표-->>캡처기: 101
Note over 캡처기: 101 까지 읽었다고 적는다
쓰기->>표: 100 번 행을 뒤늦게 커밋한다
캡처기->>표: 101 보다 큰 번호를 묻는다
표-->>캡처기: 없다
트리거
트리거는 표에 변경이 생길 때마다 데이터베이스가 알아서 실행하는 작은 절차입니다. 이 절차가 변경 내용을 별도의 변경 표에 한 줄씩 적습니다. 캡처기는 원본 표가 아니라 그 변경 표를 읽습니다.
PostgreSQL 로 짠 감사 트리거가 이 꼴입니다. emp 표에 넣기 · 고치기 · 지우기가 생기면 emp_audit
표에 연산 글자(I · U · D) · 시각 · 사용자 이름 · 행의 값을 한 줄로 적습니다. 지우기는 지운 행을
적습니다. 넣기와 고치기는 새 행을 적습니다.
트리거는 그 표에 들어온 넣기 · 고치기 · 지우기라면 어디서 왔든 잡습니다. 애플리케이션을 거친 쓰기든 콘솔에서 직접 고친 것이든 같습니다. 대신 원본을 고치는 트랜잭션 안에서 같이 돕니다. 원본 쓰기 하나마다 변경 표 쓰기가 하나 더 붙습니다.
변경 기록 읽기
데이터베이스가 이미 적어 둔 변경 기록을 캡처기가 읽어 내보냅니다. 원본 표에 열이나 트리거를 더하지 않습니다. 기록의 이름과 형식은 제품마다 다릅니다. 기본 설정의 기록에는 캡처에 필요한 만큼이 안 적힐 수 있어서 설정을 먼저 바꿉니다.
| 제품 | 변경 기록 | 먼저 켜는 것 | 바뀌기 전 값을 정하는 설정 |
|---|---|---|---|
| PostgreSQL | WAL | wal_level = logical |
표마다 REPLICA IDENTITY. 기본은 기본 키 열만, FULL 이면 모든 열 |
| MySQL | 바이너리 로그(binlog) | 행 단위 기록 ROW. 8.4 의 기본값입니다 |
binlog_row_image. 기본 full 은 모든 열, minimal 은 행을 가릴 열만 |
| SQL Server | 트랜잭션 로그 | 표마다 변경 데이터 캡처를 켭니다 | 고치기를 이전 행과 이후 행 두 줄로 적습니다 |
SQL Server 는 위 세 갈래 중 둘을 이어 붙인 꼴입니다. 변경 기록인 트랜잭션 로그를 읽는 것이 앞쪽
절반입니다. 읽은 것을 변경 표에 옮겨 적어 캡처기가 그 표를 읽게 하는 것이 뒤쪽 절반입니다. 뒤쪽은
트리거 갈래와 같은 꼴입니다. 로그를 읽어 변경 표를 채우는 이 작업이 캡처 과정입니다. 변경 표의
__$operation 칸 값이 1 지우기 · 2 넣기 · 3 고치기 전 · 4 고치기 후입니다.
셋을 나란히 놓으면 이렇게 갈립니다.
| 갈래 | 잡히는 것 | 놓치거나 치르는 것 |
|---|---|---|
| 질의로 훑기 | 가리는 열로 새것이라 드러난 행 | 지운 행 · 주기 사이 중간 상태 · 열이 못 가린 행 |
| 트리거 | 그 표에 들어온 모든 넣기 · 고치기 · 지우기 | 원본 쓰기마다 붙는 변경 표 쓰기 |
| 변경 기록 읽기 | 원본이 기록에 적은 변경을 적힌 순서대로 | 제품마다 다른 형식 · 설정과 읽기 권한 · 원본에 더해질 수 있는 부하 |
마지막 칸의 부하는 원본 안에 아무것도 저장하지 않는 캡처기에도 생길 수 있습니다. 캡처기를 돌리는 것만으로 원본이 일을 더 하기 때문입니다.
보장과 가정
아래 약속 다섯은 변경 기록 읽기 갈래의 캡처기가 받는 쪽에 하는 것입니다. 약속마다 그것이 서 있는 가정이 있습니다. 가정이 깨지면 약속도 깨집니다. 약속하지 않는 것도 하나 있습니다.
갈래를 바꾸면 약속도 바뀝니다. 질의로 훑기에서는 빠짐없는 전달과 순서가 서지 않습니다. 가리는 열이 시각 하나면 고친 데이터가 다 넘어간다고 보장하지 못합니다. 번호 열 하나면 커밋 순서가 어긋난 행을 잃습니다. 갈래 절에 적은 그대로입니다.
한 행의 변경 순서
보장 — 한 행에 생긴 변경은 원본에서 일어난 순서대로 받는 쪽에 닿습니다. 같은 행의 상태가 결제 대기에서 결제됨으로, 다시 환불됨으로 바뀌었다면 이 셋이 닿는 순서가 곧 결과이기 때문입니다.
원본 쪽은 이 순서를 갖고 있습니다. PostgreSQL 의 복제 슬롯은 원본에서 일어난 순서대로 변경을 내줍니다. SQL Server 는 커밋 LSN 으로 트랜잭션의 순서를 매깁니다. DynamoDB Streams 는 같은 항목에 대한 레코드를 실제 변경 순서대로 둡니다. 다만 DynamoDB 가 지키는 단위는 항목 하나입니다. 여기서 항목 하나는 같은 기본 키를 가리킵니다. DynamoDB 의 기본 키는 파티션 키 하나이거나, 파티션 키와 정렬 키 둘입니다. 같은 파티션 키를 나눠 가진 항목이 여럿이면 그 항목들 사이의 순서까지는 지키지 않습니다.
가정 — 캡처기 뒤의 전달 경로도 그 순서를 지켜야 합니다. Debezium 은 변경 이벤트를 Kafka 에 싣습니다. Kafka 에서 이벤트를 싣는 통로가 토픽입니다. 토픽을 여러 갈래로 나눈 것 하나하나가 파티션입니다. 앞의 DynamoDB 파티션 키와는 다른 것입니다 — 이쪽은 전달 경로를 나눈 갈래입니다.
Kafka 로 가는 이벤트에는 키가 하나 붙습니다. 이것을 메시지 키라고 합니다. Debezium 의 PostgreSQL 커넥터에서 표를 통째로 비우는 잘라내기 이벤트에는 이 키가 붙지 않습니다. 그래서 파티션이 여럿인 토픽에서는 한 표의 이벤트 순서가 보장되지 않습니다. 표를 비운 잘라내기 이벤트를 받은 다음에 같은 표의 고치기 이벤트가 다른 파티션에서 도착할 수 있습니다. 이 커넥터에서 한 표의 이벤트 순서가 보장되는 것은 파티션이 하나인 토픽뿐입니다. 이 절의 보장은 거기까지입니다 — 파티션이 여럿이면 한 표 안의 순서를 이 커넥터가 약속하지 않습니다.
빠짐없는 전달과 되풀이
보장 — 변경 기록을 읽는 캡처기는 변경을 잃지 않습니다. 평소에는 변경 하나가 한 번씩 갑니다. 장애가 나면 같은 변경이 다시 올 수 있습니다. 잃지는 않되 두 번 올 수 있다는 이 약속을 최소 한 번 전달이라고 부릅니다.
가정 — 다시 뜬 캡처기는 마지막으로 적어 둔 위치부터 읽습니다. 그런데 위치는 매번이 아니라 띄엄띄엄 적힙니다. 체크포인트는 데이터베이스가 메모리에 든 변경을 디스크에 정리해 적는 때입니다. PostgreSQL 의 복제 슬롯은 위치를 체크포인트 때만 디스크에 남깁니다. 그 사이에 원본이 죽으면 슬롯이 더 앞의 LSN 으로 돌아갈 수 있습니다. 그러면 서버가 다시 뜰 때 최근 변경이 다시 나갑니다.
Debezium 도 같습니다. 커넥터를 돌리던 Kafka Connect 프로세스가 갑자기 멈추면, 마지막으로 처리한 위치를 적지 못한 채 끝납니다. Kafka Connect 를 여러 프로세스에 나눠 돌리는 분산 모드라면, 멈춘 작업을 다른 프로세스에서 다시 띄웁니다. 다시 뜬 작업은 마지막으로 적힌 위치부터 읽습니다. 그래서 멈추기 직전에 처리한 이벤트를 다시 낼 수 있습니다.
sequenceDiagram
participant 원본 as 원본 데이터베이스
participant 캡처기
participant 받는쪽 as 받는 쪽
캡처기->>받는쪽: 변경 1
Note over 캡처기: 위치를 적는다 · 변경 1 까지
캡처기->>받는쪽: 변경 2
캡처기->>받는쪽: 변경 3
Note over 캡처기: 위치를 적기 전에 멈춘다
캡처기->>원본: 적어 둔 위치 뒤부터 다시 읽는다
캡처기->>받는쪽: 변경 2 · 두 번째
캡처기->>받는쪽: 변경 3 · 두 번째
그래서 받는 쪽은 같은 변경을 두 번 받아도 결과가 같게 만들어야 합니다. 이 성질이 멱등성입니다. 값을 더하는 대신 행 전체를 덮어쓰는 식입니다. 변경마다 실린 위치를 기억해 두고 이미 본 것을 건너뛰는 방법도 있습니다.
제품마다 약속하는 범위가 다릅니다. DynamoDB Streams 는 레코드 하나가 스트림 안에 정확히 한 번 나타나도록 돕습니다. 스트림 안에 한 번 있다는 말이지, 읽는 쪽이 한 번만 받는다는 말은 아닙니다. MongoDB 의 change stream 은 데이터를 가진 구성원 과반에 기록된 변경만 알립니다. 장애가 나도 되돌려지지 않을 변경만 내보낸다는 약속입니다.
멈춘 뒤 이어 읽기
보장 — 캡처기가 멈췄다 다시 뜨면 적어 둔 위치부터 이어 읽습니다. 멈춰 있던 동안의 변경도 빠지지 않습니다.
가정 — 변경 기록이 그 위치를 아직 갖고 있어야 합니다. 기록은 끝없이 쌓이지 않습니다. 남겨 두는 범위를 넘기면 지워집니다.
| 제품 | 변경 기록이 남는 범위 | 넘기면 |
|---|---|---|
| PostgreSQL | 복제 슬롯이 필요한 WAL 을 지우지 못하게 붙잡습니다. 상한 max_slot_wal_keep_size 의 기본값은 -1, 무제한입니다 |
상한을 넘겨 필요한 WAL 이 지워지면 그 슬롯으로 복제를 이어 가지 못할 수 있습니다 |
| SQL Server | 변경 표를 기본 3일 남깁니다 | 읽으려는 구간이 남은 구간 밖이면 변경을 놓칠 수 있습니다 |
| MongoDB | oplog 에 남은 만큼 | 재개 토큰이 가리키는 작업을 oplog 에서 찾을 수 있어야 이어 받습니다 |
| DynamoDB Streams | 24시간 | 레코드가 자동으로 지워집니다 |
이 가정을 지키는 데도 값이 듭니다. PostgreSQL 의 복제 슬롯은 연결이 없어도 필요한 자원을 지우지 못하게 붙잡습니다. 슬롯이 그것을 요구하는 동안에는 청소 작업이 필요한 WAL 도, 시스템 카탈로그의 필요한 행도 지우지 못합니다. 캡처기가 오래 멈춰 있으면 그만큼 저장 공간을 먹습니다.
트랜잭션 ID 는 데이터베이스가 트랜잭션마다 매기는 번호입니다. 극단적인 경우 데이터베이스는 이 번호가 한 바퀴 돌아 앞의 번호와 겹치는 것을 막으려고 스스로 멈춥니다. 그래서 더 쓰지 않는 슬롯은 지웁니다.
처음 붙일 때의 스냅샷
보장 — 처음 붙일 때도 이미 있던 행부터 빠짐없이 시작합니다.
변경 기록에는 지난 변경이 다 남아 있지 않습니다. 기록만 처음부터 읽어서는 지금 표에 든 행을 다 만들 수 없습니다. 그래서 먼저 지금 표에 든 행을 한 번 전부 읽어 받는 쪽에 옮깁니다. 이것이 스냅샷입니다.
가정 — 스냅샷을 뜨기 전에 변경 기록의 지금 위치를 먼저 읽어 둡니다. 스냅샷이 끝나면 그 위치부터 변경 기록을 이어 읽습니다.
Debezium 의 PostgreSQL 커넥터는 이 순서로 스냅샷을 뜹니다.
sequenceDiagram
participant 캡처기
participant 원본 as 원본 데이터베이스
participant 받는쪽 as 받는 쪽
캡처기->>원본: 1 트랜잭션을 연다
캡처기->>원본: 2 변경 기록의 지금 위치를 읽는다
캡처기->>원본: 3 표를 전부 훑는다
캡처기->>받는쪽: 행마다 읽기 이벤트
캡처기->>원본: 4 트랜잭션을 닫는다
Note over 캡처기: 5 스냅샷을 마쳤다고 적는다
캡처기->>원본: 2 에서 읽은 위치부터 변경 기록을 이어 읽는다
2 에서 읽은 위치부터 이어 읽으므로 스냅샷을 뜨는 동안의 변경도 놓치지 않습니다. 5 를 적기 전에 멈추면 다시 뜰 때 스냅샷을 처음부터 새로 뜹니다.
격리 수준은 한 트랜잭션이 다른 트랜잭션의 변경을 어디까지 보느냐를 정하는 설정입니다. 1 에서 연 트랜잭션은 이 설정에 따라 그동안 다른 쓰기가 한 변경을 볼 수도 있습니다. 2 뒤에 커밋된 변경이 3 의 훑기에도 보였다면, 그 변경은 스냅샷으로 한 번 가고 2 의 위치 뒤 변경 기록으로 또 한 번 갈 수 있습니다. 같은 변경이 두 길로 받는 쪽에 닿을 수 있다는 뜻입니다.
PostgreSQL 도 복제 슬롯을 만들 때 스냅샷을 함께 내보낼 수 있습니다. 이 스냅샷이 보여 주는 것은 슬롯을 만든 순간의 상태입니다. 그 순간 뒤의 변경은 전부 슬롯으로 들어옵니다. 이 상태를 떠 놓고 슬롯의 변경을 이어 적용하면 잃는 변경이 없습니다.
원본 표의 모양 변경
보장 — 원본 표의 열이 늘거나 줄어도 변경의 흐름이 이어집니다.
가정 — 받는 쪽이 새 모양을 알아야 합니다. 이 가정이 깨지면 변경은 계속 오는데 받는 쪽이 그것을 자기 표에 넣을 데가 없습니다. 원본 표의 열이 바뀌는 것은 받는 쪽에 까다로운 일입니다. 제품마다 다루는 방법이 다릅니다.
PostgreSQL 의 논리 디코딩은 표 정의를 바꾸는 문장을 변경으로 내보내지 않습니다. 이런 문장을
DDL(Data Definition Language)이라고 부릅니다. 예시의 data 표를 만든 트랜잭션을 슬롯에서 꺼내
보면 BEGIN 과 COMMIT 두 줄만 나옵니다. 그러면 받는 쪽이 새 모양을 어디서 알게 되는지는 자료에
없습니다(미확인).
SQL Server 는 변경 표의 열 구조를 고정해 둡니다. 원본에 새 열이 생기면 무시합니다. 추적하던 열이
사라지면 그 칸에 null 을 넣습니다. 열의 타입이 바뀌면 변경 표에도 반영해 값을 잃지 않게 합니다.
구조가 바뀐 기록은 cdc.ddl_history 표에 따로 적어 받는 쪽이 확인하게 합니다.
약속하지 않는 것
원본과 받는 쪽이 같은 순간에 같은 값을 보인다는 약속은 없습니다. 원본에 변경이 커밋된 때와 받는 쪽에 닿는 때 사이에는 틈이 있습니다.
SQL Server 는 캡처 과정이 로그를 읽은 뒤에야 변경 표에 변경이 나타납니다. 그 지연은 대개 작습니다.
그래도 늘 있습니다. DynamoDB Streams 도 레코드를 거의 실시간으로 적는다고 할 뿐입니다. Debezium
에서는 앞의 두 ts_ms 를 견줘 이 틈을 잽니다.
주문을 저장하자마자 검색 결과에서 찾으면 안 나올 수 있습니다. 이렇게 시간이 지나면 같아지는 상태를 최종 일관성이라고 부릅니다. 쓰자마자 같은 값을 읽어야 하는 곳에는 변경 데이터 캡처가 안 맞습니다.
관련 항목
변경 데이터 캡처가 읽어 내는 데이터베이스 기록
트랜잭션 로그 · WAL · 미리 쓰기 로그 · binlog · oplog · redo 로그 · 복제 슬롯 · LSN · 논리 디코딩 · REPLICA IDENTITY
변경 데이터 캡처가 변경을 잡아내는 다른 방식
트리거 · 폴링 · 타임스탬프 · 버전 열 · 소프트 삭제 · 툼스톤
변경 데이터 캡처와 같은 문제를 다르게 푸는 설계
이중 쓰기 · 아웃박스 패턴 · 이벤트 소싱 · CQRS · 배치 ETL · 전량 적재
변경 데이터 캡처를 내장 기능으로 갖춘 데이터베이스
PostgreSQL · MySQL · SQL Server · MongoDB · change stream · DynamoDB · DynamoDB Streams
변경 데이터 캡처를 구현한 도구와 서비스
Debezium · Kafka Connect · Apache Kafka · AWS DMS · Oracle GoldenGate · Maxwell
변경 데이터 캡처가 변경을 실어 보내는 전달 경로
이벤트 스트림 · 메시지 큐 · 메시지 브로커 · 토픽 · 파티션 · 스트림 처리
변경 데이터 캡처가 변경을 흘려보내는 대상 저장소
검색 색인 · Elasticsearch · 데이터 웨어하우스 · 데이터 레이크 · 캐시 · 읽기 모델 · 구체화 뷰
변경 데이터 캡처가 지켜야 하는 전달 성질
멱등성 · 최소 한 번 전달 · 정확히 한 번 전달 · 순서 보장 · 오프셋 · 체크포인트 · 재개 토큰
변경 데이터 캡처를 쓸 때 벌어지는 어긋남
최종 일관성 · 복제 지연 · 스키마 변경 · 스키마 레지스트리 · 백프레셔 · 재처리
변경 데이터 캡처가 처음 붙을 때 거치는 단계
변경 데이터 캡처가 놓이는 데이터 파이프라인 단계
원천 · 대상 · 수집 · 변환 · 적재 · 파이프라인 · 데이터 엔지니어링 · ETL · ELT
변경 데이터 캡처가 실어 나르는 흐름을 쓰는 용도
다른 이름: Change Data Capture · CDC · 변경 데이터 수집 · 체인지 데이터 캡처