(KO) CUBRID LOB — 외부 파일 저장, locator 라이프사이클, 그리고 트랜잭션 정리
목차
학술적 배경
섹션 제목: “학술적 배경”Large Object (LOB) — BLOB 은 바이너리, CLOB 은 문자 — 는 컬 럼의 페이로드가 너무 커서 다른 컬럼들과 같은 행에 그대로 두면 저 장 전략이 망가지는 데이터 타입이다. 테이블은 한 페이지에 짧은 행 을 많이 담도록 튜닝된다. 4 번째 컬럼이 50 MB 이미지면 그 전제가 무너진다. 그래서 모든 관계형 엔진은 LOB 의 저장을 나머지 행 저장 에서 떼어 놓는다.
교재가 정리하는 설계 공간은 세 축으로 나뉜다.
- 바이트 페이로드를 어디에 둘지. 같은 heap 파일 안 (행 내 부 chunking + continuation pointer), 별도의 내부 LOB 파일, 아니면 호스트 파일시스템이나 오브젝트 스토어 같은 외부 저장 소.
- 행이 무엇을 들고 있을지. 직접 포인터, 간접 핸들 (locator), 아니면 둘 다. 작은 LOB 은 핸들로, 큰 LOB 은 간접으로 푸는 식.
- 저장 선택을 트랜잭션 의미와 어떻게 맞출지. INSERT 가 롤 백되면 LOB 파일 쓰기도 사라져야 한다. DELETE 가 commit 되면 바이트가 결국 회수되어야 한다. 그런데 그 바이트가 WAL 보호 를 받는 데이터 볼륨 바깥에 있다면, 엔진이 로그 undo / redo 로 파일을 정리할 수가 없다. 그래서 commit / abort 경로를 엔 진이 직접 가로채야 한다.
교재 출처는 Database Internals (Petrov, 2019) Ch. 2 “File Formats” 다. 페이지 밖 레코드 저장 부분이 이 주제를 다룬다. Database System Concepts (Silberschatz et al., 7e) §13.5 Record Organization 도 BLOB / CLOB 의미를 다룬다. 트랜잭션 안 전한 out-of-row LOB 관리는 Lehman 과 Lindsay 의 “The Starburst Long Field Manager” (VLDB 1989) 가 초기 표준 참 고문헌이다.
CUBRID 가 위 세 축에서 고른 답은 이렇다. 첫째 축에서는 데이터 볼륨 바깥의 파일시스템을 골랐다. 둘째 축에서는 행에 locator URI 를 두는 간접 방식을 골랐다. 셋째 축에서는 commit / abort 경로 에 트랜잭션 단위 추적 자료구조를 끼워 넣어 직접 처리한다. 이어 지는 두 절은 이 선택들이 왜 합리적인지, 그리고 CUBRID 가 어떻게 구현하는지를 풀어 본다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”out-of-row LOB 저장은 잘 알려진 패턴이다. 거의 모든 상용 엔진이 아래의 관행 가운데 일부를 채택한다. 그러므로 다음 절에서 보는 CUBRID 의 구체적 선택은 이 공유 설계 공간 위에서 어떤 다이얼을 어느 방향으로 돌렸는지로 읽으면 된다.
Locator 핸들, 원시 포인터가 아니다
섹션 제목: “Locator 핸들, 원시 포인터가 아니다”LOB 컬럼이 행에 담는 것은 바이트가 아니라 locator 다. 고정
길이 문자열이거나 작은 구조체다. locator 는 엔진이 필요할 때 바
이트를 찾아갈 정도의 정보만 담고, 행과 함께 페이지로 들어갈 만
큼 작다. PostgreSQL 의 lo_oid, Oracle 의 LOB locator, CUBRID
의 DB_ELO.locator URI 가 모두 같은 역할을 한다.
locator 가 보통 들고 다니는 정보는 셋이다. (a) 어느 백엔드 로 보낼지 (파일시스템, 오브젝트 스토어, 내부 저장소). (b) 그 백엔드 안에서의 주소 (경로, OID, 해시). (c) 역참조 없이도 locator 가 유효한지 가볍게 점검할 수 있게 해 주는 메타데이터 (크기, 타입, 소유 클래스). locator 자체는 SQL 계층을 불투 명하다.
백그라운드 회수, 동기 unlink 가 아니다
섹션 제목: “백그라운드 회수, 동기 unlink 가 아니다”DELETE 를 commit 하는 트랜잭션이 있다고 해 보자. LOB 파일을
행의 MVCC delete 비트가 켜지는 순간에 곧장 unlink 할 수는 없
다. 다른 스냅샷이 아직 옛 버전을 읽고 있을 수 있기 때문이다.
그래서 엔진은 unlink 를 유예 (defer) 한다. “이 locator 는
commit 시점에 지워야 한다” 라는 의도만 기록해 두고, 트랜잭션이
끝난 뒤에 실제 unlink 를 수행한다. 그 수행은 commit 직후 동기
일 수도 있고, vacuum 형 데몬으로 비동기일 수도 있다. 이 패
턴은 본질적으로 MVCC 의 dead-tuple 회수와 같다.
두 단계 해시 디렉토리
섹션 제목: “두 단계 해시 디렉토리”LOB 을 그냥 파일로 만들면 디렉토리 inode 한계 에 부딪힌다.
한 디렉토리가 수백만 개의 파일을 들고 있으면 커널의 readdir
도, 백업 도구도 함께 느려진다. 호스트 파일시스템을 쓰는 엔진들
은 이 문제에 똑같은 방식으로 대응한다. 파일 이름을 해시해서 한
단계 또는 두 단계의 중간 디렉토리에 흩어 두고, leaf 디렉토리당
파일 수가 일정 한도 안에 머물게 한다. PostgreSQL 의
pg_largeobject 는 내부 페이지를 쓰므로 이 문제가 없다. 호스
트 파일시스템으로 가는 쪽 (Oracle BFILE, file-per-LOB 확장을 쓴
MySQL, CUBRID 의 POSIX / OWFS 백엔드) 은 모두 해시 디렉토리 패
턴을 쓴다.
TDES 단위 locator 리스트로 트랜잭션 정리
섹션 제목: “TDES 단위 locator 리스트로 트랜잭션 정리”지연 정리를 하려면 어떤 locator 가 처리 대기 중인지를 어딘가에
적어 둬야 한다. 표준 답은 트랜잭션 디스크립터에 매단
(locator, 의도) 쌍의 리스트 (혹은 트리) 다. commit 가 그 리
스트를 걸으며 의도를 적용한다. rollback 도 같은 리스트를 걸으
며 반대 의도를 적용한다. 이 리스트는 휘발성 메모리에 산다. 복
구 도중에는 로그 레코드로부터 다시 만들 수 있지만, 정상 동작 중
에는 단순한 부기 자료구조다.
locator 별 savepoint 스택
섹션 제목: “locator 별 savepoint 스택”서브트랜잭션과 savepoint 가 한 가지 문제를 더 만든다. locator
를 savepoint A 에서 만들고, savepoint B 에서 변경하고, A 까지
만 부분 롤백할 수 있어야 한다. 교재의 구현은 각 locator 항목에
이전 상태들의 스택 을 매단다. 롤백 LSA 가 savepoint 경계를
넘을 때마다 스택을 하나씩 pop 한다. 이 부분을 명시적으로 문서화
한 시스템은 많지 않다. CUBRID 의 lob_savepoint_entry 는 구현
이 깔끔한 편에 든다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론 | CUBRID 명칭 |
|---|---|
| Locator (백엔드 안의 핸들) | DB_ELO.locator — <scheme>:<backend-specific> URI 문자열 (elo.c) |
| 백엔드 선택을 위한 prefix | ES_OWFS_PATH_PREFIX = "owfs:", ES_POSIX_PATH_PREFIX = "file:", ES_LOCAL_PATH_PREFIX = "local:" (es_common.h) |
| 저장 타입별 백엔드 | es_owfs_* (One-World FS), xes_posix_* (호스트 파일시스템), es_local_* (클라이언트 read-only 캐시) (es*.c) |
| 두 단계 해시 디렉토리 | ces_<HASH1>/ces_<HASH2>/<file> 를 es_get_unique_name, ES_POSIX_HASH1/2 으로 만든다 (es_posix.c) |
| LOB 상태 머신 | enum lob_locator_state — 6 개 상태. 전이표는 lob_locator.hpp 헤더 주석에 인라인되어 있다 |
| TDES 단위 locator 리스트 | tdes->lob_locator_root — red-black 트리 (lob_rb_root). 키는 locator 의 hash + key 부분 (transaction_transient.cpp) |
| locator 별 savepoint 스택 | lob_locator_entry::top — 상태 변경 때 push, 부분 롤백 때 pop 하는 lob_savepoint_entry 연결 리스트 (같은 파일) |
| Commit / rollback 워커 | tx_lob_locator_clear (tdes, at_commit, savept_lsa) — 단일 진입점. log_manager.c (commit, abort) 와 transaction_sr.c (sysop 종료) 에서 호출 |
| 백그라운드 회수 핸드오프 | vacuum_notify_es_deleted — commit 시점의 LOB_PERMANENT_DELETED 파일은 즉시 unlink 하지 않고 vacuum 큐로 넘긴다 |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”LOB 경로 위에서 CUBRID 가 다루는 부분은 네 가지다. ES (External Storage) 계층 은 바이트가 어디에 있는지를 추상화한 다. locator URI 는 LOB 한 건의 이름이다. TDES 단위 locator 트리 는 이번 트랜잭션이 건드린 locator 들을 기억한 다. commit / rollback 워커 가 그 트리를 실제 파일시스템 동 작으로 바꾼다. 이 순서로 하나씩 풀어 본다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph SQL["SQL row"]
R["heap record<br/>(...) ... locator='file:/.../ces_007/dba.t1.123_4567' ..."]
end
subgraph TDES["트랜잭션 단위 상태 (TDES)"]
RB["lob_locator_root<br/>(red-black 트리)"]
E1["lob_locator_entry<br/>key, hash, top"]
E2["lob_locator_entry<br/>key, hash, top"]
SV["savepoint 스택<br/>state, savept_lsa"]
RB --> E1
RB --> E2
E1 --> SV
end
subgraph ES["ES 계층 (es.c, es_init/es_create_file/...)"]
POS["es_posix_∗<br/>호스트 파일시스템"]
OWFS["es_owfs_∗<br/>오브젝트 스타일 FS"]
LOC["es_local_∗<br/>클라이언트 캐시"]
end
DISK[("파일시스템<br/>ces_<H1>/ces_<H2>/<file>")]
R --> RB
ES --> DISK
POS --> DISK
OWFS --> DISK
TX[("commit / rollback")] --> CLR["tx_lob_locator_clear"]
CLR --> RB
CLR --> ES
이 그림이 잡아내는 세 경계가 정합성의 핵심이다. (SQL 경계)
행은 locator URI 만 들고 있고, 바이트는 절대 직접 들지 않는다.
(TDES 경계) locator 의 상태를 바꾸는 모든 동작이
xtx_add_lob_locator / xtx_change_state_of_locator /
xtx_drop_lob_locator 한 곳을 지나가므로, 트랜잭션 단위 트리가
단일 진실의 출처가 된다. (ES 경계) 모든 파일시스템 호출이
es.c 한 곳을 지나가므로, 백엔드 선택 (OWFS / POSIX / LOCAL) 도
한 곳에서만 결정된다.
ES 계층 — URI prefix 로 백엔드 디스패치
섹션 제목: “ES 계층 — URI prefix 로 백엔드 디스패치”LOB 바이트의 모든 read / write 가 es.c 를 지나간다. 모듈은
es_init(uri) 한 번으로 초기화된다. URI 의 prefix 가 어느 백엔
드로 라우팅할지를 정한다.
// ES_TYPE — src/storage/es_common.htypedef enum{ ES_NONE = -1, ES_OWFS = 0, ES_POSIX = 1, ES_LOCAL = 2} ES_TYPE;
#define ES_OWFS_PATH_PREFIX "owfs:"#define ES_POSIX_PATH_PREFIX "file:"#define ES_LOCAL_PATH_PREFIX "local:"// es_create_file — src/storage/es.c (condensed)intes_create_file (char *out_uri){ // ... condensed ... if (es_initialized_type == ES_OWFS) { memcpy (out_uri, ES_OWFS_PATH_PREFIX, sizeof (ES_OWFS_PATH_PREFIX)); ret = es_owfs_create_file (ES_OWFS_PATH_POS (out_uri)); } else if (es_initialized_type == ES_POSIX) { memcpy (out_uri, ES_POSIX_PATH_PREFIX, sizeof (ES_POSIX_PATH_PREFIX));#if defined (CS_MODE) ret = es_posix_create_file (ES_POSIX_PATH_POS (out_uri));#else ret = xes_posix_create_file (ES_POSIX_PATH_POS (out_uri));#endif } // ... condensed (ES_LOCAL is read-only, no create) ...}이 디스패치에서 두 가지 성질이 떨어져 나온다. (a) ES_LOCAL
은 create 경로 자체가 없다. ES_LOCAL 은 다른 곳에서 만들어진
파일을 읽기만 하는 클라이언트 측 캐시다. deck 이 이 백엔드를
다루지 않는 이유도 deck 이 서버 관점이기 때문이다. (b) 서
버 안에서 POSIX 경로는 xes_posix_* 가 처리한다. x 접두사가
붙은 함수가 서버 측 구현이고, 접두사 없는 es_posix_* 는 클라
이언트가 network_interface_cl.h 로 서버로 RPC 하기 위한
stub 이다.
locator URI — name, key, meta
섹션 제목: “locator URI — name, key, meta”locator 는 다음 모양의 NUL 종단 문자열이다.
file:/<base-dir>/ces_<HASH1>/ces_<HASH2>/<metaname>.<unum>_<rand>lob_locator.cpp 의 두 헬퍼가 이 문자열을 메모리 할당 없이 파
싱한다.
// lob_locator_meta / lob_locator_key — src/object/lob_locator.cppconst char *lob_locator_key (const char *locator){ return std::strrchr (locator, '.') + 1; // 마지막 '.' 바로 뒤}
const char *lob_locator_meta (const char *locator){ return std::strrchr (locator, PATH_SEPARATOR); // 마지막 '/'}key 는 unique-name 접미부 (<unum>_<rand>) 로, 트랜잭션 단위
트리에서 locator 를 식별하는 키다. meta 는 디렉토리 부분이
다. 부분 롤백 경로가 locator 를 savepoint 이전 이름으로 되돌릴
때 이 부분을 쓴다. 자세한 동작은 §savepoint 스택 에서 다룬
다.
<metaname> 부분은 meta 와 key 사이에 들어 있는, 사람이 읽
을 수 있는 문자열이다. 보통 <schema>.<table> 의 형태다. deck
은 이를 db 정보 (예) dba.t1 라고 부른다. 생성 직후의 metaname
은 임시 sentinel 인 ces_temp 다. commit 시 영구화되면 비로소
바뀐다 (§Create flow 참고).
상태 머신 — 6 개 상태, 하나의 전이표
섹션 제목: “상태 머신 — 6 개 상태, 하나의 전이표”엔진이 다루는 모든 locator 는 6 개 중 하나의 상태에 있다. 정식
참고는 lob_locator.hpp 의 주석 블록이다.
// enum lob_locator_state — src/object/lob_locator.hpp/* * locator | created | deleted * ----------|-----------------------|-------------------------- * in-tran | LOB_TRANSIENT_CREATED | LOB_UNKNOWN (s1) * permanent | LOB_PERMANENT_CREATED | LOB_PERMANENT_DELETED (s3) * out-tran | LOB_UNKNOWN | LOB_UNKNOWN * | LOB_UNKNOWN | LOB_TRANSIENT_DELETED (s4) * * s1: create transient locator and delete it * LOB_TRANSIENT_CREATED -> LOB_UNKNOWN * s2: create transient locator and bind it to a row * LOB_TRANSIENT_CREATED -> LOB_PERMANENT_CREATED * s3: bind transient locator to a row and delete the locator * LOB_PERMANENT_CREATED -> LOB_PERMANENT_DELETED * s4: delete a locator created out of transaction * LOB_UNKNOWN -> LOB_TRANSIENT_DELETED */enum lob_locator_state{ LOB_UNKNOWN, LOB_TRANSIENT_CREATED, LOB_TRANSIENT_DELETED, LOB_PERMANENT_CREATED, LOB_PERMANENT_DELETED, LOB_NOT_FOUND};LOB_TRANSIENT_* 상태는 “파일은 디스크에 있지만 어떤 commit 된
테이블의 행도 아직 그 파일을 가리키지 않는다” 라는 뜻이다. 트
랜잭션이 elo_create() 를 막 호출했거나, 아직 commit 되지 않
은 파일을 지우려고 표시한 상태다. LOB_PERMANENT_* 는 “이미
commit 된 (또는 곧 commit 될) 행이 이 locator 를 가리킨다” 라는
뜻이다. LOB_UNKNOWN 과 LOB_NOT_FOUND 는 lob_locator_find
가 트리에서 항목을 찾지 못했을 때 돌려주는 sentinel 이다.
이 상태 머신이 commit / rollback 워커가 무엇을 할지를 결정한다. 워커 자체는 §Commit / rollback 디스패치 에서 다룬다.
TDES 단위 locator 트리 — red-black, 해시드 키
섹션 제목: “TDES 단위 locator 트리 — red-black, 해시드 키”LOB 을 건드리는 트랜잭션은 자기 TDES 위에 red-black 트리를 키 운다.
// lob_locator_entry / lob_savepoint_entry — src/transaction/transaction_transient.cppstruct lob_savepoint_entry{ LOB_LOCATOR_STATE state; LOG_LSA savept_lsa; // 이 상태가 설정된 savepoint char locator[ES_URI]; lob_savepoint_entry *prev; // savepoint 스택};
struct lob_locator_entry{ RB_ENTRY (lob_locator_entry) head; lob_savepoint_entry *top; // savepoint 스택의 top int key_hash; // m_key 의 5strhash, 빠른 비교 용 std::string m_key; // <unum>_<rand> 접미부};비교 함수는 먼저 key_hash 만 보고, 해시가 같을 때만
std::string::compare 까지 내려간다.
// lob_locator_cmp — src/transaction/transaction_transient.cppstatic intlob_locator_cmp (const lob_locator_entry *e1, const lob_locator_entry *e2){ if (e1->key_hash != e2->key_hash) return e1->key_hash - e2->key_hash; return e1->m_key.compare (e2->m_key);}표준적인 hash 먼저, 같으면 비교 패턴이다. 대부분의 lookup 이 싼 정수 비교에서 끝나고, 충돌이 난 경우에만 문자열 비교 비용을 낸다.
Savepoint 스택 — 부분 롤백 시 pop
섹션 제목: “Savepoint 스택 — 부분 롤백 시 pop”xtx_change_state_of_locator 는 locator 의 상태가 바뀔 때마다
호출된다. 예컨대 commit 바인딩에서의
LOB_TRANSIENT_CREATED → LOB_PERMANENT_CREATED, 또는 update
도중의 rename 이 그렇다. 핵심은, 이 변경이 직전 savepoint LSA
보다 엄격히 늦은 savepoint LSA 에서 일어나면, 새 상태로 덮어쓰
지 않고 이전 상태를 savepoint 스택으로 push 한다는 것이다.
// xtx_change_state_of_locator — src/transaction/transaction_transient.cpp (condensed)last_lsa = LSA_GE (&tdes->savept_lsa, &tdes->topop_lsa) ? tdes->savept_lsa : tdes->topop_lsa;
if (LSA_LT (&entry->top->savept_lsa, &last_lsa)) { lob_savepoint_entry *savept = new lob_savepoint_entry (); savept->state = entry->top->state; savept->savept_lsa = entry->top->savept_lsa; std::strcpy (savept->locator, entry->top->locator); savept->prev = entry->top; entry->top = savept; // push }
if (new_locator != NULL) strlcpy (entry->top->locator, new_locator, sizeof (ES_URI));entry->top->state = state;entry->top->savept_lsa = last_lsa;트랜잭션이 어느 savepoint LSA 까지 부분 롤백되면,
tx_lob_locator_clear 가 스택을 걸으며 savept_lsa >= 롤백 LSA
인 항목을 pop 한다. pop 도중 locator 문자열이 바뀐 자리마다
es_rename_file 을 호출해서 디스크 파일이름을 롤백 이전 상태
로 되돌린다. CUBRID 안에서 LOB 용 es_rename_file 을 부르는
곳은 이 워커뿐이다.
Commit / rollback 디스패치 — 단일 진입점
섹션 제목: “Commit / rollback 디스패치 — 단일 진입점”tx_lob_locator_clear 는 엔진의 정확히 네 곳에서 호출된다. commit (log_commit), abort (log_rollback), savepoint 부분
롤백 (log_rollback_to_savepoint), 그리고 nested system op 종
료 (xlog_topop_end). 인자는 at_commit (bool) 과
savept_lsa (전체 commit / abort 면 NULL, 부분 롤백이면 비-NULL)
다. 항목별로 파일을 지울지, 이름을 되돌릴지, 그대로 둘지를 결정
한다.
// tx_lob_locator_clear — src/transaction/transaction_transient.cpp (condensed)for (entry = RB_MIN (lob_rb_root, &tdes->lob_locator_root); entry != NULL; entry = next) { next = RB_NEXT (lob_rb_root, &tdes->lob_locator_root, entry); need_to_delete = false;
if (at_commit) { // commit 시점에 행에 바인딩되지 않은 locator 는 모두 garbage if (entry->top->state != LOB_PERMANENT_CREATED) need_to_delete = true; } else // rollback { if (savept_lsa != NULL) { // 부분 롤백: savepoint 스택 pop, 파일이름 되돌림 // ... condensed: see source ... } // 만들었다가 롤백한 locator 는 garbage if ((savept_lsa == NULL || LSA_GE (&entry->top->savept_lsa, savept_lsa)) && entry->top->state != LOB_TRANSIENT_DELETED) need_to_delete = true; }
if (need_to_delete) {#if defined (SERVER_MODE) if (at_commit && entry->top->state == LOB_PERMANENT_DELETED) vacuum_notify_es_deleted (thread_p, entry->top->locator); else (void) es_delete_file (entry->top->locator);#else (void) es_delete_file (entry->top->locator);#endif RB_REMOVE (lob_rb_root, &tdes->lob_locator_root, entry); lob_locator_free (entry); } }이 디스패치가 인코딩하는 규칙은 다음과 같다.
| at_commit | top->state | 동작 |
|---|---|---|
| true | LOB_PERMANENT_CREATED | 파일 그대로 둠 (commit 된 행이 가리킴) |
| true | LOB_PERMANENT_DELETED | vacuum_notify_es_deleted 로 vacuum 에 핸드오프 |
| true | LOB_TRANSIENT_* | es_delete_file 즉시 (다른 누구도 보지 못한 파일) |
| false | LOB_TRANSIENT_DELETED | 그대로 둠 (트랜잭션이 지우려던 파일은 아직 존재해야 함) |
| false | 그 외 | es_delete_file 즉시 (롤백된 생성, 어떤 행도 가리키지 않음) |
vacuum 핸드오프는 heap 의 vacuum_log_vacuum_record 에 해당하
는 LOB 측 메커니즘이다. commit 시점에
LOB_PERMANENT_DELETED 파일은 아직 옛 스냅샷에 보일 수 있다.
vacuum 데몬이 실제 unlink 시점을 통제한다. heap 에서 행이 사라
졌고 그 행을 참조할 수 있는 in-flight 스냅샷이 없어진 뒤에야 비
로소 회수한다.
Create flow — elo_create 끝에서 끝까지
섹션 제목: “Create flow — elo_create 끝에서 끝까지”사용자의 INSERT INTO t VALUES (1, BIT_TO_BLOB(X'...')) 는 파
서 / 값 계층이 DB_ELO 를 만든 뒤 elo_create 에 도달한다.
// elo_create — src/object/elo.c (condensed)intelo_create (DB_ELO *elo){ ES_URI out_uri; int ret;
ret = es_create_file (out_uri); // (1) 백엔드가 "ces_temp.<unum>_<rand>" 생성 // ... condensed ... elo->locator = db_private_strdup (NULL, out_uri); elo->type = ELO_FBO; // FBO = File-Backed Object elo->es_type = es_get_type (uri);
if (ELO_NEEDS_TRANSACTION (elo)) // ES_OWFS 또는 ES_POSIX { ret = lob_locator_add (elo->locator, LOB_TRANSIENT_CREATED); // (2) TDES 트리에 등록 } return ret;}
#define ELO_NEEDS_TRANSACTION(e) \ ((e)->es_type == ES_OWFS || (e)->es_type == ES_POSIX)직관과 다른 두 성질이 있다.
- 파일을 만든 뒤 에 트리에 추가한다. create 가 실패하면
TDES 부기 자체가 필요 없다. 그러나 create 는 성공했는데
lob_locator_add가 실패하면 디스크에 고아 파일이 남는다. deck 도 이 점을 다루지 않고, 현재 코드의 에러 처리도 파일을 unlink 하지 않는다. §미해결 질문 에서 다시 다룬다. ES_LOCAL은 트랜잭션 추적 자체를 거치지 않는다 (ELO_NEEDS_TRANSACTION이 ES_LOCAL 을 제외). read-only 클 라이언트 캐시에는 쓰기 경로가 없으므로, commit / rollback 이 화해해 줄 것 자체가 없다.
이후의 라이프사이클 호출은 다음과 같다.
elo_write (elo, pos, buf, count)→es_write_file(locator 상태는 변하지 않는다. 파일이LOB_TRANSIENT_CREATED상태이 므로 쓰기는 비용 없이 자유롭다).INSERT가 heap 계층에 도달해 행이elo->locator를 컬럼 값 으로 가지고 들어간다.- commit 시점에 행의 heap insert 가 가시화된다. 그 다음
tx_lob_locator_clear가 돈다. 이 때 locator 는 아직LOB_TRANSIENT_CREATED상태인데 — 행은 이미 그 locator 를 가 리킨다. 누가LOB_PERMANENT_CREATED로 바꿔 주는가 라는 질문이 자연스럽다. 현재 소스에서xtx_change_state_of_locator를 모듈 바깥에서 호출하는 곳은network_interface_sr.cpp:2380한 곳뿐이다. deck 의 “rename ces_temp.xxx → dba.t1.xxx” 시퀀스가 이 호출에 해당한다. 정확 한 호출 시점은 §미해결 질문 에서 다시 다룬다.
Read flow — elo_read 와 locator 왕복
섹션 제목: “Read flow — elo_read 와 locator 왕복”SELECT clob_to_char(c2) FROM t1 은 값 계층을 따라 내려와
elo_read 에 도달한다.
// elo_read — src/object/elo.c (signature)extern ssize_t elo_read (const DB_ELO *elo, off_t pos, void *buf, size_t count);호출자는 위치와 길이를 넘긴다. elo_read 는 es_read_file 로
넘기고, locator 의 prefix 에 따라 xes_posix_read_file 이나
es_owfs_read_file 이 처리한다. read 경로에서는 트리 탐색이
일어나지 않는다. heap 행 안의 locator 문자열만으로 충분하기
때문이다. read 는 TDES 상태를 만들지 않는다. 롤백해야 할 read
라는 것은 존재하지 않기 때문이다. 트랜잭션이 나중에 롤백되더
라도, read 자체는 디스크 파일에 보이지 않게 일어난 일이다.
읽기 전 사이즈 조회도 같은 이유로 elo_size →
es_get_file_size 로 단순하게 흘러간다. locator 만으로 충분
하다.
Update flow — copy-then-replace, in-place 가 아니다
섹션 제목: “Update flow — copy-then-replace, in-place 가 아니다”deck 이 update 경로를 따로 다룬다. create 와 다르기 때문이다.
new directory / file 생성 — create와 로직이 다름 generated_file → locator entry (LOB_TRANSIENT_CREATED) old_file → locator entry (LOB_TRANSIENT_DELETED) commit 시 old_file delete
elo_create + elo_write 를 그대로 쓰지 못하는 이유는 옛
locator 도 추적해야 하기 때문이다. CUBRID 는 이 상황을 두 개의
locator 트리 항목으로 푼다. 새 파일을
LOB_TRANSIENT_CREATED 하나, 옛 파일을
LOB_TRANSIENT_DELETED 하나. commit 시점에는 다음과 같이 동작
한다.
LOB_TRANSIENT_CREATED의 새 파일은 행에 바인딩되어LOB_PERMANENT_CREATED가 된다 (xtx_change_state_of_locator).LOB_TRANSIENT_DELETED의 옛 파일은 vacuum 핸드오프로 간다. 옛 파일을 가리키던 행 이 이제 MVCC dead version 이고, 다른 스냅샷이 그 행을 아직 읽고 있을 수 있기 때문이다.
elo_copy (in elo.c) 가 update 가 쓰는 공유 헬퍼다. 이 함수
가 es_copy_file 을 호출하고, 새 locator 를 등록하고, 옛
locator 를 삭제 표시하고 돌아온다. deck 의 “elo_copy() 함수는
rename, locator entry drop, copy file 작업 수행” 이 이 합성 동
작을 묘사한 것이다.
해시 디렉토리 레이아웃 — 디렉토리당 파일 수 제한
섹션 제목: “해시 디렉토리 레이아웃 — 디렉토리당 파일 수 제한”POSIX 백엔드는 파일을 두 단계 해시로 흩어 둔다. 어느 디렉토리 도 너무 많은 항목을 들지 않게 하기 위해서다.
// es_get_unique_name — src/storage/es_posix.c (condensed)static voides_get_unique_name (char *dirname1, char *dirname2, const char *metaname, char *filename){ UINT64 unum; int hashval, r;
r = (rand () < 0) ? -rand () : rand (); unum = es_get_unique_num (); // 마이크로초 정밀도 시간
snprintf (filename, NAME_MAX, "%s.%020llu_%04d", metaname, unum, r % 10000);
hashval = es_name_hash_func (ES_POSIX_HASH1, filename); snprintf (dirname1, NAME_MAX, "ces_%03d", hashval);
hashval = es_name_hash_func (ES_POSIX_HASH2, filename); snprintf (dirname2, NAME_MAX, "ces_%03d", hashval);}ES_POSIX_HASH1 과 ES_POSIX_HASH2 가 각 단계의 버킷 수다. 해
시는 mht_5strhash(filename) mod 버킷수 이므로, 같은 metaname
(예: dba.t1.*) 의 파일들도 두 단계에 걸쳐 골고루 흩어진다.
균등 분포가 의도된 결과다.
deck 이 짚는 trade-off 는 깔끔하다. 해시 디렉토리는 디렉토리당
파일 수를 줄여서 readdir 류 동작의 락 경합을 완화한다는 장점
이 있다. 단점은 백업 시나리오가 복잡해진다는 것이다. 모든
LOB 이 서로 다른 leaf 디렉토리에 살기 때문이다. 그리고 어떤 관
리용 순회도 두 단계를 모두 걸어야 한다. CUBRID 는 이 trade-off
를 받아들였다. 데이터 볼륨 안에 LOB 을 두는 대안은 모든 LOB 을
페이지 버퍼와 WAL 로 통과시키게 만들기 때문이다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”줄 번호가 아니라 심볼 이름 에 닻을 내린다는 원칙을 그대 로 따른다. 함수 이름은 대부분의 리팩토링을 견딘다. 줄 번호는 헤더 한 번 재포맷되면 곧장 흐트러진다.
ES 진입과 디스패치
섹션 제목: “ES 진입과 디스패치”es_init,es_final(ines.c) — 첫 호출 시 URI prefix 로 백엔드를 결정. 종료 시 정리.es_get_type,es_get_type_string(ines_common.c) — URI prefix 와ES_TYPEenum 사이의 변환.es_create_file,es_read_file,es_write_file,es_delete_file,es_copy_file,es_rename_file(ines.c) — 엔진의 다른 부분이 쓰는 공개 API.es_initialized_type으로 분기해서 백엔드로 전달한다.
POSIX 백엔드
섹션 제목: “POSIX 백엔드”xes_posix_create_file,xes_posix_write_file,xes_posix_read_file,xes_posix_delete_file,xes_posix_rename_file,xes_posix_copy_file(ines_posix.c) — 서버 측 구현.es_posix_create_file등 (x접두사 없는 형, ines_posix.c) — 클라이언트 측 stub.network_interface_cl.h로 서버로 RPC 한다.es_get_unique_name(ines_posix.c) — 파일 이름 + 해시 dirname 생성기.es_make_dirs(ines_posix.c) — 두 단계 해시 디렉토리에 대한mkdir -p.
OWFS 백엔드
섹션 제목: “OWFS 백엔드”es_owfs_create_file,es_owfs_write_file등 (ines_owfs.c) — One-World FS 오브젝트 스토리지 백엔드. POSIX 와 같은 surface.
LOB locator 상태 머신
섹션 제목: “LOB locator 상태 머신”enum lob_locator_state(inlob_locator.hpp) — 6 개 상태 와 전이표 인라인 주석.lob_locator_is_valid,lob_locator_key,lob_locator_meta(inlob_locator.cpp) — locator 문자열 파싱.lob_locator_add,lob_locator_change_state,lob_locator_drop,lob_locator_find(inlob_locator.cpp) 공개 wrapper. 서버 쪽xtx_*또는 클라이언트 RPC stub 으 로 디스패치한다.
TDES 단위 추적
섹션 제목: “TDES 단위 추적”struct lob_rb_root,struct lob_locator_entry,struct lob_savepoint_entry(intransaction_transient.cpp) 자료구조.xtx_add_lob_locator— 첫 상태 변경 때 RB-insert.xtx_change_state_of_locator— savepoint 스택 push + 상태 변경.xtx_drop_lob_locator— RB-remove. 트랜잭션이 끝나기 전에 locator 를 더 이상 신경 쓰지 않기로 한 호출자가 사용한다 (예 를 들어 실패한 create 에 대한 클라이언트 측 정리).xtx_find_lob_locator— 해시드 키로 RB-find. top 상태를 돌 려 준다.tx_lob_locator_clear— commit / rollback 워커.lob_locator_cmp— hash-then-compare 비교 함수.
ELO API
섹션 제목: “ELO API”elo_create,elo_copy,elo_copy_with_prefix,elo_delete,elo_size,elo_read,elo_write(inelo.c) — ES 와 locator 트리를 묶는 행-값 계층.elo_init_structure,elo_copy_structure,elo_free_structure(inelo.c) —DB_ELO라이프사이클.ELO_NEEDS_TRANSACTION매크로 — 추적 대상인 OWFS / POSIX 와 추적되지 않는 LOCAL 을 가른다.
Vacuum 핸드오프
섹션 제목: “Vacuum 핸드오프”vacuum_notify_es_deleted(vacuum 서브시스템에서 선언, 호출 은tx_lob_locator_clear에서) —LOB_PERMANENT_DELETED파일을 비동기 unlink 큐에 넣는다. vacuum 이 in-flight 스냅샷을 지나기까지 unlink 를 미룰 수 있 게 한다.
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”각 줄은 해당 심볼의 함수 정의 줄 이다. §소스 검증 의 인용 줄 범위는 같은 함수 본문 안에서의 부분 범위다. 정의 줄 에서부터 세어가며 대조하면 된다.
| 심볼 | 파일 | 줄 |
|---|---|---|
enum lob_locator_state | src/object/lob_locator.hpp | 53 |
lob_locator_key | src/object/lob_locator.cpp | 56 |
lob_locator_meta | src/object/lob_locator.cpp | 62 |
lob_locator_add | src/object/lob_locator.cpp | 90 |
lob_locator_change_state | src/object/lob_locator.cpp | 107 |
xtx_add_lob_locator | src/transaction/transaction_transient.cpp | 174 |
xtx_find_lob_locator | src/transaction/transaction_transient.cpp | 210 |
xtx_change_state_of_locator | src/transaction/transaction_transient.cpp | 245 |
xtx_drop_lob_locator | src/transaction/transaction_transient.cpp | 308 |
tx_lob_locator_clear | src/transaction/transaction_transient.cpp | 374 |
lob_locator_cmp | src/transaction/transaction_transient.cpp | 477 |
elo_create | src/object/elo.c | 85 |
ELO_NEEDS_TRANSACTION | src/object/elo.c | 71 |
es_create_file | src/storage/es.c | 142 |
es_get_unique_name | src/storage/es_posix.c | 78 |
es_get_type | src/storage/es_common.c | 45 |
enum ES_TYPE | src/storage/es_common.h | 28 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”각 항목은 현재 소스에 대한 사실을 먼저 굵게 적는다는 원 칙을 따른다. 뒤에 붙는 한 줄은 검증 경로와 옛 자료와의 차이 를 보여주는 부가 설명이다. 미해결 질문은 큐레이터가 남긴 공 백을 별도 블록으로 모은다.
검증된 사실
섹션 제목: “검증된 사실”-
ES 계층은 정확히 세 백엔드를 지원한다. 2026-05-01 에
src/storage/es_common.h에서 검증함.ES_TYPE은{ES_NONE, ES_OWFS, ES_POSIX, ES_LOCAL}이다. deck 은 POSIX / OWFS / LOCAL 순서로 세 가지를 거론했다. 현재 소스는 이를0 / 1 / 2로 번호화하고ES_NONE = -1을 미초기화 sentinel 로 쓴다. -
ELO_NEEDS_TRANSACTION은ES_LOCAL을 트리 추적에서 제 외한다. 2026-05-01 에src/object/elo.c:71에서 검증함. 매크로는(es_type == ES_OWFS || es_type == ES_POSIX)다. deck 은 이 비대칭을 다루지 않는다. LOB 컬럼 타입에서 트리로 내려가는 독자가 자칫 모든 locator 가 추적된다고 오해할 수 있 다. -
lob_locator.hpp의 상태 전이표는 정식 자료이고 deck 과 일치한다. 2026-05-01 에src/object/lob_locator.hpp:26-52주석 블록 (53 줄enum lob_locator_state정의 직전) 을 읽어 검증함. 주석의 네 라벨 (s1–s4) 이 deck 의 INSERT / UPDATE / DELETE / abort 예시와 같은 케이스다. -
commit 시점의
LOB_PERMANENT_DELETED파일은 즉시 unlink 되지 않고 vacuum 에 큐잉된다. 2026-05-01 에tx_lob_locator_clear(src/transaction/transaction_transient.cpp:374, 본문 443-457 줄) 안에서 검증함.vacuum_notify_es_deleted (thread_p, entry->top->locator)분기는SERVER_MODE에서(at_commit && state == LOB_PERMANENT_DELETED)일 때만 발화 한다. 다른 삭제 경로는es_delete_file을 직접 부른다. deck 은 commit 시점의 삭제를 즉각적인 것 (“commit 시 delete 수 행”) 처럼 묘사하는데, 영구-삭제 케이스에 한해서는 정확하지 않다. -
해시 디렉토리는 파일이름의
mht_5strhash로 두 단계로 만 들어진다. 2026-05-01 에src/storage/es_posix.c:104-108에서 검증함.ES_POSIX_HASH1과ES_POSIX_HASH2가 버킷 수 를 정한다. dirname 형식은ces_%03d다 (zero padded 3 자 리, 즉 단계당 최대 1000 버킷). 두 번째 단계가 실제 nested 인 지 flatten 인지를 결정하는 빌드 플래그CUBRID_OWFS_POSIX_TWO_DEPTH_DIRECTORY(124 줄) 가 있다. 대 부분의 빌드는 이를 켠다. -
locator 별 savepoint 스택은 savepoint LSA 가 진행했을 때만 push 한다. 2026-05-01 에
xtx_change_state_of_locator(src/transaction/transaction_transient.cpp:245, 본문 273-283 줄) 안에서 검증함.LSA_LT (&entry->top->savept_lsa, &last_lsa)가드의 의미는, 같은 savepoint 안의 상태 변경은 덮어쓰고, savepoint 경계를 넘는 변경만 새lob_savepoint_entry를 push 한다는 것이다. deck 은 이 최적화를 표면화하지 않는다.
미해결 질문
섹션 제목: “미해결 질문”-
xtx_change_state_of_locator를 부르는 사람이 누구인가.TRANSIENT_CREATED → PERMANENT_CREATED승격 경로. 모듈 바깥에서 이 함수를 부르는 곳 (테스트 제외) 은src/communication/network_interface_sr.cpp:2380한 곳뿐이 다. deck 은 이 변환을elo_copy()와 commit 핸들러의 일부 로 설명한다. INSERT 실행에서 상태 변경까지의 실제 경로를 추 적해서, heap insert 시점인지 행 commit 시점인지 별도 RPC 인 지를 문서화할 필요가 있다. 조사 경로 — sr 측 stub 의 호출 자 체인 추적,LOB_PERMANENT_CREATED쓰기 사이트 검색. -
lob_locator_add실패 시 고아 파일 처리.elo_create는es_create_file이 성공한 뒤lob_locator_add가 실패 해도 파일을 unlink 하지 않는다. 조사 경로 —lob_locator_add의 실패 모드는ER_LOG_UNKNOWN_TRANINDEX하나뿐이다. 호출 자 체인이 create 성공 + add 실패 케이스에 도달하는지 확인 하고, 도달한다면 고아 누수 코너 케이스로 CBRD 티켓을 열 만 하다. -
heap 계층에 행은 있는데 locator 가 TDES 트리에 없을 때
LOB_NOT_FOUND의 의미.xtx_find_lob_locator가LOB_NOT_FOUND를 돌려주고 입력 locator 를 그대로 복사해 준다는 사실은 검증했다. heap 측elo_read/elo_size가 이 결과에 어떻게 반응하는지는 추적하지 못했다. 답이 “디스크 파일로 그냥 fallthrough” 라면 commit-후 read 에는 정확하 다. 답이 에러 라면 동시 롤백 상황에서 잘못된 실패가 노출 될 수 있다. 조사 경로 — heap →elo_read→ ES 디스패치를 추적해서, read 경로가 locator-tree 상태를 참조하는지 확인. -
두 단계 vs. 평면화 해시 디렉토리.
CUBRID_OWFS_POSIX_TWO_DEPTH_DIRECTORY매크로가 두 번째 단 계를 게이트한다 (es_posix.c:124). 현재 빌드 기본은 켜 짐 이지만, 플래그로 fallback 할 수 있다. 조사 경로 —git log -S CUBRID_OWFS_POSIX_TWO_DEPTH_DIRECTORY로 단일 단계가 기본이었던 시기를 찾고, 활성 배포 가운데 단일 단계를 여전히 쓰는 곳이 있는지 확인. -
다른 replica 가 이미 본 파일이름을 부분 롤백할 때의
xes_posix_rename_file동작. HA 복제는 로그 레코드는 배 달하지만 파일시스템 rename 은 배달하지 않는다. replica 가 rename 후의 locator 를 읽었는데 primary 가 그 rename 을 롤 백하면, replica 의 디스크 경로가 더 이상 존재하지 않는다. 조사 경로 — HA 의 LOB 처리 추적, HA 경로에서lob_locator검색, 물리 복제가 ES 측 동작을 어떻게 따라가는지 확인. -
OWFS 의 deprecation 상태. deck 도 현재 소스도 OWFS 를 일급 백엔드로 유지한다. 그러나 가시성 있는 모든 배포는 POSIX 를 쓴다. 조사 경로 —
git log src/storage/es_owfs.c로 새 개발이 있는지 확인. 11.x 고객 가운데 OWFS 운영 사례가 있는지 문의.
CUBRID 너머 — 비교 설계와 연구 프론티어
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 프론티어”깊이가 아니라 포인터다. 각 항목은 후속 문서를 시작할 손잡이 다. 여기서의 깊이는 일부러 얕게 잡았다.
-
PostgreSQL TOAST — The Oversized-Attribute Storage Technique (Stonebraker 외, PostgreSQL 내부 문서로 정리). 인 라인 LOB 의 페이로드를 같은 테이블당 별도의 TOAST 테이블로 chunking 한다. MVCC 가시성 규칙은 일반 행과 같다. CUBRID 의 외부 파일 방식은 MVCC 통합을 LOB 단위 I/O 단순화와 맞바꾼 것이다. 비교 항목 — vacuum 기반 TOAST chunk 회수 비용 vs. ES 의 commit 시점 unlink 비용을 정량화한다.
-
PostgreSQL Large Objects (
lo_*) — TOAST 와 별개의 옛pg_largeobject시스템 테이블. locator 는 OID, 페이로드는 2 KB 내부 페이지로 자른다. WAL 로 완전히 보호된다. CUBRID 가 WAL 바깥의 ES 저장을 고른 것은, 일부 복구 의미를 I/O 성능과 단순한 LOB 복사로 맞바꾼 결정이다. -
Oracle SecureFiles — Oracle 의 현대 LOB 엔진. 옛 BasicFiles 위에 in-place update, 중복 제거, 암호화, 압축을 얹었다. CUBRID 와의 비교 관점은 암호화다. CUBRID 의 TDE 는 현재 ES 파일을 암호화하지 않는다 (
cubrid-tde.md가 작성되 면 거기서 다룬다). -
Oracle BFILE — Oracle 의 read-only 외부 파일 포인터. CUBRID 의
ES_LOCAL에 가장 가까운 대응물이다. 비교를 하면 ES_LOCAL 이 실제 운영에서 쓰이는지를 분명히 할 수 있다. deck 이 ES_LOCAL 을 다루지 않으므로 더 그렇다. -
MySQL InnoDB off-page columns — InnoDB 는 큰 컬럼을 같 은 tablespace 의 overflow 페이지에 둔다.
ROW_FORMAT=DYNAMIC은 긴 컬럼을 20 바이트 포인터만 행에 두고 모두 page 바깥으로 보낸다. ES 보다는 TOAST 에 가깝지만 페이지 포맷 관점이 다르 다. 비교가 짚어줄 점은 CUBRID 가 페이지 포맷 자체를 우회한 설계 선택이다. -
Lehman & Lindsay, “The Starburst Long Field Manager” (VLDB 1989). 트랜잭션 안전한 out-of-row LOB 관리를 처음 정리 한 논문.
lob_savepoint_entry의 스택 패턴은 Starburst 의 multi-level recovery 의 후예라는 점을 알아볼 수 있다. 다시 읽어 보면, CUBRID 의 스택 의미가 Starburst 와 정확히 같은지 아니면 갈라지는지를 분명히 할 수 있다. -
오브젝트 스토어를 LOB 백엔드로 (S3, GCS, MinIO) — OWFS 의 현대적 후계. cloud-native 오브젝트 스토어는 어떤 것도 파 일시스템 rename 의 원자성을 지원하지 않는다. rollback 경로 를 다시 설계해야 한다. CUBRID 는 부분 롤백 시 파일 이름 복원 을 위해
es_rename_file에 의존하기 때문이다. 연구급 후속은 CUBRID 의 rename 기반 rollback 을 versioned-PUT 방식의 오브 젝트 스토어로 어떻게 매핑할 수 있는지를 그리는 작업이다.
원본 분석 자료 (under raw/code-analysis/cubrid/storage/lob/)
섹션 제목: “원본 분석 자료 (under raw/code-analysis/cubrid/storage/lob/)”LOB 세미나.pptx— CUBRID 개발 2팀 인치준 연구원의 단일-deck 세미나. locator 단위에서 본 CRUD 로직,ces_temprename 트 릭, TDES 단위 locator entry 리스트, hashdir trade-off 를 다 룬다.
교과서 챕터 (under knowledge/research/dbms-general/)
섹션 제목: “교과서 챕터 (under knowledge/research/dbms-general/)”- Database Internals (Petrov), Ch. 2 File Formats — 페이지 바깥 레코드, indirection vs. inline.
- Database System Concepts (Silberschatz et al.), §13.5 Record Organization — BLOB / CLOB 의미.
외부 워크스페이스 페이지
섹션 제목: “외부 워크스페이스 페이지”- (인용 없음. 본 문서는 raw deck + 소스로만 조립.)
CUBRID 소스 (under /data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (under /data/hgryoo/references/cubrid/)”src/object/elo.h,src/object/elo.csrc/object/lob_locator.hpp,src/object/lob_locator.cppsrc/storage/es.h,src/storage/es.csrc/storage/es_common.h,src/storage/es_common.csrc/storage/es_posix.h,src/storage/es_posix.csrc/storage/es_owfs.h,src/storage/es_owfs.csrc/transaction/transaction_transient.hpp,src/transaction/transaction_transient.cppsrc/compat/db_elo.h,src/compat/db_elo.c(클라이언트 측 wrapper)