(KO) CUBRID 쓰기 경로 — INSERT와 COMMIT의 끝까지
목차
- 이 문서가 따라가는 경로
- 1단계 — 클라이언트에서 서버로 (간략히)
- 2단계 — 파싱 + 의미 검사 + 구문 디스패치
- 3단계 — Locator의 force 패밀리
- 4단계 — Heap 삽입
- 5단계 — 인덱스 갱신
- 6단계 — 제약 및 FK 검사
- 7단계 — 트리거 실행 (있는 경우)
- 8단계 — prior 리스트를 통한 로그 레코드
- 9단계 — 복제 레코드 (HA인 경우)
- 10단계 — 락 획득
- 11단계 — COMMIT 구문
- 12단계 — 내구성
- 13단계 — 이후: 더티 페이지 flush + DWB
- 14단계 — 한참 뒤: MVCC vacuum이 죽은 버전을 거둔다
- 다이어그램 — 전체 파이프라인
- 다루지 않은 내용
- 출처
이 문서가 따라가는 경로
섹션 제목: “이 문서가 따라가는 경로”클라이언트 커넥션 하나가 INSERT INTO t VALUES (...) 를 보내고 곧이어 COMMIT 을 보낸다. 이 문서는 그 두 구문이 어떻게 안정 저장소의 세 표적 (활성 로그 볼륨, heap 데이터 볼륨, overflow 파일) 에 바이트로 자리 잡는지를 추적한다. 이른바 내구 경로 (durable path) 다. 클라이언트가 커밋 응답을 받는 순간이 엔진이 던지는 약속 이다. 이후에 어떤 크래시가 와도 그 행은 살아남는다는 약속이다.
추적은 약 13개의 세부 문서를 순서대로 엮는다. 파서, 의미 검사, 구문 디스패치, locator _force fan-in, heap 관리자, overflow 파일, B+Tree, MVCC, 트리거, prior 리스트, 로그 관리자, 락 관리자, HA 복제, 트랜잭션 상태 머신, 페이지 버퍼, DWB, 체크포인트, vacuum이 그 순서다. 종착점은 “COMMIT이 반환된 시점”이 아니다. 결국 일어나는 페이지 flush와, 그보다 한참 뒤에 일어나는 vacuum 회수까지가 끝이다. 쓰기를 “완료”라고 부르려면 세 가지가 동시에 충족되어야 한다. LOG_COMMIT 레코드가 안정 저장소 위에 있어야 하고, 모든 격리 수준에서 모든 관련 인덱스로 행에 닿을 수 있어야 하며, 더티 heap·btree 페이지가 flush되었거나 torn-write 방어가 그것을 덮고 있어야 한다.
1단계 — 클라이언트에서 서버로 (간략히)
섹션 제목: “1단계 — 클라이언트에서 서버로 (간략히)”SELECT 경로와 같다. 애플리케이션이 db_* API(CCI, ODBC, JDBC, 또는 CAS 브로커 shim)를 호출하면, 브로커가 SQL 텍스트와 bind 파라미터를 네트워크 프로토콜로 서버 요청 디스패처에 흘려보낸다. 워커 스레드 하나가 그것을 집어 든다. 클라이언트 측 워크스페이스(work_space.c, locator_cl.c)는 나중에 영구 OID를 받게 될 MOP들을 들고 있다. cubrid-rpath-select.md의 1~3단계를 참고하라.
INSERT 에만 해당하는 사항이 하나 있다. 워크스페이스는 새 MOP 을 LC_FLUSH_INSERT 로 표시해 두고, 최종 flush 시점에 그것을 LC_COPYAREA 로 묶어 보낸다. 그러나 이 문서가 따라가는 실행기 구동 경로, 즉 INSERT ... VALUES (...) 를 단일 구문 안에서 파싱하고 실행하는 경우에는 워크스페이스 flush 가 통째로 우회된다. 실행기가 서버 위에서 직접 locator_attribute_info_force 를 호출하며, attribute-info 번들은 그 자리에서 만들어 넘긴다.
2단계 — 파싱 + 의미 검사 + 구문 디스패치
섹션 제목: “2단계 — 파싱 + 의미 검사 + 구문 디스패치”서버 측 파서 단계는 SELECT와 공유된다. csql_grammar.y의 GLR Bison 문법이 INSERT INTO t VALUES (...) 텍스트를 PT_INSERT 노드로 환원한다. 이 노드는 PT_NODE의 한 갈래다. node_type 필드가 어느 종류인지를 가리는 태그 역할을 하고, info.insert 팔(arm)이 대상 클래스, 값 목록, 선택적 ON DUPLICATE KEY UPDATE 절을 담는다. 렉서는 Flex DFA이며 start-condition 상태를 활용한다. 파서는 %glr-parser로 빌드되어 있어 SQL의 역사적 모호성을 흡수할 수 있다.
이어서 의미 검사(cubrid-semantic-check.md)가 돈다. 이름 해결 단계에서 t가 자기 클래스 OID에 묶이고, 값 목록의 타입이 타입 강제 규칙을 따라 테이블 컬럼 타입과 맞춰진다. 사용자가 지정하지 않은 DEFAULT 값도 이 시점에 채워 넣는다. 파스 트리 모양은 cubrid-parser.md를, 이름 해결과 타입 검사 흐름은 cubrid-semantic-check.md를 참고하라.
의미 검사를 통과한 구문은 디스패치 단계로 넘어간다. 진입점은 src/query/execute_statement.c의 do_statement 단 하나다. 이 함수는 DDL 디스패처도 겸한다. PT_NODE.node_type을 두고 커다란 switch 문이 펼쳐지며, 종류별 핸들러로 흘려보낸다. PT_INSERT에 대한 핸들러는 do_insert다. 준비된 구문(prepared statement) 형태라면 do_execute_statement가 같은 switch를 다시 들어간다. do_insert는 값 목록을 위한 XASL 조각을 만들어 실행기에 태워 돌리고, 만들어진 행마다 locator의 force 패밀리를 호출한다. switch 구조는 cubrid-ddl-execution.md §최상위 디스패치 — do_statement와 DDL switch를 참고하라. DML 팔과 DDL 팔이 같은 switch 안에 나란히 앉아 있다.
3단계 — Locator의 force 패밀리
섹션 제목: “3단계 — Locator의 force 패밀리”locator_attribute_info_force(locator_sr.c)는 서버 측에서 일어나는 모든 행 변경의 공식 fan-in 진입점이다. 본체는 LC_COPYAREA_OPERATION을 두고 도는 switch (operation)이다. INSERT에서는 LC_FLUSH_INSERT 팔이 HEAP_CACHE_ATTRINFO로부터 locator_allocate_copy_area_by_attr_info를 거쳐 RECDES를 만들어 내고, locator_insert_force로 디스패치한다. UPDATE는 기존 레코드를 읽은 뒤 같은 인코딩 단계로 흘러든다. INSERT는 기존 버전이 없으므로 스냅샷 기반 읽기를 건너뛴다.
locator_insert_force 는 여섯 가지 책임을 순서대로 짊어진다. (1) 실제 파티션 클래스를 골라 잡는 파티션 프루닝, (2) OID 를 결정하는 heap_insert_logical 을 거친 heap 삽입, (3) 클래스에 달린 모든 B+Tree 를 건드리는 locator_add_or_remove_index 의 인덱스 루프, (4) locator_check_foreign_key 를 통한 FK 검사다. 부수 효과로 (5) repl_log_insert 를 통한 HA 복제 기록과 (6) heap · btree 프리미티브가 자기 안에서 부르는 log_append_* 호출을 통한 WAL 기록이 따라온다. cubrid-locator.md §‘force’ 패밀리 를 참고하라.
4단계 — Heap 삽입
섹션 제목: “4단계 — Heap 삽입”heap_insert_logical (heap_file.c) 은 slotted-page 쪽 코드다. (a) 레코드의 MVCC 헤더에 스탬프를 찍는다. mvcc_ins_id 에는 트랜잭션의 MVCCID 가 들어간다. 이 MVCCID 는 첫 쓰기라면 mvcctable::get_new_mvccid 를 거쳐 그 자리에서 게으르게 받아낸다 (cubrid-mvcc.md §MVCCID 할당 정책 참고). 갓 만들어진 INSERT 의 mvcc_rec_header 플래그 바이트에는 VALID_INSID 하나만 들어간다.
(b) 레코드가 어떤 heap 페이지에도 다 안 들어갈 만큼 크면, 행 본문은 overflow 파일로 빠져나간다. heap_ovf_insert → overflow_insert (cubrid-overflow-file.md) 가 호출되어 OVERFLOW_FIRST_PART + OVERFLOW_REST_PART 페이지의 사슬로 펼쳐 저장한다. 이 작업은 log_sysop_start / log_sysop_attach_to_outer 괄호 안에서 돈다. heap 홈 슬롯에는 REC_BIGONE 전달 (forwarding) 레코드만 남는다. overflow 페이지 하나하나는 RVOVF_NEWPAGE_INSERT redo 레코드를 뱉어내고, 헤드 페이지 위의 LOG_DUMMY_OVF_RECORD 는 HA 복제와 vacuum 이 잡고 갈 LSN 앵커 노릇을 한다.
(c) 그렇지 않으면 레코드는 REC_HOME 이다. heap 관리자는 HEAP_STATS_BESTSPACE_CACHE 로 대상 홈 페이지를 먼저 찾고, 못 찾으면 HEAP_HDR_STATS.estimates.best[], 그래도 안 되면 제한된 스캔, 마지막 수단으로 heap_alloc_new_page 까지 차례대로 내려간다. (d) 슬롯 하나가 잡힌다. 슬롯 id 와 (volid, pageid) 의 조합이 그 행의 영구 OID 가 된다. FILE_HEAP 페이지는 ANCHORED_DONT_REUSE_SLOTS 이므로 OID 가 같은 자리를 두 번 가리키는 일은 없다. (e) 페이지별 통계와 bestspace 캐시가 갱신되고, 페이지는 더티가 된다. cubrid-heap-manager.md §삽입 흐름 을 참고하라.
5단계 — 인덱스 갱신
섹션 제목: “5단계 — 인덱스 갱신”클래스에 달린 B+Tree마다, locator의 locator_add_or_remove_index가 새 레코드에서 키 컬럼을 heap_attrvalue_get_key로 뽑아내어 (key, OID) 쌍을 들고 btree_insert를 부른다.
B+Tree 쪽은 latch-coupling 규율로 트리를 내려간다. 부모를 S 래치로 잡고, 자식을 잡은 뒤, 부모를 풀어 주는 동작을 끝까지 반복한다. 다만 쓰기 경로에서는 리프에 닿는 순간 X 래치로 끌어올린다. 리프가 꽉 차서 분할이 필요하면, btree_insert_helper가 log_sysop_start로 시스템 연산 괄호를 열고, btree_split_node(루트 높이가 늘어나는 분할이라면 btree_split_root)를 호출해 분리자 키를 부모로 끌어올린 뒤, log_sysop_end_logical_undo로 닫는다. 그 덕에 abort 시 물리적 페이지 이동을 거꾸로 되돌리지 않고도, 논리적 undo를 다시 돌려 병합을 복원할 수 있다. cubrid-btree.md §분할 — 분할 지점, 키 승격, 부모 갱신을 참고하라.
리프 자체에는 LEAF_REC(고정 접두어 VPID ovfl + short key_len) 다음에 키 바이트, 그 뒤에 OID 목록이 자리한다. 비유일 키일 때 OID 목록은 페이지별 임계값까지 인라인으로 늘어난다. 임계값을 넘으면 BTID_INT::ovfid에서 따낸 키별 overflow 사슬로 흘러 넘친다. 이 사슬의 페이지는 PAGE_BTREE 타입이며, BTREE_OVERFLOW_HEADER가 헤더 자리에 있고 슬롯 0에는 next_vpid, 슬롯 1에는 이진 탐색이 가능하도록 OID 순으로 정렬된 OID 목록이 담긴다. 새 overflow 페이지는 사슬 머리에 끼워진다. 새 페이지의 next_vpid가 기존 첫 overflow 페이지를 가리키므로, 뒤따르는 삽입이 사슬 꼬리까지 걸어갈 비용을 치를 일이 없다. cubrid-overflow-file.md §탐색: B+Tree overflow OID 목록을 참고하라.
유일 키 검사는 삽입 시점에, 리프의 X 래치 아래에서 돌아간다. BTREE_OP_PURPOSE = INSERT로 btree_find_oid_and_its_page가 디스패치된다. 리프 레코드 suffix 목록의 OID들이 OID 순으로 정렬되어 있으므로, 유일 인덱스에서는 키 하나당 OID가 최대 하나다. 이미 그 키에 OID가 하나라도 박혀 있다면 그것이 곧 중복이다. 이 검사는 BTREE_NEED_UNIQUE_CHECK 매크로가 켜고 끈다. 활성 트랜잭션에서만 돌고, 복구 redo에서는 절대 돌지 않는다. 복구 시점에는 원래 삽입이 이미 한번 검증되었으므로 중복이 들어올 길이 없기 때문이다. cubrid-btree.md §유일 키 처리 — OID 목록과 통계를 참고하라.
6단계 — 제약 및 FK 검사
섹션 제목: “6단계 — 제약 및 FK 검사”heap과 인덱스 작업이 끝나면, locator_insert_force가 locator의 제약 조율 헬퍼들을 부른다. INSERT와 관련 있는 것은 셋이다.
locator_add_or_remove_index 는 5 단계에서 이미 한 번 부른 함수이지만, 그 자체가 유일 키 검사 루프이기도 하다. btree_insert 가 유일 B+Tree 에서 같은 키를 만나면 ER_BTREE_UNIQUE_FAILED 를 반환하고, locator 가 그 에러를 그대로 위로 흘려보낸다. locator_check_unique_btree_entries 는 더 깊은 무결성 검사로, CHECKDB 나 복원 직후의 일관성 점검에 쓰인다. 핫한 삽입 경로에서는 부르지 않는다. locator_check_foreign_key 는 클래스 representation 에 달린 FK 목록을 훑으면서 새 레코드에서 참조 컬럼의 키를 뽑아내고, btree_keyoid_checks 로 부모 클래스의 PK B+Tree 를 더듬는다. 못 찾으면 삽입은 ER_FK_INVALID 로 거절된다. cubrid-locator.md §제약 조율 을 참고하라.
순서에는 의도가 있다. heap 삽입을 가장 먼저 한다(OID를 못 박기 위해서). 그다음이 인덱스다(다른 FK가 가리킬 수도 있는 PK까지 모든 키를 채우기 위해서). 마지막이 FK 검사다. 같은 트랜잭션이 같은 배치 안에서 부모 행을 먼저 삽입했을 수 있기 때문이다. locator_check_foreign_key 안에서 부모 조회는 트랜잭션 자기 스냅샷으로 하는 평범한 btree fetch다. “내가 쓴 것은 내게 보인다”는 원칙 덕분에, 이 트랜잭션이 앞서 끼워 넣은 부모 행도 보인다. cubrid-mvcc.md의 mvcc_satisfies_snapshot 진리표 가운데 MVCC_IS_REC_INSERTED_BY_ME 팔을 참고하라.
7단계 — 트리거 실행 (있는 경우)
섹션 제목: “7단계 — 트리거 실행 (있는 경우)”대상 클래스에 BEFORE INSERT 나 AFTER INSERT 트리거가 정의되어 있다면, 이 트리거는 클라이언트 측에서 돈다. 서버가 아니다. 트리거 발화는 obt_apply_assignments(object_template.c) 안에서, 더티 MOP이 LC_COPYAREA로 묶이기 직전에 일어난다. 디스패치는 sm_active_triggers가 켜고 끈다. 트리거가 없는 경우는 O(1)에 곧장 빠져나간다.
트리거가 실재하면 흐름은 이렇다. tr_prepare_class가 TR_STATE를 짜 두고, BEFORE 패스가 tr_before_object → tr_execute_activities를 거쳐 돌아간다. 트리거마다 우선순위 순서로 eval_action이 호출된다. heap 변경이 일어난 뒤, tr_after_object가 AFTER 패스를 실행한다. DEFERRED 트리거는 트랜잭션별 tr_Deferred_activities 사슬에 줄을 선다.
재귀는 두 갈래로 묶인다. 깊이 카운터(tr_Current_depth ≤ 32)가 행 단위 무한 재귀를 잡는다. OID 스택(tr_Stack)은 STATEMENT 단위 트리거의 재진입을 조용히 흘려 보낸다. cubrid-trigger.md §발화 경로와 §재귀 제어를 참고하라.
이 문서가 따라가는 실행기 구동 삽입 경로에서는, 트리거가 DML 이 locator_attribute_info_force 에 닿기 전에 이미 돌아간다. 그러므로 4~6단계에 이를 즈음 트리거는 이미 셋 가운데 한 가지 상태다. 받아들였거나, ER_TR_REJECTED를 일으켜 구문 경계까지 롤백했거나, tr_Invalid_transaction = true로 트랜잭션을 무효화해 끝에 가서 COMMIT이 ABORT로 뒤집히게 만들어 놨거나. AFTER 트리거는 heap 변경 뒤에, 다음 차례로 tr_after_object에 들렀을 때 돈다. locator의 force 패밀리는 트리거를 전혀 모른다. cubrid-locator.md의 서버 측은 heap, lock, btree, FK, 로그, 복제까지만 다루고 트리거에는 손대지 않는다. 이는 trigger_manager.h의 #error Does not belong to server module 가드가 코드로 드러난 결과다.
8단계 — prior 리스트를 통한 로그 레코드
섹션 제목: “8단계 — prior 리스트를 통한 로그 레코드”4~5단계에서 일어나는 모든 페이지 변경은 WAL 추가 API(log_append_undoredo_data, log_append_redo_data, log_append_undo_data)를 부른다. MVCC 변형(LOG_MVCC_UNDOREDO_DATA 등)은 작성자의 MVCCID와 LOG_VACUUM_INFO를 함께 들고 다닌다. LOG_VACUUM_INFO의 prev_mvcc_op_log_lsa는 MVCC 연산들을 한 줄의 사슬로 엮는다. vacuum 서브시스템이 모든 레코드를 다시 읽지 않고도 그 사슬만 따라가면 된다.
이 호출들은 모두 prior_lsa_alloc_and_copy_data 와 _crumbs (log_append.cpp:273 / :410) 로 흘러든다. 이 함수는 글로벌 뮤텍스 바깥에서 LOG_PRIOR_NODE 를 malloc 하고, 페이로드가 log_Zip_min_size_to_compress 를 넘으면 zlib 로 압축해 둔 다음 노드를 돌려준다. 그다음 prior_lsa_next_record 가 prior_lsa_mutex 를 잡고 log_Gl.prior_info.prior_lsa 로 LSA 를 발행한다. 이어서 노드를 꼬리에 끼우고 list_size 를 올리고 락을 푼다. 뮤텍스가 잡혀 있는 구간은 O(1) 짜리 연결 조작과 LSN 산술뿐이다. 비싼 압축과 memcpy 는 모두 락 바깥에서 일어나므로 프로듀서 N 개가 나란히 빌드한다. cubrid-prior-list.md §프로듀서 2단계 를 참고하라.
드레인은 따로 돈다. log_Flush_daemon (그리고 역압을 받은 자구책) 이 LOG_CS_OWN_WRITE_MODE 안에서 logpb_prior_lsa_append_all_list 를 부른다. 이 함수는 뮤텍스 안쪽에서 prior 리스트를 통째로 떼어 낸다 (head / tail / size 를 NULL / NULL / 0 으로 갈아 끼운다). 그 뒤 락을 풀고, 떼어 낸 리스트를 logpb_append_next_record 로 훑어 가며 노드 하나하나의 바이트를 권위 있는 LOG_PAGE 버퍼로 옮긴다. 디스크 쓰기는 또 다른 단계로, 12 단계에서 다룬다.
인덱스 두 개가 걸린 클래스에 INSERT 한 건만 들어간다고 치면, prior 리스트로 들어오는 것은 LOG_MVCC_UNDOREDO(heap_insert_logical에서) 하나, LOG_UNDOREDO_DATA(btree_insert × 2에서) 두 개, 그리고 행이 overflow까지 흘러갔다면 LOG_DUMMY_OVF_RECORD 하나다. 각 레코드는 LOG_RECORD_HEADER { prev_tranlsa, back_lsa, forw_lsa, trid, type }을 머리에 단다. ARIES가 요구하는 3중 LSA 구조다. prev_tranlsa는 같은 트랜잭션의 레코드들을 묶어 undo가 거꾸로 걸어갈 길을 깔고, back_lsa/forw_lsa는 물리적 로그 순서로 레코드들을 묶어 redo가 앞으로 훑어갈 길을 깐다.
9단계 — 복제 레코드 (HA인 경우)
섹션 제목: “9단계 — 복제 레코드 (HA인 경우)”서버가 HA 마스터로 구성되어 있다면, WAL 레코드를 뱉어낸 바로 그 locator_*_force 흐름이 repl_log_insert (replication.c) 도 함께 부른다. 이 함수는 트랜잭션별 스테이징 배열 tdes->repl_records[]에 LOG_REPL_RECORD를 끼운다. 스테이징 항목은 일부러 깡마르게 짰다. repl_data 페이로드의 모양은 | packed_pkey_size | class_name | pkey_dbvalue |. 클래스 이름과 기본 키 값이 전부고, 행 전체 이미지는 들어가지 않는다. 슬레이브는 이벤트를 적용할 때 마스터의 heap에서 행을 다시 끌어다 쓴다. 덕분에 수백만 행을 건드리는 배치 삽입에서도 트랜잭션별 스테이징 비용이 한정된 수준에 머문다.
실제 LOG_REPLICATION_DATA 로그 레코드는 이 시점에 추가되지 않는다. 항목들은 커밋 시점까지 tdes->repl_records[]에 머문다. cubrid-ha-replication.md §마스터 측 — LOG_REPL_RECORD와 스테이징 배열을 참고하라. CDC 채널은 따로 채워진다. 모든 DML은 WAL과 함께 인라인으로 log_append_supplemental_*를 거쳐 LOG_SUPPLEMENTAL_INFO 레코드(레코드 타입 52)를 뱉어낸다. 이 레코드는 자기를 스스로 설명하는 풍부한 페이로드(테이블 OID, before/after 이미지, 트랜잭션 사용자)를 담고 있다. 외부 pull 방식 컨슈머가 카탈로그를 들춰 보지 않아도 디코딩할 수 있도록 한 장치다. cubrid-cdc.md §LOG_SUPPLEMENTAL_INFO — 현대 이벤트 포맷과 cubrid-log-manager.md §LOG_SUPPLEMENTAL_INFO는 CDC가 쓰는 채널을 참고하라.
10단계 — 락 획득
섹션 제목: “10단계 — 락 획득”락은 locator 경로를 따라 흐른다. INSERT의 경우 행의 OID는 슬롯이 잡히는 그 시점에 heap_insert_logical 안에서 결정된다. 그래서 X-락도 그 위쪽이 아니라 heap 경로 안쪽에서 잡힌다. INSERT는 행 락을 heap 프리미티브 안에서 잡는, 손에 꼽는 연산 가운데 하나다. 락 관리자의 공개 진입점은 lock_object(lock_manager.c:5945)이며, 해시 → 리소스 → 호환성 검사 시퀀스는 lock_internal_perform_lock_object에 맡긴다.
lock_object는 LK_RES_KEY{type=INSTANCE, oid, class_oid}로 키를 잡아 LK_RES를 찾거나 새로 만든다. 그다음은 셋 중 하나다. 비어 있는 자리에 곧장 부여하거나, total_holders_mode | total_waiters_mode와 호환되면 보유자 목록 끝에 붙여 부여하거나, 호환이 안 되면 대기자 목록에 끼워 잠재운다. 호환성 검사는 집계된 모드 비트를 두고 돌리는 O(1)짜리 행렬 조회다. CUBRID의 12개 모드(NA … SCH-M) 가운데, 부모 클래스에는 IX가(실행기가 위쪽에서 미리 잡아 둔다), 행 OID에는 X가 붙는다. 새 OID에는 앞서 잡고 있는 자가 없으니 LK_RES는 새것이고 부여는 그대로 통과한다.
인덱스 키 락은 인라인 OID 목록 위에는 걸지 않는다. CUBRID는 비SERIALIZABLE 격리 수준에서는 MVCC와 행 OID 락에 기댄다. SERIALIZABLE에서는 스캔 경계에서 키 범위 락이 걸린다. READ COMMITTED에서는 인스턴스 락이 짧게 머문다(구문이 끝날 때 lock_unlock_object_by_isolation이 풀어 준다). REPEATABLE READ와 SERIALIZABLE에서는 길게 머문다. cubrid-lock-manager.md §락 획득 흐름을 참고하라.
11단계 — COMMIT 구문
섹션 제목: “11단계 — COMMIT 구문”클라이언트가 COMMIT을 보낸다. xtran_server_commit(transaction_sr.c:71)이 그것을 log_commit(log_manager.c:5352)으로 넘기고, 다시 log_commit_local로 위임한다.
- 커밋 측 트리거.
tr_check_commit_triggers가 사용자 트리거의TR_EVENT_COMMIT을 돌리고, 트랜잭션별tr_Deferred_activities큐를 비운다. 지연된 액션이tr_Invalid_transaction을 일으키면 커밋이 abort로 전환된다 (ER_TR_TRANSACTION_INVALIDATED). - postpone 비우기.
LOG_POSTPONE레코드가 버퍼에 쌓여 있다면LOG_COMMIT_WITH_POSTPONE을 추가한 뒤log_do_postpone이 그것들을 다시 돌린다. 상태는TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE으로 옮겨간다. - 원자적 repl 과 커밋 발행.
log_append_repl_info_and_commit_log가prior_lsa_mutex를 한 번만 잡고,tdes->repl_records[]의 모든 항목을LOG_REPLICATION_DATA(또는_STATEMENT) 레코드로 커밋 레코드와 한 묶음으로 추가한다. 어떤 동료 트랜잭션의 커밋도 그 사이에 끼어들 수 없다.cubrid-ha-replication.md§원자적 발행 을 참고하라. LOG_COMMIT추가. 커밋 레코드의 LSA (commit_lsa) 가 트랜잭션의 약속을 가리키는 손잡이가 된다.- 내구성 대기.
logpb_flush_pages(commit_lsa)가gc_cond위에서 커미터를 잠재운다 (기본 설정은async_commit=false, group_commit=true). 로그 flush 데몬이log_get_log_group_commit_interval마다 한 번 틱하고 브로드캐스트를 던지면,nxio_lsa >= commit_lsa가 되는 순간 커미터가 깨어난다.cubrid-prior-list.md§커밋 대기자 를 참고하라. - 상태 전환과 해제. TDES 상태가
TRAN_UNACTIVE_COMMITTED로 넘어간다.logtb_complete_mvcc가 활성 집합에서 비트를 뒤집는다. 락이 풀린다 (retain_lock이 켜져 있으면 그대로 들고 있다). 마지막으로logtb_release_tran_index가 trantable 인덱스를 돌려준다.cubrid-transaction.md를 참고하라.
12단계 — 내구성
섹션 제목: “12단계 — 내구성”log_Flush_daemon(log_manager.c::log_flush_execute)이 바이트를 디스크 위에 올린다. 틱마다(타이머 또는 on-demand 깨우기) 다음을 돌린다.
// log_flush_execute — log_manager.c (condensed)LOG_CS_ENTER (&thread_ref);logpb_flush_pages_direct (&thread_ref); // → logpb_prior_lsa_append_all_list (prior 리스트 드레인 → LOG_PAGE 버퍼) // → logpb_flush_all_append_pages (LOG_PAGE → 활성 로그 + fsync)LOG_CS_EXIT (&thread_ref);pthread_cond_broadcast (&log_Gl.group_commit_info.gc_cond);logpb_flush_all_append_pages는 더티 LOG_PAGE 목록을 훑으면서, 활성 로그 볼륨에 fileio_write_pages를 던지고 log_append_info::nxio_lsa를 끌어올린다. 이 값은 안정 저장소에 아직 닿지 못한 가장 낮은 LSA다.
부분 레코드를 두 단계로 흘려보내는 장치도 있다. 가장 최근 레코드의 헤더가 들어 있는 페이지를 빼고 모든 것을 먼저 쓰고, 그 헤더 페이지는 맨 마지막에 쓴다. 덕분에 flush 도중에 크래시가 나도 쓰기는 탄력 있다. 디스크 위의 로그는 언제 봐도 옛날 end-of-log 마커나 새 마커, 둘 중 하나에서 끊긴다. 허공으로 뻗은 전방 포인터는 절대 남지 않는다. cubrid-log-manager.md §Flush를 참고하라.
데몬의 브로드캐스트가 떨어지면, commit_lsa <= nxio_lsa인 모든 커미터가 깨어나 워터마크를 확인하고 클라이언트에 응답을 돌려준다. 바로 이 순간이 내구성 약속이 굳어지는 시점이다. log_commit이 TRAN_UNACTIVE_COMMITTED를 반환하면, 그 이후 어떤 크래시가 닥쳐도 모든 인덱스와 모든 제약으로 행에 닿을 수 있다.
이 시점에 아직 디스크 위에 없는 것은 따로 있다. heap 페이지에 손댄 슬롯, btree 리프의 새 항목, overflow 사슬이 그것이다. 내구한 것은 WAL뿐이다. 데이터 페이지는 뒤이어 따라온다.
13단계 — 이후: 더티 페이지 flush + DWB
섹션 제목: “13단계 — 이후: 더티 페이지 flush + DWB”더티 heap 페이지와 btree 페이지는 페이지 버퍼의 세 데몬이 게으르게 흘려보낸다 (cubrid-page-buffer-manager.md 참고). Page Flush Daemon 은 더티 BCB 를 골라 더티 비율에 맞춰 조절된 속도로 쓴다. Page Post-Flush Daemon 은 flush 가 끝난 BCB 를 후처리해 직접 victim 을 기다리던 자에게 넘긴다. Page Maintenance Daemon 은 100 ms 마다 프라이빗 LRU 별 쿼터를 손본다.
더티 데이터 페이지 쓰기는 모두 torn write 방어를 위해 double-write buffer (DWB) 를 거친다 (cubrid-double-write-buffer.md). 프로듀서 측에서는 dwb_acquire_next_slot 이 CAS 로 위치 카운터를 끌어올려 인메모리 DWB 블록의 슬롯을 챙긴다. dwb_set_data_on_next_slot 이 페이지 바이트를 거기에 옮겨 담는다. 페이지는 dwb_Global.slots_hashmap 에 등록된다. 이 등록 덕분에 동시 읽기자가 dwb_read_page 를 거쳐 찢어졌을 수도 있는 홈 페이지 대신 이쪽에서 페이지를 끌어다 쓸 수 있다. 블록이 가득 차면 dwb-flush-block 데몬이 DWB 볼륨에 블록을 순차적으로 쓰고 fsync 한 뒤, 각 슬롯의 내용을 자기 홈 볼륨으로 옮긴다.
WAL 불변식과의 맞물림도 있다. 어떤 데이터 페이지든 홈으로 쓰기 전에, pgbuf_flush_check_log_lsa가 nxio_lsa >= page->lsa를 보장한다. 12단계의 커밋 강제 flush가 이 INSERT의 페이지를 그 조건을 이미 채워 놓았으므로, 이제부터 그 페이지들의 flush는 무조건 통과다.
다음 체크포인트가 그 flush 가 일어났음을 기록한다. logpb_checkpoint의 pgbuf_flush_checkpoint(newchkpt_lsa, ...)가 tmp_chkpt.redo_lsa로 남아 있는 가장 작은 oldest_unflush_lsa를 돌려준다. 이 값이 복구 앵커를 앞으로 밀어낸다. 이어 체크포인트는 trantable을 돌면서 활성 트랜잭션 스냅샷을 LOG_REC_CHKPT에 묶어 넣고, LOG_END_CHKPT를 발행해 fsync를 부른 뒤, 활성 로그 헤더의 log_Gl.hdr.chkpt_lsa를 갱신한다. cubrid-checkpoint.md §최상위 흐름을 참고하라. 체크포인트가 끝난 뒤 다음 더티 페이지 flush가 일어나기 전에 크래시가 나면, 새 redo-LSA 아래의 redo 레코드만 다시 돌리면 된다.
14단계 — 한참 뒤: MVCC vacuum이 죽은 버전을 거둔다
섹션 제목: “14단계 — 한참 뒤: MVCC vacuum이 죽은 버전을 거둔다”INSERT 하나만 놓고 보면 vacuum이 할 일은 거의 없다. 거둘 이전 버전이 없기 때문이다. 그래도 MVCC 기계의 마지막 고리를 닫기 위해 끝까지 따라가 보자. 가장 오래된 활성 스냅샷이 우리 커밋의 MVCCID를 지나가 버린 순간, 그 행은 누구에게나 보이는 상태가 된다. 트랜잭션이 남긴 LOG_MVCC_UNDOREDO 레코드는 그래도 vacuum의 블록 단위 스캐너에는 잡힌다.
vacuum_consume_buffer_log_blocks 가 로그를 31 개 로그 페이지짜리 블록 (VACUUM_LOG_BLOCK_PAGES_DEFAULT) 으로 끊어 가며 앞으로 쓸어 나간다. 우리 레코드의 MVCCID 는 그 블록의 newest_mvccid 에 보태진다. 전역 oldest_visible_mvccid 가 newest_mvccid 를 넘어서면 그 블록이 디스패치 가능 상태가 된다. vacuum_master_task 가 자기 vacuum_job_cursor 로 그것을 집어 들어 CAS 로 AVAILABLE → IN_PROGRESS 로 뒤집은 뒤, 최대 50 개 풀의 vacuum_worker 한 명에게 넘긴다.
워커는 LOG_VACUUM_INFO::prev_mvcc_op_log_lsa를 따라 블록의 MVCC 사슬을 거꾸로 걷는다. undo 이미지를 스레드별 log_zip_p로 풀어내고, 후보마다 VACUUM_HEAP_OBJECT { vfid, oid }를 쌓고, 상태를 EXECUTE로 옮긴 뒤, 프라이빗 LRU를 거쳐 대상 페이지를 잡고 죽은 버전을 떼어낸다. 우리 INSERT에서 워커가 하는 일은 “지나치기, 살아 있음” 한 줄이다. 알맹이 있는 작업은 나중에 이 행을 건드리는 DELETE/UPDATE에서 일어난다. 클래스가 이미 사라진 경우는 vacuum_is_file_dropped가 빨리 빠져나가게 한다. 성공하면 블록은 VACUUMED로 넘어간다. 도중에 끊기면 INTERRUPTED + AVAILABLE로 되돌려 마스터가 다시 디스패치한다. cubrid-vacuum.md §워커를 참고하라.
다이어그램 — 전체 파이프라인
섹션 제목: “다이어그램 — 전체 파이프라인”flowchart TB
CLIENT["클라이언트: INSERT INTO t VALUES (...);<br/>이후 COMMIT"]
CLIENT -->|"네트워크 프로토콜<br/>(cubrid-rpath-select.md 1-3단계)"| PARSE
subgraph SERVER["cub_server 워커 스레드"]
direction TB
PARSE["파싱<br/>(cubrid-parser.md)<br/>PT_INSERT 노드"]
SEM["의미 검사<br/>(cubrid-semantic-check.md)<br/>이름 해결 + 타입 통일"]
DISP["do_statement → do_insert<br/>(cubrid-ddl-execution.md)"]
LOC["locator_attribute_info_force<br/>switch (LC_FLUSH_INSERT)<br/>(cubrid-locator.md)"]
INS["locator_insert_force"]
HI["heap_insert_logical<br/>(cubrid-heap-manager.md)"]
HSTAMP["MVCC 스탬프<br/>mvcc_ins_id<br/>(cubrid-mvcc.md)"]
OVF["heap_ovf_insert → overflow_insert<br/>(cubrid-overflow-file.md)<br/>레코드 > 페이지인 경우만"]
BTI["btree_insert × N 인덱스<br/>(cubrid-btree.md)<br/>latch-coupling, 유일 검사"]
CONS["locator_check_unique_btree_entries<br/>locator_check_foreign_key<br/>(cubrid-locator.md)"]
LK["lock_object<br/>행 OID에 X<br/>(cubrid-lock-manager.md)"]
REPL["repl_log_insert<br/>tdes->repl_records[]<br/>(cubrid-ha-replication.md)"]
SUP["log_append_supplemental_∗<br/>(cubrid-cdc.md)"]
end
CLIENT_TR["BEFORE/AFTER 트리거<br/>(클라이언트 측, cubrid-trigger.md)<br/>obt_apply_assignments"]
CLIENT -. "클래스에 트리거가 있으면" .-> CLIENT_TR
CLIENT_TR -.-> DISP
PARSE --> SEM --> DISP --> LOC --> INS
INS --> HI --> HSTAMP
INS --> BTI
HI --> OVF
HI -.OID 할당.-> LK
INS --> CONS
INS --> REPL
INS --> SUP
subgraph WAL["페이지 변경마다 → WAL"]
direction TB
APP["log_append_undoredo_data<br/>log_append_redo_data<br/>(cubrid-log-manager.md)"]
PRA["prior_lsa_alloc_and_copy_data<br/>뮤텍스 밖에서 노드 malloc, zlib<br/>(cubrid-prior-list.md)"]
PRN["prior_lsa_next_record<br/>LSN 할당, 꼬리에 연결<br/>prior_lsa_mutex 아래"]
PL["prior_list<br/>단방향 연결 큐"]
APP --> PRA --> PRN --> PL
end
HI --> APP
BTI --> APP
OVF --> APP
CLIENT -->|"두 번째 구문: COMMIT"| COMMIT
COMMIT["log_commit_local<br/>(cubrid-transaction.md, cubrid-log-manager.md)"]
COMMIT --> TRDC["tr_check_commit_triggers<br/>tr_Deferred_activities 비우기<br/>(cubrid-trigger.md)"]
COMMIT --> POST["LOG_POSTPONE 있으면 다시 돌리기"]
COMMIT --> RPC["log_append_repl_info_and_commit_log<br/>tdes->repl_records[] flush<br/>· LOG_COMMIT 추가<br/>prior_lsa_mutex 한 번만 보유"]
RPC --> PRA
COMMIT --> WAIT["logpb_flush_pages(commit_lsa)<br/>gc_cond 위에서 timed-wait"]
subgraph DAEMON["log_Flush_daemon"]
DR["logpb_prior_lsa_append_all_list<br/>뮤텍스 안에서 prior 리스트 떼어내기<br/>노드를 LOG_PAGE 버퍼로 복사"]
FLU["logpb_flush_all_append_pages<br/>fileio_write_pages → 활성 로그 볼륨<br/>fsync; nxio_lsa 전진"]
BC["pthread_cond_broadcast(gc_cond)"]
DR --> FLU --> BC
end
PL --> DR
WAIT --> BC
BC -->|"nxio_lsa >= commit_lsa"| ACK["TRAN_UNACTIVE_COMMITTED 반환<br/>락 해제<br/>logtb_release_tran_index"]
ACK --> CLIENT_OK["클라이언트에 COMMIT 응답"]
subgraph LATER["이후 — 페이지 버퍼 flush 데몬들"]
direction TB
PFD["Page Flush Daemon<br/>(cubrid-page-buffer-manager.md)"]
PPF["Page Post-Flush Daemon"]
PMD["Page Maintenance Daemon<br/>100ms마다 쿼터 조정"]
DWB["dwb_acquire_next_slot<br/>dwb_add_page<br/>(cubrid-double-write-buffer.md)<br/>순차 쓰기 + fsync"]
HOME["fileio_write_pages → 홈 볼륨"]
PFD --> DWB --> HOME
PPF -.-> DWB
end
HI -. "더티 heap 페이지" .-> PFD
BTI -. "더티 btree 페이지" .-> PFD
subgraph CHK["주기적 — 로그 체크포인트 데몬"]
CHKD["logpb_checkpoint<br/>(cubrid-checkpoint.md)<br/>LOG_START_CHKPT → pgbuf_flush_checkpoint<br/>→ LOG_END_CHKPT → 로그 헤더 fsync"]
end
HOME -. "redo-LSA 전진" .-> CHKD
subgraph VACUUM["한참 뒤 — vacuum"]
VC["vacuum_consume_buffer_log_blocks<br/>(cubrid-vacuum.md)<br/>WAL을 31페이지 블록으로 끊기"]
VM["vacuum_master_task<br/>vacuum_Data 위 커서<br/>IN_PROGRESS 디스패치"]
VW["vacuum_worker × ≤ 50<br/>MVCC 사슬 거꾸로 탐색<br/>페이지 고정, 죽은 버전 떼어내기"]
VC --> VM --> VW
end
PL -. "LOG_MVCC_* 레코드" .-> VC
VW -. "다음 방문" .-> HOME
화살표 하나하나에는 그 메커니즘을 담당하는 세부 문서가 주석으로 달려 있다. 같은 실행 스레드를 공유하는 단계들(파싱 → 의미 검사 → 디스패치 → locator → heap → btree → WAL 추가)은 그림 위쪽으로 모인다. 내구성으로 넘어가는 전환(커밋 대기 → 데몬 flush → 브로드캐스트)이 클라이언트가 넘어가는 경계선이다. 그림 아래쪽 — 페이지 버퍼 flush, DWB, 체크포인트, vacuum — 은 클라이언트의 커밋 응답을 받은 뒤에 일어나는 일들이다.
다루지 않은 내용
섹션 제목: “다루지 않은 내용”위에서 따라간 것은 단일 행 · 단일 구문 · 단일 서버 INSERT 와 COMMIT 이다. 의도적으로 범위 바깥에 둔 인접 경로는 다음과 같다.
loaddb를 통한 대량 INSERT.BU클래스 락과xbtree_load_index를 거친 상향식 B+Tree 빌드다.cubrid-loaddb.md와cubrid-btree.md§대량 로드 를 참고하라.- DELETE 세부.
mvcc_del_id를 박아 두면 vacuum 이 나중에 거두어 간다.cubrid-heap-manager.md§삭제 흐름 을 참고하라. - UPDATE 세부. 기존 레코드 읽기 + 새 레코드 인코딩 +
att_id[]로 걸러낸 차분 기반 인덱스 갱신이다. 행이 자리를 옮기거나 overflow 로 빠지기도 한다.cubrid-locator.md§locator_update_force와cubrid-heap-manager.md§갱신 흐름 을 참고하라. - 트리거 내부. ECA 모델, 액션
PT_NODE지연 컴파일, 재귀 카운터와 OID 스택, 지연된 드레인이다.cubrid-trigger.md를 참고하라. - 교착 상태 감지. 충돌이 나면
LK_WFG_EDGE가 그어지고,lock_detect_local_deadlock이 가장 늦게 막힌 트랜잭션을 abort시킨다.cubrid-lock-manager.md§교착 상태 감지 를 참고하라. - 2-phase commit (서버 사이 XA).
LOG_2PC_*레코드,LOG_TDES::coord/gtrinfo,TRAN_STATE위에 얹힌 별도 상태 머신이다.cubrid-2pc.md를 참고하라. - 복제 적용 / CDC 컨슈머. 슬레이브 쪽
applylogdb/la_apply_log_file과 pull 방식cdc_make_loginfo다.cubrid-ha-replication.md와cubrid-cdc.md를 참고하라. - 크래시 복구.
log_Gl.hdr.chkpt_lsa에 닻을 내린 3 패스 ARIES (분석 / redo / undo) 다.cubrid-recovery-manager.md와cubrid-checkpoint.md§복구 통합 을 참고하라.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/parser/csql_grammar.y,parse_tree.h—PT_INSERT.src/query/execute_statement.c—do_statementswitch,do_insert,do_execute_statement.src/transaction/locator_sr.c—locator_attribute_info_force,locator_insert_force,locator_add_or_remove_index,locator_check_foreign_key.src/storage/heap_file.c—heap_insert_logical,heap_ovf_insert,heap_set_mvcc_rec_header_on_overflow.src/storage/btree.c,btree_load.c—btree_insert,btree_split_node,btree_find_oid_and_its_page,btree_start_overflow_page.src/storage/overflow_file.c—overflow_insert,RVOVF_NEWPAGE_INSERT.src/transaction/mvcc_table.cpp—mvcctable::get_new_mvccid,complete_mvcc.src/object/trigger_manager.c—tr_prepare_class,tr_before_object,tr_after_object,tr_check_commit_triggers.src/transaction/replication.c—repl_log_insert,repl_add_update_lsa.src/transaction/log_manager.c,log_append.cpp,log_page_buffer.c—log_append_*,prior_lsa_alloc_and_copy_data,prior_lsa_next_record,logpb_prior_lsa_append_all_list,logpb_flush_all_append_pages,log_flush_execute,log_commit,log_append_repl_info_and_commit_log.src/transaction/transaction_sr.c,log_tran_table.c—xtran_server_commit,logtb_release_tran_index,logtb_complete_mvcc.src/transaction/lock_manager.c—lock_object,lock_internal_perform_lock_object,lock_detect_local_deadlock.src/storage/page_buffer.c—pgbuf_flush_check_log_lsa,pgbuf_flush_victim_candidates, 세 flush 데몬.src/storage/double_write_buffer.cpp—dwb_acquire_next_slot,dwb_add_page,dwb_flush_block.src/query/vacuum.c—vacuum_consume_buffer_log_blocks,vacuum_master_task,vacuum_process_log_block.
형제 읽기 경로 문서
섹션 제목: “형제 읽기 경로 문서”cubrid-rpath-select.md— 읽기 경로. 1~3단계는 위에서 그대로 다시 쓴다.
이 종합 문서를 가로지르는 세부 문서들
섹션 제목: “이 종합 문서를 가로지르는 세부 문서들”cubrid-parser.md, cubrid-semantic-check.md,
cubrid-ddl-execution.md, cubrid-locator.md,
cubrid-heap-manager.md, cubrid-overflow-file.md,
cubrid-btree.md, cubrid-mvcc.md, cubrid-trigger.md,
cubrid-lock-manager.md, cubrid-prior-list.md,
cubrid-log-manager.md, cubrid-ha-replication.md,
cubrid-cdc.md, cubrid-transaction.md,
cubrid-page-buffer-manager.md, cubrid-double-write-buffer.md,
cubrid-checkpoint.md, cubrid-vacuum.md.