(KO) CUBRID checksumdb — chunked, replication-replayed 비교를 통한 HA replica vs master 행 checksum 검증기
이론적 배경
섹션 제목: “이론적 배경”Replica 무결성 검증기 는 “내 replica 가 정말 master 와 동일
한가?” 라는 질문에 답하는 도구이다 — 긴 복제 chain 후, 데이터
부패 의심 후, 또는 계획된 failover 전 정기 점검으로 떠오르는
질문이다. 사소한 구현 (양쪽을 dump 하고 diff)은 규모에서
비실용적이다 — dump 동안 쓰기를 중단하거나 비싼 스냅샷-추적
비용을 내야 하고, 대역폭 비용이 replica 당 한 번의 full
table-scan 이 든다.
Production 엔진은 따라서 sampling 또는 chunked-checksum 검증기 를 ship 한다 — exhaustiveness 를 online 동작과 trade 한다는 점이다:
- Per-row checksum. 행별 hash 비교; exhaustive 하지만 데이터 크기에 비례하고 모든 행을 양측 동기화가 필요하다.
- Chunked checksum. 표가 chunk 로 split (PK 범위, 행 수, 또는 시간 window 별); chunk 별 checksum 을 양측에서 계산해 비교. chunk 수준 mismatch 가 divergence 를 표시; 운영자가 표시된 chunk 에서만 per-row 로 drill down 할 수 있다는 점이다.
- Replication-replayed checksum. master 가 chunk 별 checksum
을 복제되는 표 로 계산하고, 같은 복제 stream 이 slave 로
운반하고, slave 가 같은 chunk 를 로컬에서 재계산해 자기 checksum
으로 표를 update. 비교가 결과 표에 대한
WHERE master_checksum != slave_checksum쿼리가 된다는 뜻이다. 대역폭 비용이 chunk 당 summary 행 한 개이지 데이터 행당 한 개가 아니다.
CUBRID 의 checksumdb 가 옵션 3 을 고른다 — primary-key 범위
(각 chunk 가 표의 논리적 순서에서 contiguous), 행으로 측정되는
configurable chunk size, 그리고 행 데이터가 멀쩡할 때조차 DDL
drift 를 감지하는 별도 schema-checksum 측면.
일반 DBMS 디자인
섹션 제목: “일반 DBMS 디자인”| 엔진 | 도구 | 접근 | Replica 왕복 |
|---|---|---|---|
| MySQL | pt-table-checksum (Percona Toolkit) | PK 범위별 chunked CRC32; checksum SQL 을 binlog 통해 복제해 slave 가 자기 데이터로 계산 | chunk 당 결과 행 하나 복제 |
| PostgreSQL | pg_compare, pglogical_compare, bucardo_ctl validate | per-table 스냅샷 diff; 일부 도구는 chunked hash 사용 | 도구마다 다름 |
| Oracle | DBMS_COMPARISON 패키지 | chunked PK 범위 hash; 차이 reconcile 도 가능 | scan 컬럼당 summary 행 하나 |
| MongoDB | db.collection.dataSize() summary 검사; first-class row-level 무결성 검증기 없음 | application 수준 | n/a |
| CUBRID checksumdb | PK 범위별 chunked CRC, 데이터를 운반하는 같은 WAL stream 으로 복제, chunk 동안 lock; 명시적 schema-checksum 측면 | chunk 당 결과 행 하나 복제; 스키마 행 별도 복제 |
CUBRID 디자인은 pt-table-checksum 에 가장 가깝다 — chunked PK
범위, replication-replayed, lock-during-checksum. 차별점은 명시적
schema-checksum 표 이다 — master 와 replica 사이 DDL drift 가
행-데이터 divergence 보고와 충돌하지 않는 별도 row 클래스를
생성한다는 점이다.
CUBRID 의 접근
섹션 제목: “CUBRID 의 접근”두 복제 표
섹션 제목: “두 복제 표”checksumdb 가 자기 사용을 위해서만 존재하는 두 전용 표를
유지한다는 점이다:
| 표 | 내용 | 목적 |
|---|---|---|
db_ha_apply_info_chksum_* (결과 표) | (target 표, chunk_id) 쌍당 한 행: PK lower bound, chunk 크기, master checksum, master 복제 정보, slave checksum (replay 시점에 계산) | chunk 별 검증 |
db_ha_apply_info_chksum_*_schema (schema 표) | 검사된 target 표마다 한 행: 직렬화된 schema 정의 + repid + master schema checksum | DDL-drift 감지 |
정확한 이름 suffix 는 configurable 하다. 상수 chksum_result_Table_name
와 chksum_schema_Table_name 가 startup 동안 데이터베이스 이름
에서 채워진다.
두 표는 일반 CUBRID 표 다 — 다른 카탈로그 행처럼 WAL 에
참여한다. 즉 master 의 그 표에 대한 쓰기가 기존 HA 복제 경로
(cubrid-ha-replication.md 참조)로 자동으로 slave 로
전파된다는 뜻이다. Slave 측에서는 apply-log 데몬이 master 의
행 insert 를 결과 표로 replay; slave 측 checksumdb 가 그 다음
각 행을 다시 읽고 chunk 의 checksum 을 로컬에서 재계산하고
slave 의 자기 checksum 을 비교를 위해 같은 행에 다시 쓴다.
PK 범위별 chunking
섹션 제목: “PK 범위별 chunking”각 user 표를 checksumdb 가 PK 순서에 따라 데이터를 walk
하면서 --chunk-size 행 (기본 10,000)으로 chunk 로 자른다는
점이다. 각 chunk 가 다음을 가진다:
lower_bound: 적절한 quoting 과 escaping 과 함께 “(pk_col1, pk_col2, …) >= (lb1, lb2, …) 인 행” 을 select 하는WHERE절로 쓰여진, chunk 의 첫 행과 동등한 출력 가능 표현식.upper_bound: 암묵적 — 다음 chunk 의 lower bound 또는 표의 끝.chunk_id: (표, chunk) 쌍당 순차적 정수.checksum: PK 순서로 chunk 의 행에 대한 CRC, WHERE-bound 가 적용되고LIMIT chunk_size가 있는 표 자체에서 read 되는SELECT BIT_XOR(MD5(...))쿼리로 계산.
// chksum_calculate_checksum — checksumdb.c:1686chksum_calculate_checksum (PARSER_CONTEXT *parser, const OID *class_oidp, const char *table_name, DB_ATTRIBUTE *attributes, PARSER_VARCHAR *lower_bound, int chunk_id, int chunk_size){ /* 이 chunk 에 대한 실제 checksum SELECT 빌드 */ checksum_query = chksum_print_checksum_query (parser, table_name, attributes, lower_bound, chunk_id, chunk_size);
/* master 의 result-table-INSERT replay 가 slave 로 올바른 컨텍스트 * 를 운반하게 복제 record 정보 설정; * SELECT 동안 chunk 가 일관되게 user 표에 SHARED lock 획득 */ chksum_set_repl_info_and_demote_table_lock (table_name, checksum_query, ...);
/* checksum SELECT 실행; 결과 표에 결과 행 INSERT; * chksum_get_next_lower_bound 통해 다음 chunk 로 진행 */}Lock 패턴이 중요하다 — 배타 LOCK TABLE WRITE 는 reader 를
block 한다. LOCK TABLE READ (SHARED)가 user 표에 획득되어
다른 reader 는 허용하고 chunk 의 SELECT 동안만 writer 를 block
한 다음, downgrade / release 된다. 이는 일관성과 online 동작
사이의 trade-off 다 — lock 동안 복제는 잠시 queue 되었다가 잠시
멈췄다가 재개된다는 점이다.
Lower-bound iteration
섹션 제목: “Lower-bound iteration”chksum_get_next_lower_bound (chunk 사이에 호출됨)가 다음 chunk
의 첫 행이 될 행의 PK 를 찾는 짧은 SELECT 를 발사한다는 점이다:
SELECT pk_col1, pk_col2, ...FROM <table>WHERE (pk_col1, pk_col2, ...) >= (lb1, lb2, ...)ORDER BY pk_col1 ASC, pk_col2 ASC, ...LIMIT <chunk_size>;-- 결과의 마지막 행이 boundarylimited 결과의 마지막 행 이 다음 chunk 의 lower bound 가 된다.
결과가 chunk_size 보다 적으면 표가 끝에 있는 것이고 루프가
종료된다.
가장 첫 chunk 를 chksum_get_initial_lower_bound 가 합성
최소 PK 표현식 — 보통 PK 컬럼 type 에 따라 NULL 또는 MIN(pk)
파생물 — 을 돌려준다. 모든 행을 select 한다.
chksum_arg — 옵션 carrier
섹션 제목: “chksum_arg — 옵션 carrier”struct chksum_arg { int chunk_size; // chunk 당 행; 기본 10000 int sleep_msecs; // throttle 위해 chunk 사이 pause int timeout_msecs; // chunk 당 lock-wait timeout bool resume; // 이전 실행에서 이어받기 bool cont_on_err; // 실패한 표 건너뛰고 계속 bool schema_only; // 데이터 chunk 건너뛰고 schema 검사만 dynamic_array *include_list; // 명시적 표 allowlist dynamic_array *exclude_list; // 표 blocklist (기본: 카탈로그 표)};다섯 운영 노브:
--chunk-size. 더 작은 chunk = 더 많은 lock-acquire 오버헤드- 더 많은 결과 행, 하지만 더 짧은 chunk 당 lock window. 기본 10,000 은 OLTP 표에 production sweet spot 이다 — 상당히 넓은 행이나 상당히 바쁜 표는 더 작게 원할 수 있다는 점이다.
--sleep-msecs. 검증기가 양측을 saturate 하지 않게 chunk 사이에 N ms sleep — 특히 SELECT 가 live 워크로드와 경쟁하는 master 에 관련됨.--timeout-msecs. chunk 당 SHARED lock 을 얼마나 기다릴 지; expiration 이 chunk 를 abort (그리고--cont-on-err에 따라 표 또는 전체 실행).--resume. 이전 결과 표를 읽어 표당 가장 높은 저장된chunk_id부터 시작해 이미 checksum 된 chunk 를 건너뛴다.chksum_get_prev_checksum_results가chksum_Prev_results를 채우고,chksum_set_initial_chunk_id_and_lower_bound가 소비 한다는 뜻이다.--schema-only. 데이터를 건너뛰고 schema 표만 다시 검사. full data 실행 전 빠른 pre-flight 로, 또는 per-row 비용 없이 지속적인 DDL-drift 모니터링에 유용.
Include / exclude list 필터링
섹션 제목: “Include / exclude list 필터링”Include 와 exclude list 가 chksum_need_skip_table 에 의해 표별
로 처리된다는 점이다:
include_list가 비어 있지 않으면 list 된 표만 checksum 된다.exclude_list는 항상 include list 를 이긴다.- 기본
exclude_list는 카탈로그 표 (db_class,db_attribute등)를 포함하며 — 그 외에는 거대한 schema-noise 를 생산할 것이다.
두 list 는 --include-class-file / --exclude-class-file 로
명시된 파일 (한 줄에 한 표 이름)에서 읽거나, 반복된
--include-class / --exclude-class 플래그로 직접 설정한다.
Schema 검사
섹션 제목: “Schema 검사”데이터 실행이 시작하기 전에 checksumdb 가 모든 target 표를 schema checksum 을 계산한다는 점이다:
// 각 target 표에 대해:chksum_update_current_schema_definition (table_name, repid);// 클래스 정의 (type 을 가진 컬럼 list, 제약, owner)를 텍스트// canonical 형식으로 직렬화;// MD5 계산; 저장된 schema 행과 비교;// 없으면 새 행 insert;// mismatch 면 chksum_report_schema_diff 에 divergence 표시.Schema checksum 은 현재 revision 시점 (repid 는 카탈로그의
schema revision id) 으로 계산된다. Slave 에서 같은 canonical 형식
직렬화가 로컬에서 돌고 재계산된 checksum 이 비교된다는 뜻이다.
표의 schema 가 diverge 하면, 데이터 chunk checksum 이 여전히 시도되지만 (또한 diverge 하거나 우연히 매치할 수 있다 — 둘 다 기록), 실행은 schema mismatch 를 primary 에러로 보고한다.
Report
섹션 제목: “Report”실행이 완료 (또는 부분 완료)된 후, chksum_report 가 출력 파일
을 열고 세 절을 쓴다는 점이다:
- Header —
chksum_report_header: 데이터베이스 이름, 실행 시작 시간, 출력 표 이름,--chunk-size, include/exclude list. - Summary —
chksum_report_summary: 표별 합계 (검사된 chunk, 매치된 chunk, 미스매치 chunk, schema 상태). - Diff —
chksum_report_diff(데이터)와chksum_report_schema_diff(schema): PK lower bound, master vs slave checksum, 복제 정보를 가진 미스매치별 행.
Report 는 사람이 읽을 수 있는 텍스트 다. Downstream 도구는 보통 모니터링 통합을 위해 summary 표를 파싱한다.
Resume 모드
섹션 제목: “Resume 모드”--resume 는 중단될 수 있는 long-running 검증을 위한 운영 모드
이다 (운영자가 실행을 중지, slave 가 apply 동안 재시작 등).
Resume 시:
chksum_get_prev_checksum_results가 결과 표의 모든 기존 행을 in-memory linked listchksum_Prev_results로 읽는다 (이미 완료된 가장 높은 chunk_id 를 가진 표당 한 entry).- chunk-start 시점에 각 target 표를
chksum_set_initial_chunk_id_and_lower_bound가 list 를 참조; 표에 이전 결과가 있으면chunk_id = prev_chunk_id + 1와 이전 행의last_lower_bound에서lower_bound를 설정. 그 외에는 chunk 1 과 합성 최소-PK bound 부터 시작한다는 뜻이다.
결과 표는 따라서 출력이자 checkpoint 다 — 결과 행을 slave 로 전파하는 같은 WAL 복제가 master 의 부분 실행에서의 재시작도 지원한다.
소스 워크스루
섹션 제목: “소스 워크스루”Top-level (checksumdb.c)
섹션 제목: “Top-level (checksumdb.c)”| 심볼 | 역할 |
|---|---|
checksumdb (진입, cubrid checksumdb 가 호출) | UTIL_FUNCTION_ARG-style 진입; 옵션 파싱; chksum_start 호출 |
chksum_start | 메인 루프; 각 target 표를 schema 검사 그 다음 데이터 chunk |
chksum_init_checksum_tables | 결과 표 drop + 재생성, 또는 resume 시 이전 결과 read |
chksum_drop_and_create_checksum_table | result/schema 표의 카탈로그 수준 drop + create |
chksum_get_prev_checksum_results / _get_checksum_result / _free_results | Resume-mode 이전-실행 consumer |
chksum_need_skip_table | Include/exclude 술어 |
Chunking 과 SQL emit
섹션 제목: “Chunking 과 SQL emit”| 심볼 | 역할 |
|---|---|
chksum_calculate_checksum | per-chunk: SELECT 빌드, repl info 설정, lock, 실행, 결과 insert, 진행 |
chksum_print_checksum_query | chunk 위에 SELECT BIT_XOR(MD5(...)) 빌드 |
chksum_print_pk_list | PK-컬럼 list 렌더 |
chksum_print_attribute_list | 전체 attribute list 렌더 (checksum query 에 사용) |
chksum_print_select_last_chunk | Boundary-finding SELECT |
chksum_print_lower_bound_string / chksum_get_quote_escaped_lower_bound | PK tuple 을 quoted SQL literal 로 렌더 |
chksum_get_initial_lower_bound / chksum_get_next_lower_bound | per-chunk boundary 진행 |
chksum_set_initial_chunk_id_and_lower_bound | Resume-aware 첫-chunk setup |
Locking 과 복제
섹션 제목: “Locking 과 복제”| 심볼 | 역할 |
|---|---|
chksum_set_repl_info_and_demote_table_lock | user 표에 SHARED lock 획득, master 의 result-table-INSERT 가 올바른 metadata 로 복제되도록 복제-record 컨텍스트 설정 |
Schema 검사
섹션 제목: “Schema 검사”| 심볼 | 역할 |
|---|---|
chksum_update_current_schema_definition | 현재 schema 직렬화, MD5 계산, 기존 행 update |
chksum_insert_schema_definition | 이전 행이 없을 때 insert |
Master checksum SELECT
섹션 제목: “Master checksum SELECT”| 심볼 | 역할 |
|---|---|
chksum_print_select_master_checksum | 결과 표에서 master checksum 을 read 하는 SELECT 빌드 |
chksum_print_update_master_checksum | master checksum 을 write 하는 UPDATE 빌드 |
chksum_update_master_checksum | update 실행 |
Reporting
섹션 제목: “Reporting”| 심볼 | 역할 |
|---|---|
chksum_report | top-level report driver |
chksum_report_open_file | report 출력 파일 열기 |
chksum_report_header | header |
chksum_report_summary | 표별 합계 |
chksum_report_diff | per-mismatch 데이터 행 |
chksum_report_schema_diff | per-mismatch schema 행 |
위치 힌트 (2026-05-05 기준)
섹션 제목: “위치 힌트 (2026-05-05 기준)”| 심볼 | 경로 |
|---|---|
CHKSUM_RESULT (struct) | src/executables/checksumdb.c:84 |
CHKSUM_ARG (struct) | src/executables/checksumdb.c:94 |
chksum_calculate_checksum | src/executables/checksumdb.c:1686 |
chksum_print_checksum_query | src/executables/checksumdb.c:1082 |
chksum_get_next_lower_bound | src/executables/checksumdb.c:1319 |
chksum_start | src/executables/checksumdb.c:1798 |
chksum_report_open_file | src/executables/checksumdb.c:160 |
chksum_report_header | src/executables/checksumdb.c:175 |
심볼 이름이 정규 anchor 이고, 라인 번호는 updated: 날짜에
스코프된 힌트이다.
Cross-check 노트
섹션 제목: “Cross-check 노트”- 결과 표 자체가 복제된다. master 의 checksum 행을 복제하는
요점은 slave 의 로컬 재계산이 master 에 별도 연결 없이 비교할
무언가를 가지게 하는 것이다. non-HA 데이터베이스는 schema-only
모드 이상으로는
checksumdb의 사용처가 없다는 뜻이다. - 복제 지연이 중요하다. master 가 chunk 를 끝낸 순간까지
slave 가 따라잡지 못했다면, slave 의 재계산된 checksum 이
표의 이전 상태에 대한 것이 된다. 운영자는 (
cubrid heartbeatstatus 또는db_ha_apply_info_*지연 지표로) 복제가 catch-up 되었다고 확인된 후에만checksumdb를 돌린다는 점이다. - SHARED lock 의미. chunk 당 lock 은
LOCK TABLE READ, SELECT 후 downgrade. 같은 표에 대한 동시 INSERT/UPDATE 가 chunk 당 잠시 queue 되며, 표의 chunk 수만큼 곱해진다. 매우 큰 표를 누적 영향이 의미 있다 —--sleep-msecs가 정확히 그 영향을 분산시키기 위해 존재한다. - PK 가 필수다. primary key 가 없는 표는 안정된 순서를 따라 chunk 할 수 없다. checksumdb 가 경고와 함께 건너뛰고 report 에 skip 을 기록한다는 뜻이다.
- 카탈로그-표 제외가 기본에 hard-coded. exclude list 기본값에
db_class,db_attribute,db_attr_setdomain_elm등이 포함 된다. 카탈로그 자체를 검증하고 싶은 운영자는 exclude list 를 override 해야 하지만, 카탈로그가 또한 가장 가능성 있는 복제 drift 의 희생자다 — 그래서 이는 정확성 호출이라기보다는 noise- suppression 기본값에 가깝다. - Schema 직렬화는 canonical 이어야 한다. 양측이 같은 schema 에 서 byte-identical 텍스트를 생산해야 한다. 그렇지 않으면 모든 checksum 비교가 거짓으로 drift 를 표시할 것이다. 직렬화기가 명시적으로 whitespace, attribute 순서 (storage-order 가 아닌 attribute-order 사용), quoting 을 normalise 한다.
열린 질문
섹션 제목: “열린 질문”- chunk 당 lock backoff. timeout 된 chunk 가 실패로 기록된다 — loop 안 backoff 와 함께 retry 가 없다는 뜻이다. 오래 걸리는 application 트랜잭션이 따라서 실행 안에서 다시 시도되지 않는 chunk grid 의 gap 을 남길 수 있다.
- MVCC 스냅샷 bracketing.
checksumdb가 chunk 너머로 스냅샷 을 pin 하지 않는다 — 각 chunk 가 자기 MVCC 스냅샷에서 read 한다. 실행 동안 chunk 사이를 옮기는 행이 빠지거나 (둘 어느 chunk 에도 안 카운트) 이중 카운트 될 수 있다는 뜻이다. 실제 divergence 감지가 따라서 동시 쓰기로부터의 작은 false-positive 비율을 견딘다 — 운영자는 보통checksumdb를 quiesced 또는 read-mostly window 에 돌린다. - Cross-version 호환성. 직렬화된 schema 형식은 schema 의
repid에 묶여 있다 — schema 변경이 진행 중인 cross-version 복제는 의미 없는 diff 를 생산할 수 있다. 해결책은 master 와 slave 가 같은repid일 때만checksumdb를 돌리는 것이다. - ECC-style 자동 수리 없음.
checksumdb가 차이를 보고하지만 reconcile 하지는 않는다. 수리는 수동 개입 또는 새 master backup 에서 slave 를 re-baseline 해야 한다는 점이다.
Sources
섹션 제목: “Sources”src/executables/checksumdb.c— 전체 utility (단일 파일, ~2400 줄)src/executables/AGENTS.md— agent 가이드- 인접 문서:
cubrid-ha-replication.md(checksumdb 가 의존하는 복제 채널),cubrid-cdc.md(변경 capture 를 위한 CDC 대안; checksumdb 가 HA 를 검증, CDC 가 변경을 publish),cubrid-cub-admin.md§데이터베이스-admin verb (checksumdb 는 통합cubridadmin 진입의 CS_ONLY verb),cubrid-master-process.md(cub_master 는 연결 broker 로만 관여; checksumdb 자체는libcubridcs에서 돈다)