(KO) CUBRID XASL Cache — SQL 해시 텍스트의 SHA-1 키, RT 재컴파일, 클래스별 무효화로 동작하는 plan cache
목차
학술적 배경
섹션 제목: “학술적 배경”Plan cache(계획 캐시)가 존재하는 이유는 분명하다. SQL 한 문장을 컴파일하는 비용은 비싸고, 같은 문장을 다시 실행하는 비용은 거의 공짜이기 때문이다. 컴파일 파이프라인 — 어휘 분석, 파싱, 이름 해석, 타입 검사, 재작성, 비용 기반 최적화, 실행 계획 생성, 직렬화 — 은 데이터의 양과 무관하게 수 ms에서 수백 ms 수준의 고정 지연을 발생 시킨다. Database Internals(Petrov, 12장)와 Hellerstein, Stonebraker, Hamilton의 Architecture of a Database System(2007) 논문은 이 캐시를 명시적으로 호명한다. parse-and-plan once, execute many. 한 번 파싱하고 계획을 세운 뒤, 여러 번 실행한다는 원칙이다.
모든 plan cache는 다음 네 가지 질문에 답해야 한다.
- 캐시 키.
compile : SQL_text → plan을 어떻게 메모이즈할 것인가? 원시 텍스트 는 정확하지만 잘 깨진다. 정규화된 텍스트 (Oracle의cursor_sharing=force, SQL Server의 forced parameterisation)는 동치인 문장을 하나로 합치지만 parameter sniffing 문제를 만든다. AST 해시 는 정규화된 텍스트와 동치이지만 계산이 비싸다. - Stale 판정. 참조 객체에 대한 DDL 은 hard invalidation 대상 이고, 통계 drift 는 soft invalidation 대상(계획은 여전히 동작 하므로 비효율적이지만 즉시 죽일 필요는 없다. 재컴파일을 예약 한다), 바인드 파라미터의 모양 변경 은 hard invalidation 대상이다.
- 동시성. 실행마다 매번 건드려지는 자료 구조이므로 단순 락은 곧 병목이다. 표준적 해법은 latch-free 또는 sharded-lock 해시맵에 참조 카운트를 곁들이고, 회수를 지연시키는 패턴이다. Fraser의 Practical Lock-Freedom(2004)이 정리한 epoch reclamation, Mohan 외의 ARIES(TODS 1992)에 등장하는 buffer pool reader-writer 프로토콜이 같은 가족이다.
- Eviction. LRU, ARC, 시간 기반, 크기 기반 등 선택지가 있다. 너무 공격적이면 매번 컴파일 비용이 들고, 너무 관대하면 캐시가 buffer pool을 잡아먹는다.
비교 기준점은 다음과 같다. PostgreSQL의 백엔드별 prepared statement 캐시(개별 백엔드 소유, 사용자가 부여한 이름이 키), Oracle library cache(서버 전역, 해시 키, parent/child cursor로 모양 변형 추적), SQL Server plan cache(서버 전역, 해시 키, parameter sniffing). CUBRID 는 Oracle과 가장 가깝다. 서버 전역, 공유, 재작성된 SQL을 해시한 키.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”모든 production-grade plan cache가 공유하는 네 개의 가동 부품이
있다. 결합 방식은 다르지만 부품 자체는 동일하다. 다음 절
## CUBRID의 구현 에서 보게 될 선택은 이 설계 공간 안의 다이얼
조합이지 발명이 아니다.
캐시 키 — 정규화된 SQL 텍스트의 해시
섹션 제목: “캐시 키 — 정규화된 SQL 텍스트의 해시”Plan cache 키는 계산이 싸고, 사소한 문법 변형을 안정적이어야 한다. 지배적인 패턴은 재작성 후의 SQL 텍스트를 해시하고, 원본 텍스트 는 진단용 보조 필드로 함께 보관하는 것이다. PostgreSQL은 해시를 생략한다(백엔드 단위 캐시이므로 사용자 이름이 키). Oracle은 MD5에 충돌 시 전체 텍스트 비교를 추가한다. SQL Server는 64비트 해시를 사용한다. CUBRID는 재작성된 SQL의 SHA-1(이를 hash text 라 부른다)을 사용한다.
캐시 엔트리 — plan 본체와 의존성 메타데이터
섹션 제목: “캐시 엔트리 — plan 본체와 의존성 메타데이터”Plan cache 엔트리는 다섯 가지를 함께 들고 다닌다. 직렬화된
plan, plan이 건드리는 카탈로그 객체들의 의존성 리스트, 실행
시점에 잡아야 하는 lock 요구, 컴파일 시점에 찍어 둔 통계
스냅샷(drift 감지용), 그리고 참조 카운트와 eviction 메타데이터.
CUBRID의 XASL_CACHE_ENTRY 가 이 다섯 가지를 모두 담는다.
동시성 — 참조 카운트 엔트리, latch-free 조회
섹션 제목: “동시성 — 참조 카운트 엔트리, latch-free 조회”현대적인 plan cache는 조회를 위해 latch-free 또는 sharded-lock
해시맵을 사용한다. 엔트리는 참조 카운트로 보호된다. 읽는 쪽은
fix count를 증가시키고, DDL은 삭제 표시 비트만 세팅한 뒤 fix
count = 0이 될 때까지 회수를 지연한다. Fraser(2004)의 epoch 기반
회수 패턴 그대로다. CUBRID는 fix count와 상태 플래그를 단 하나의
32비트 cache_flag 안에 패킹해, 전체 atomic CAS가 한 워드 안에
들어가도록 만들었다.
무효화 — DDL hook + 재컴파일 임계값
섹션 제목: “무효화 — DDL hook + 재컴파일 임계값”트리거는 둘이다. 클래스에 대한 DDL. locator가 plan cache를
호출해, 의존성 리스트에 해당 OID가 들어 있는 모든 엔트리를 떨어
뜨린다. PostgreSQL은 이를 RelationCacheInvalidateEntry 로 처리
하고, Oracle은 수정된 객체를 들고 있는 모든 cursor를 무효화한다.
CUBRID의 hook은 xcache_remove_by_oid 다. 통계 drift. 엔트리들을
주기적으로 순회하며 저장된 cardinality와 현재 cardinality를 비교하고
임계값을 넘으면 soft invalidation과 함께 재컴파일을 요청한다.
Oracle의 adaptive cursor sharing이 같은 발상이다. CUBRID에서는 이를
xcache_check_recompilation_threshold 안의 재컴파일 임계값(RT)
검사 라 부른다(10배 계수, 엔트리당 6분 cooldown).
Eviction — 엔트리 수와 메모리 양으로 묶인 LRU
섹션 제목: “Eviction — 엔트리 수와 메모리 양으로 묶인 LRU”예산이 둘이다. 최대 엔트리 수(soft cap, cleanup 트리거)와 최대
메모리(hard cap, insert 차단). Eviction은 LRU 후보를 고른다.
CUBRID의 xcache_cleanup 은 time_last_used 기반 binary heap을
사용하고, 실제 삭제는 DDL 무효화가 사용하는 동일한 참조 카운트
인지 경로로 이루어진다.
이론 ↔ CUBRID 매핑
섹션 제목: “이론 ↔ CUBRID 매핑”| 이론적 개념 | CUBRID의 이름 |
|---|---|
| 캐시 키(정규화 SQL의 해시) | XASL_ID 안의 SHA1Hash sha1, compile_context::sql_hash_text 에 SHA1Compute 호출 |
| 캐시 엔트리 | XASL_CACHE_ENTRY (xasl_cache.h) |
| 직렬화된 plan | 엔트리 내부의 XASL_STREAM stream |
| 의존성 리스트 | XCACHE_RELATED_OBJECT *related_objects (oid, lock 모드, cardinality) |
| 통계 스냅샷 | 각 related object의 tcard 필드(컴파일 시점의 heap page 수) |
| 참조 카운트 + 상태 머신 | XASL_ID 의 INT32 cache_flag (하위 24비트 = fix count, 상위 8비트 = 플래그) |
| Latch-free 조회 | cubthread::lockfree_hashmap<xasl_id, xasl_cache_ent> (xcache_Hashmap) |
| 삭제 표시 + 지연 회수 | XCACHE_ENTRY_MARK_DELETED / _DELETED_BY_ME + xcache_unfix |
| DDL 무효화 hook | xcache_remove_by_oid (locator_sr.c, serial.c 에서 호출) |
| 통계 drift 기반 재컴파일 | xcache_check_recompilation_threshold + XCACHE_RT_FACTOR |
| LRU eviction | xcache_cleanup + time_last_used 기반 binary heap |
| 세션별 prepared statement 레지스트리 | SESSION_STATE 의 PREPARED_STATEMENT 리스트(session.c) |
| Plan stream → 메모리 트리 | stx_map_stream_to_xasl → XASL_CLONE 채움 |
| 미리 deserialise된 plan tree pool | 엔트리의 XASL_CLONE 배열(상한: xasl_cache_max_clones) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”XASL cache는 서버 전역으로 한 인스턴스만 존재하는 해시맵으로,
직렬화된 plan을 재작성된 SQL의 SHA-1로 키잉한다. 가동 부품은
여섯이다. 키(XASL_ID: SHA-1 + cache_flag + time_stored), 엔트리
(XASL_CACHE_ENTRY: plan stream + 의존성 리스트), 해시맵
(cubthread::lockfree_hashmap), 32비트 cache_flag 에 패킹된
fix/unfix 상태 머신, 재컴파일 트리거(DDL OID hook + RT 임계값),
eviction 경로(binary heap LRU). 이 순서로 따라간 뒤, prepare/
execute 와이어 흐름으로 마무리한다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph CLIENT["클라이언트 (CAS / 드라이버 프로세스)"]
USR["사용자 SQL 텍스트"]
PARSE["parser_main.c<br/>parse + name-resolve<br/>· semantic-check<br/>· rewrite"]
SHA["sql_hash_text 에 대해<br/>SHA1Compute<br/>(execute_statement.c)"]
PREP["network_interface_cl.c<br/>net_prepare_query"]
end
subgraph SERVER["서버 (cub_server)"]
SQMGR["sqmgr_prepare_query<br/>(network_interface_sr.cpp)"]
XQMGR["xqmgr_prepare_query<br/>(query_manager.c)"]
FIND["xcache_find_sha1<br/>(xasl_cache.c)"]
INSERT["xcache_insert<br/>(xasl_cache.c)"]
HM[("xcache_Hashmap<br/>(latch-free)")]
EXEC["xqmgr_execute_query<br/>· xcache_find_xasl_id_for_execute<br/>· stx_map_stream_to_xasl"]
RUNNER["query_executor.c<br/>qexec_execute_mainblock"]
DDL["DDL / locator_sr.c<br/>serial.c"]
INVAL["xcache_remove_by_oid"]
end
USR --> PARSE
PARSE --> SHA
SHA --> PREP
PREP --> SQMGR
SQMGR --> XQMGR
XQMGR --> FIND
FIND -->|적중| HM
FIND -->|miss| INSERT
INSERT --> HM
XQMGR -->|XASL_ID 반환| PREP
PREP -->|EXECUTE: XASL_ID 송신| EXEC
EXEC --> HM
EXEC --> RUNNER
DDL --> INVAL
INVAL --> HM
세 개의 경계선이 보인다. (parser ↔ cache) 파서, 옵티마이저,
XASL generator는 클라이언트 측에서 동작한다(#if !defined(SERVER_MODE)).
캐시는 서버 안에 산다. SHA-1은 클라이언트에서 재작성된
sql_hash_text 로부터 계산되어 prepare 요청에 실려 보내진다.
(prepare ↔ execute) prepare는 XASL_ID 를 반환하거나 새로 설치
한다. execute는 그 XASL_ID 로 다시 찾고, 엔트리가 사라졌으면
ER_QPROC_INVALID_XASLNODE 로 실패하며 — 클라이언트는 다시 prepare
한다. (executor ↔ DDL) DDL이 xcache_remove_by_oid 를 호출해
의존 엔트리를 삭제 표시하고, 마지막 reader가 떠날 때 참조 카운트
경로가 실제 삭제를 마무리한다.
캐시 키 — XASL_ID
섹션 제목: “캐시 키 — XASL_ID”// XASL_ID — src/storage/storage_common.hstruct xasl_id{ SHA1Hash sha1; /* SHA-1 of rewritten SQL text */ INT32 cache_flag; /* refcount + state flags */ CACHE_TIME time_stored; /* when this plan was stored */};버킷 선택은 sha1.h[0] 한 워드만 사용한다(xcache_hash_key).
xcache_compare_key 는 160비트 전체와 상태 플래그를 다시 본다.
time_stored 는 prepare 키의 일부가 아니지만 execute 조회의
일부이긴 하다. prepare와 execute 사이에 엔트리가 무효화되고 새로
재컴파일된 엔트리로 교체되었을 수 있기 때문이다. 클라이언트가
들고 있던 (sha1, time_stored) 와 현재 엔트리의 time_stored 가
어긋나면 xcache_find_xasl_id_for_execute 가 이를 감지하고 강제로
재 prepare를 유도한다. cache_flag 는 동시성 상태 워드이며 다음
절에서 다룬다.
SHA-1 계산 — 재작성 후, 클라이언트에서
섹션 제목: “SHA-1 계산 — 재작성 후, 클라이언트에서”클라이언트는 파서 재작성이 끝난 뒤, prepare 요청을 보내기 직전에
sha1 을 계산한다. execute_statement.c 안에 호출 지점이 다섯 군데
있는데(캐시되는 statement 종류별로 하나씩 — SELECT, INSERT, UPDATE,
DELETE, MERGE), 모두 같은 패턴을 따른다.
// do_execute_update — src/query/execute_statement.c (condensed)PT_NODE_PRINT_TO_ALIAS (parser, statement, CUSTOM_PRINT_4_SHA_COMPUTE | PT_PRINT_LOWER);contextp->sql_hash_text = (char *) statement->alias_print;err = SHA1Compute ((unsigned char *) contextp->sql_hash_text, (unsigned) strlen (contextp->sql_hash_text), &contextp->sha1);CUSTOM_PRINT_4_SHA_COMPUTE 는 바인드 값과 주석 잡음을 제거하고,
PT_PRINT_LOWER 는 식별자를 소문자로 정규화한다. 결과적으로 해시는
재작성된, 정규화된, 소문자화된 SQL을 덮는다. 두 클라이언트의
재작성 결과가 같다면 같은 엔트리를 공유한다는 뜻이다. 해시와 SQL
텍스트는 network_interface_cl.c::net_prepare_query 안의
or_pack_sha1 으로 prepare 요청에 직렬화되고, 서버는
sqmgr_prepare_query 에서 COMPILE_CONTEXT::sha1 으로 풀어 낸다.
캐시 엔트리 — XASL_CACHE_ENTRY
섹션 제목: “캐시 엔트리 — XASL_CACHE_ENTRY”// XASL_CACHE_ENTRY (condensed) — src/query/xasl_cache.hstruct xasl_cache_ent{ XASL_ID xasl_id; /* embedded key */ XASL_STREAM stream; /* serialised plan bytes */ XASL_CACHE_ENTRY *stack, *next; /* lockfree freelist + bucket chain */ UINT64 del_id; /* deferred-delete txn id */ EXECUTION_INFO sql_info; /* hash/user/plan text (diagnostics) */ int xasl_header_flag; XCACHE_RELATED_OBJECT *related_objects; /* dependency list */ int n_related_objects; struct timeval time_last_used; /* LRU timestamp */ INT64 ref_count, clr_count; /* stats / qfile clears */ int list_ht_no; /* result-cache hash slot */ bool free_data_on_uninit; XASL_CLONE *cache_clones, one_clone; /* deserialised plan pool */ int n_cache_clones, cache_clones_capacity; pthread_mutex_t cache_clones_mutex; INT64 time_last_rt_check; /* RT cooldown */ bool initialized;};다섯 그룹으로 나누어 풀어 보자.
Plan 본체. stream 은 generator가 만들어 낸 byte-packed XASL 을
들고 있다(자세한 사항은 cubrid-xasl-generator.md). 실행 시점에
stx_map_stream_to_xasl 로 들어가 실행 가능한 노드 트리를 다시
만든다. 이 비용은 가볍지 않으며, 아래에서 설명할 plan clone으로
여러 호출에 걸쳐 분산된다.
의존성 리스트. related_objects 는 XCACHE_RELATED_OBJECT { OID oid, LOCK lock, int tcard } 의 배열이다. 클래스 또는 serial OID,
실행 시점에 잡아야 할 lock, 컴파일 시점의 테이블 cardinality(heap
page 수)가 들어간다. 소비자는 셋이다.
- 실행 시 lock 획득.
xcache_find_xasl_id_for_execute가 리스트를 순회하며 각 OID를lock_object를 호출한다. 이를 빠뜨리면 schema-modify lock(SCH_M_LOCK) 보유자가 그 사이 엔트리를 무효화 할 수 있다. lock을 잡은 뒤XCACHE_ENTRY_MARK_DELETED플래그를 다시 한 번 확인한다. - DDL 무효화.
xcache_remove_by_oid가xcache_entry_is_related_to_oid로 OID와 이 리스트를 매칭 한다. - RT 재컴파일.
xcache_check_recompilation_threshold가 리스트를 순회하며catalog_get_class_info로 현재 cardinality를 받아와 저장된tcard와 비교한다.
해시맵 부속. stack, next, del_id 는 lockfree 해시맵이
소유한다. 각각 freelist 링크, 버킷 체인 링크, 지연 삭제 transaction
id다. xasl_cache.c 상단의 LF_ENTRY_DESCRIPTOR(xcache_Entry_descriptor)
가 이 필드들의 오프셋과 alloc/free/init/uninit/compare/hash 콜백을
해시맵에게 알려 준다.
진단용 SQL 정보. sql_info 안의 세 문자열(sql_hash_text,
sql_user_text, sql_plan_text)은 조회에 쓰이지 않는다. 키는 SHA-1
뿐이다. 이들은 xcache_dump, SHOW PLAN_CACHE, trace 로그 출력에
쓰인다.
Plan clone. cache_clones 는 미리 deserialise된 plan tree의
배열이며, 원소 타입은 XASL_CLONE { xasl_buf, xasl } 이다. 첫 실행
이 deserialise를 수행하고, 실행이 끝나면(xcache_retire_clone)
clone이 배열로 되돌아간다. 이후 실행은 byte stream을 다시 풀지 않고
준비된 clone을 pop만 한다. xasl_cache_max_clones 로 상한을 두며
0이면 비활성화된다.
해시맵 — latch-free, 서버 전역
섹션 제목: “해시맵 — latch-free, 서버 전역”// using declaration — src/query/xasl_cache.cusing xcache_hashmap_type = cubthread::lockfree_hashmap<xasl_id, xasl_cache_ent>;전역 객체 xcache_Global 은 해시맵에 더해 사이즈 파라미터, cleanup
용 binary heap, timeout 경로를 위한 cleanup 배열, 동시 cleaner를 한
명으로 제한하는 cleanup_flag CAS 게이트, 메모리 회계 카운터
(memory_usage_cache, memory_usage_clone), 통계 카운터를 함께 묶고
있다. 초기 크기는 PRM_ID_XASL_CACHE_MAX_ENTRIES 이며, 해시맵은
freelist로 자동으로 늘어난다. freelist는 두 블록으로 쪼개져 있어
freelist claim 경로의 CAS 컨텐션을 줄인다. 블록 크기는
max(1, soft_capacity / 2) 다.
// xcache_initialize — src/query/xasl_cache.cconst int freelist_block_count = 2;const int freelist_block_size = std::max (1, xcache_Soft_capacity / freelist_block_count);xcache_Hashmap.init (xcache_Ts, THREAD_TS_XCACHE, xcache_Soft_capacity, freelist_block_size, freelist_block_count, xcache_Entry_descriptor);동시성 — 패킹된 cache_flag
섹션 제목: “동시성 — 패킹된 cache_flag”cache_flag 는 엔트리의 동시성 상태를 32비트 한 워드에 패킹한다.
상위 8비트는 상태 플래그, 하위 24비트는 fix count(현재 활성
reader 수)다.
// flag bit layout — src/query/xasl_cache.c#define XCACHE_ENTRY_MARK_DELETED ((INT32) 0x80000000)#define XCACHE_ENTRY_TO_BE_RECOMPILED ((INT32) 0x40000000)#define XCACHE_ENTRY_WAS_RECOMPILED ((INT32) 0x20000000)#define XCACHE_ENTRY_SKIP_TO_BE_RECOMPILED ((INT32) 0x10000000)#define XCACHE_ENTRY_CLEANUP ((INT32) 0x08000000)#define XCACHE_ENTRY_RECOMPILED_REQUESTED ((INT32) 0x04000000)#define XCACHE_ENTRY_FLAGS_MASK 0xFF000000#define XCACHE_ENTRY_FIX_COUNT_MASK 0x00FFFFFF모든 상태 전이는 워드 전체에 대한 CAS다. 즉 플래그 갱신과 참조
카운트 갱신이 하나의 원자 연산으로 직렬화된다. xcache_compare_key
는 비교만 하지 않는다. 비교에 성공하면 그 자리에서 동일한 CAS로
fix count를 증가시켜 엔트리를 fix 한다. 따라서 xcache_find_sha1
이 반환하는 엔트리는 fix된 상태이며, 호출자는 반드시 xcache_unfix
를 호출해야 한다.
상위 8비트의 상태 머신은 cleanup을 포함해 다섯 가지 엔트리 lifecycle 을 인코딩한다.
stateDiagram-v2 [*] --> NEW: xcache_Hashmap.freelist_claim NEW --> READY: insert_given (CAS into bucket) READY --> IN_USE: xcache_compare_key CAS \n fix_count++ IN_USE --> READY: xcache_unfix CAS \n fix_count-- READY --> RECOMP_PENDING: xcache_check_recompilation_threshold \n RECOMPILED_REQUESTED 설정 RECOMP_PENDING --> READY: 클라이언트 재 prepare \n RR 플래그 클리어 READY --> RECOMPILING: xcache_insert recompile_xasl \n TO_BE_RECOMPILED 설정 RECOMPILING --> WAS_RECOMPILED: 새 엔트리 삽입 \n WAS_RECOMPILED 설정, TBR 클리어 WAS_RECOMPILED --> MARKED_DELETED: 마지막 unfix가 \n WAS_RECOMPILED to MARK_DELETED 승격 READY --> MARKED_DELETED: xcache_remove_by_oid \n 또는 xcache_drop_all IN_USE --> MARKED_DELETED: 동일, 회수는 지연 MARKED_DELETED --> DELETED_BY_ME: 마지막 unfix CAS \n 한 스레드만 승리 DELETED_BY_ME --> [*]: xcache_Hashmap.erase + free
미묘한 부분이 한 가지 있다. xcache_unfix 가 fix_count = 0 으로
줄였는데 MARK_DELETED 가 설정되어 있다면(이 호출자가 마지막
fixer), 플래그를 XCACHE_ENTRY_DELETED_BY_ME 로 승격시킨다. 이때
값은 현재 transaction의 인덱스로 인코딩되어, 동시 deleter들이 자기
엔트리를 구분할 수 있게 된다.
#define XCACHE_ENTRY_DELETED_BY_ME \ ((XCACHE_ENTRY_MARK_DELETED | XCACHE_ENTRY_FIX_COUNT_MASK) - logtb_get_current_tran_index ())이후 비교 함수는 현재 transaction에 속한 DELETED_BY_ME 인코딩만
일치 처리한다. 공유 락 없이 다중 deleter를 허용하기 위해 CUBRID의
다른 모듈에서도 쓰는 락 프리 트릭이다.
조회 — xcache_find_sha1 / xcache_find_xasl_id_for_execute
섹션 제목: “조회 — xcache_find_sha1 / xcache_find_xasl_id_for_execute”조회 함수가 두 개인 이유는 prepare와 execute가 요구하는 바가 다르기 때문이다.
Prepare 경로 — xcache_find_sha1. 클라이언트가 SQL 문자열을
보내며 이 plan이 있냐? 고 묻는 경우다. 키는 SHA-1 하나.
성공하면 fix된 엔트리를 반환한다. 모드 플래그
XASL_CACHE_SEARCH_FOR_PREPARE vs XASL_CACHE_SEARCH_FOR_EXECUTE
가 RT 검사의 어느 가지를 탈지 결정한다.
// xcache_find_sha1 — src/query/xasl_cache.c (condensed)*xcache_entry = xcache_Hashmap.find (thread_p, lookup_key);if (*xcache_entry == NULL) { XCACHE_STAT_INC (miss); return NO_ERROR; }xcache_Hashmap.end_tran (thread_p); XCACHE_STAT_INC (hits);
if (rt_check) { if (search_mode == XASL_CACHE_SEARCH_FOR_PREPARE && ((*xcache_entry)->xasl_id.cache_flag & XCACHE_ENTRY_RECOMPILED_REQUESTED) != 0) *rt_check = XASL_CACHE_RECOMPILE_PREPARE; else if (xcache_check_recompilation_threshold (thread_p, *xcache_entry)) { xcache_unfix (thread_p, *xcache_entry); *xcache_entry = NULL; *rt_check = (search_mode == XASL_CACHE_SEARCH_FOR_EXECUTE) ? XASL_CACHE_RECOMPILE_EXECUTE : XASL_CACHE_RECOMPILE_PREPARE; } }Execute 경로 — xcache_find_xasl_id_for_execute. 클라이언트가
앞서 받은 XASL_ID 를 들고 와 실행 가능한 plan을 요청하는 경우다.
순서대로 네 단계를 거친다.
- SHA-1로 찾기.
xcache_find_sha1(FOR_EXECUTE 모드) 호출. time_stored검증. 클라이언트가 캐싱했던(sha1, time_stored)가 어긋나면, 그 사이 엔트리가 재컴파일로 교체된 것이다. unfix하고 null을 반환해 재 prepare를 강제한다.- 객체 lock 획득.
related_objects를 순회하며 각 OID에lock_object호출. 필수 단계다. 지금 schema-modify lock 보유자가 엔트리를 무효화하고 있을 수 있다. lock을 잡은 뒤XCACHE_ENTRY_MARK_DELETED를 재확인한다. - Clone 획득.
cache_clones에서 deserialise된 plan을 pop 시도하고, 비어 있으면stx_map_stream_to_xasl로stream.buffer에서 deserialise한다. 이 비용은xcache_Memory_usage_clone에 가산 된다.
// xcache_find_xasl_id_for_execute — src/query/xasl_cache.c (condensed)xcache_find_sha1 (thread_p, &xid->sha1, XASL_CACHE_SEARCH_FOR_EXECUTE, xcache_entry, &recompile_due_to_threshold);if ((*xcache_entry)->xasl_id.time_stored.sec != xid->time_stored.sec || (*xcache_entry)->xasl_id.time_stored.usec != xid->time_stored.usec) { xcache_unfix (thread_p, *xcache_entry); *xcache_entry = NULL; return NO_ERROR; }
for (oid_index = 0; oid_index < (*xcache_entry)->n_related_objects; oid_index++) lock_object (thread_p, &(*xcache_entry)->related_objects[oid_index].oid, oid_Root_class_oid, (*xcache_entry)->related_objects[oid_index].lock, LK_UNCOND_LOCK);
if (ATOMIC_INC_32 (&((*xcache_entry)->xasl_id.cache_flag), 0) & XCACHE_ENTRY_MARK_DELETED) { xcache_unfix (thread_p, *xcache_entry); *xcache_entry = NULL; return NO_ERROR; }
if (xcache_uses_clones () && (*xcache_entry)->n_cache_clones > 0) *xclone = (*xcache_entry)->cache_clones[--(*xcache_entry)->n_cache_clones]; /* clone hit */else stx_map_stream_to_xasl (thread_p, &xclone->xasl, use_xasl_clone, /* deserialise */ (*xcache_entry)->stream.buffer, (*xcache_entry)->stream.buffer_size, &xclone->xasl_buf);삽입 — xcache_insert 와 재컴파일 루프
섹션 제목: “삽입 — xcache_insert 와 재컴파일 루프”엔트리를 추가하는 경로는 xcache_insert 뿐이다. 형태는 claim-init-CAS
이지만, 한 가지 트위스트가 있다. 재컴파일이 진행 중이라면 새 엔트리
를 to-be-recompiled 상태의 옛 엔트리 옆에 설치한 뒤, 옛 엔트리를
원자적으로 was recompiled 상태로 승격시킨다.
// xcache_insert — src/query/xasl_cache.c (condensed)XASL_ID_SET_NULL (&xid); xid.sha1 = context->sha1;
while (true) { *xcache_entry = xcache_Hashmap.freelist_claim (thread_p); /* init: copy sha1, related_objects, stream, sql_info; fix_count = 1 */ inserted = xcache_Hashmap.insert_given (thread_p, xid, *xcache_entry);
if (inserted || !context->recompile_xasl) break;
/* mark existing as TO_BE_RECOMPILED via CAS on cache_flag */ do { cache_flag = (*xcache_entry)->xasl_id.cache_flag; new_cache_flag = cache_flag | XCACHE_ENTRY_TO_BE_RECOMPILED; } while (!XCACHE_ATOMIC_CAS_CACHE_FLAG (&(*xcache_entry)->xasl_id, cache_flag, new_cache_flag));
to_be_recompiled = *xcache_entry; *xcache_entry = NULL; xid.cache_flag = XCACHE_ENTRY_SKIP_TO_BE_RECOMPILED; /* next iter skips it */ }
if (to_be_recompiled != NULL) /* promote old → WAS_RECOMPILED */ { do { cache_flag = to_be_recompiled->xasl_id.cache_flag; new_cache_flag = (cache_flag & XCACHE_ENTRY_FIX_COUNT_MASK) | XCACHE_ENTRY_WAS_RECOMPILED; } while (!XCACHE_ATOMIC_CAS_CACHE_FLAG (&to_be_recompiled->xasl_id, cache_flag, new_cache_flag)); xcache_unfix (thread_p, to_be_recompiled); }
if (xcache_need_cleanup () != XCACHE_CLEANUP_NONE && xcache_Cleanup_flag == 0) xcache_cleanup (thread_p);skip 플래그 패턴은 동시 재컴파일을 처리하기 위한 것이다. 첫 호출자
는 TO_BE_RECOMPILED를 표시하고 진행하며, 두 번째 호출자는 진행 중인
재컴파일 옆에 자기 엔트리를 삽입하거나 잠시 백오프하고
(thread_sleep(1)) 다시 시도한다. 옛 엔트리는 그 포인터를 들고 있는
실행자가 남아 있는 한 계속 사용 가능하다.
재컴파일 임계값 — 통계 drift 감지
섹션 제목: “재컴파일 임계값 — 통계 drift 감지”xcache_check_recompilation_threshold 는 soft invalidation 경로다.
xcache_find_sha1 에서 엔트리 하나당 cooldown 한 번꼴로 호출되며,
related_objects 를 순회해 catalog_get_class_info 로 현재 heap
page 수를 가져온 뒤 컴파일 시점의 tcard 와 비교한다.
// xcache_check_recompilation_threshold — src/query/xasl_cache.c (condensed)if (crt_time.tv_sec - xcache_entry->time_last_rt_check < XCACHE_RT_TIMEDIFF_IN_SEC) return false;if (!ATOMIC_CAS_64 (&xcache_entry->time_last_rt_check, save_secs, crt_time.tv_sec)) return false;
for (relobj = 0; relobj < xcache_entry->n_related_objects; relobj++) { if (xcache_entry->related_objects[relobj].tcard < 0) continue; if (xcache_entry->related_objects[relobj].tcard >= XCACHE_RT_MAX_THRESHOLD) continue; cls_info_p = catalog_get_class_info (thread_p, &xcache_entry->related_objects[relobj].oid, NULL); npages = cls_info_p->ci_tot_pages; if (npages > XCACHE_RT_FACTOR * xcache_entry->related_objects[relobj].tcard || npages < xcache_entry->related_objects[relobj].tcard / XCACHE_RT_FACTOR) if (xcache_entry_set_request_recompile_flag (thread_p, xcache_entry, true)) recompile = true; catalog_free_class_info_and_init (cls_info_p); }상수는 다음과 같다. XCACHE_RT_TIMEDIFF_IN_SEC = 360(엔트리당
cooldown), XCACHE_RT_MAX_THRESHOLD = 10000(10k page 이상 테이블은
RT 건너뛴다. 거대 테이블에서 상대적 drift는 무의미하다),
XCACHE_RT_FACTOR = 10(10배 이상 변하면 트리거).
임계값을 넘은 경우 함수가 하는 일은 XCACHE_ENTRY_RECOMPILED_REQUESTED
세팅뿐이다. 즉시 무효화하지는 않는다. 다음 prepare가
ER_QPROC_XASLNODE_RECOMPILE_REQUESTED 를 받으면, 클라이언트는
recompile 비트와 함께 다시 prepare하며, 새 컴파일이 xcache_insert
의 재컴파일 루프로 옛 plan을 교체한다. Soft invalidation이라는
점이 핵심이다. 진행 중인 execute는 unfix할 때까지 옛 plan을 그대로
사용한다.
DDL 무효화 — 클래스별 삭제
섹션 제목: “DDL 무효화 — 클래스별 삭제”클래스에 대한 DDL은 xcache_remove_by_oid (thread_p, oid) 를 호출
한다. 이 함수는 실제 작업을 xcache_invalidate_entries 에 위임하고,
이 내부 함수가 해시맵을 (xcache_hashmap_iterator 로) 순회하면서
일치하는 모든 엔트리를 xcache_entry_mark_deleted 로 표시한 뒤,
키를 1024 원소짜리 delete_xids[] 버퍼에 모은다(lock-free 해시맵
transaction이 한 번에 엔트리 하나만 건드릴 수 있기 때문이다). 배치
가 끝날 때마다 erase하며, 다 비워질 때까지 반복한다. 같은 엔진을
공유하는 변종이 셋이다.
xcache_remove_by_oid—related_objects와 OID를 매칭(DDL).xcache_remove_by_sha1—xasl_id.sha1을 매칭(관리자 명령DROP CACHED PLAN <sha1>).xcache_drop_all— null 검사로 모든 엔트리 매칭.
DDL hook은 locator_sr.c 에 산다. 모든 카탈로그 갱신이 schema-modify
를 마무리하고 나면 locator가 이 캐시 hook을 호출한다. 호출 지점은
세 곳(locator_sr.c:5674, :6297, :13933)에 더해 ALTER SERIAL을
위한 serial.c:1422 가 있다. Serial은 related_objects 안에
클래스와 함께 등장하므로, plan이 바인딩한 serial을 변경하면 plan
이 무효화되어야 한다.
flowchart TB
subgraph CACHE["xcache_Hashmap"]
E1["엔트리 A<br/>related_objects = [c1, c2]"]
E2["엔트리 B<br/>related_objects = [c2, c3]"]
E3["엔트리 C<br/>related_objects = [c3]"]
E4["엔트리 D<br/>related_objects = [c1, s4]"]
end
DDL_C2["ALTER TABLE c2..."] --> XRBO["xcache_remove_by_oid(c2)"]
XRBO --> SCAN["전체 엔트리 스캔"]
SCAN --> E1
SCAN --> E2
SCAN --> E3
SCAN --> E4
E1 -.->|c2 ∈ list → MARK_DELETED| INV1["무효화"]
E2 -.->|c2 ∈ list → MARK_DELETED| INV2["무효화"]
E3 -.->|c2 ∉ list → keep| KEEP3["유지"]
E4 -.->|c2 ∉ list → keep| KEEP4["유지"]
Eviction — binary heap LRU
섹션 제목: “Eviction — binary heap LRU”xcache_cleanup 은 xcache_insert 안에서 xcache_need_cleanup() 이
non-NONE 을 반환할 때 호출된다. 이유는 둘이다. FULL_MEMORY
(memory_usage_cache + memory_usage_clone > soft_limit — hard의
80%) 와 TIMEOUT(마지막 cleanup 이후 6시간 경과 —
PRM_ID_XASL_CACHE_TIME_THRESHOLD_IN_MINUTES).
Cleanup은 두 단계로 진행된다. Collect. 해시맵을 순회해서
cache_flag == 0 인 엔트리(unfixed, 플래그 없음)를 수집한다.
FULL_MEMORY는 time_last_used 기반 binary heap에 push해 가장 오래된
K개를 남기고, TIMEOUT은 마지막 사용 시점이 time_threshold 초 이전
인 항목만 평면 배열에 모은다. Erase. 각 후보를
xid.cache_flag = XCACHE_ENTRY_CLEANUP 을 세팅하고
xcache_Hashmap.erase 를 호출한다. 비교 함수는 CLEANUP 플래그를
인지하고, 엔트리가 여전히 unfixed이고 다른 플래그가 없을 때에만
DELETED_BY_ME 로 CAS한다. cleanup은 진행 중인 reader나 recompiler
와의 경쟁에서 결코 이기지 않는다. 졌으면 다음 후보로 넘어갈 뿐이다.
동시에 도는 cleanup은 한 번에 하나로 제한된다. 이를 위한 게이트가
xcache_Cleanup_flag 에 대한 atomic CAS다. 진행 중인 cleanup을 본
다른 inserter는 트리거를 건너뛴다. 블로킹은 없다.
End-to-end PREPARE / EXECUTE 흐름
섹션 제목: “End-to-end PREPARE / EXECUTE 흐름”sequenceDiagram
participant CL as 클라이언트
participant SQ as sqmgr_prepare
participant XQ as xqmgr_prepare
participant XC as xasl_cache
participant EX as xqmgr_execute
CL->>CL: PT_NODE_PRINT_TO_ALIAS<br/>· SHA1Compute
CL->>SQ: PREPARE (sql, sha1, [stream]?)
SQ->>XQ: xqmgr_prepare_query
XQ->>XC: xcache_find_sha1(sha1, FOR_PREPARE)
alt 캐시 적중, RT 미발동
XC-->>XQ: 엔트리(fixed) → unfix → XASL_ID 반환
else 캐시 적중, RT 발동
XC-->>XQ: 엔트리 + RT_PREPARE → recompile_xasl 설정
else 캐시 miss
XC-->>XQ: NULL → 클라이언트가 컴파일 후 stream 송신
XQ->>XC: xcache_insert(stream, related_objects)
end
SQ-->>CL: XASL_ID (sha1 + time_stored)
CL->>EX: EXECUTE (XASL_ID, params)
EX->>XC: xcache_find_xasl_id_for_execute
XC->>XC: find_sha1 → time_stored 검증 → lock_object<br/>→ clone pop OR stx_map_stream_to_xasl
XC-->>EX: 엔트리 + XASL_CLONE
EX-->>CL: list_id (결과)
EX->>XC: xcache_retire_clone + xcache_unfix
첫 prepare는 클라이언트 측 전체 컴파일 파이프라인(파서 →
semantic-check → 옵티마이저 → XASL generator → 직렬화 → 송신)에
서버 측 xcache_insert 까지 모두 돈다. 같은 SHA-1에 대한 이후
prepare는 해시맵 조회 한 번으로 캐시된 XASL_ID 를 돌려받는다.
Execute는 XASL_ID 로 다시 찾고, lock을 잡고, deserialise 또는
clone pop을 거쳐 실행한다. 빠른 경로는 CAS 두 번에 mutex로
보호된 clone pop 한 번이라 — 마이크로초 단위지만, 느린 경로는 ms
에서 수백 ms에 이른다. 이 비율 자체가 캐시를 부담을 짊어진
load-bearing 컴포넌트로 만든다.
자매 캐시 — filter-predicate 캐시와 function-index 캐시
섹션 제목: “자매 캐시 — filter-predicate 캐시와 function-index 캐시”같은 설계를 공유하는 자매 캐시 둘이 있다.
- Filter-predicate 캐시(
fpcache):src/query/filter_pred_cache.c. Partial / function-based 인덱스를 위한 컴파일된 predicate를 캐싱한다. SHA-1 키 기반 latch-free 해시맵, 클래스별 무효화는fpcache_remove_by_class(locator_sr.c안에서 XASL 무효화 옆에 나란히 호출된다). - Function-index 표현식 캐시: 더 좁은 범위로, function-based 인덱스의 컴파일된 키 표현식을 캐싱한다.
이 둘은 xqmgr_drop_all_query_plans 가 XASL 캐시와 함께 떨어뜨린다.
한편 XASL 엔트리의 list_ht_no 슬롯은 세 번째 캐시인 qfile 결과
캐시로 연결된다. 이 캐시는 plan의 출력 을 파라미터 모양에 따라
메모이즈한다. 셋을 합쳐 다룰 cubrid-runtime-memoization.md 가
계획되어 있다.
세션별 prepared statement 캐시
섹션 제목: “세션별 prepared statement 캐시”CUBRID의 세션 모듈(src/session/session.c)은 XASL 캐시와는 다른
연결별 레지스트리를 유지한다.
// PREPARED_STATEMENT — src/session/session.cstruct prepared_statement{ char *name; /* user-given name, e.g. "stmt1" */ char *alias_print; /* normalised SQL text */ SHA1Hash sha1; /* same SHA-1 as the XASL cache key */ int info_length; char *info; /* serialised statement metadata */ PREPARED_STATEMENT *next;};SESSION_STATE::statements 에 단일 연결 리스트가 매달려 있고, 상한은
MAX_PREPARED_STATEMENTS_COUNT = 20 이다. API는
session_create_prepared_statement,
session_get_prepared_statement,
session_delete_prepared_statement.
두 캐시는 비대칭이다. 세션 레지스트리는 세션별 이름 → SHA-1 + statement 메타데이터(plan은 없음)를 매핑하고, XASL 캐시는 SHA-1 → plan stream(서버 전역)을 매핑한다. 두 클라이언트가 같은 문장을 서로 다른 이름으로 prepare해도 동일한 SHA-1을 만들어 내며, 공유된 XASL 엔트리 위에 수렴한다. 세션 레지스트리 엔트리는 따로 존재하더라도 말이다. 따라서 XASL 캐시는 연결이 끊어져도 살아남는다. 세션을 닫으면 레지스트리는 비워지지만 XASL 엔트리는 남아 있어, 같은 문장을 prepare하는 다른 세션이 그대로 재사용한다.
메모리 예산과 파라미터
섹션 제목: “메모리 예산과 파라미터”시스템 파라미터는 다음과 같다. xasl_cache_max_entries 가
xcache_Soft_capacity(해시맵 초기 크기)를 정한다.
xasl_cache_time_threshold_in_minutes 는 캐시 단위 cleanup cooldown
(기본 360분 = 6시간). xasl_cache_max_clones 는 엔트리당 clone 상한.
xasl_cache_logging 은 xcache_log 토글. Hard 메모리 한계는
xcache_Soft_capacity * 1024 * 128 이고, soft는 hard의 80%, plan
하나당 상한은 (hard − soft) / UNPACK_SCALE.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”서브시스템별로 묶은 심볼들이다(파일이 src/query/xasl_cache.c 가
아닌 경우만 별도 표기).
- 타입 / 전역.
XASL_ID(storage_common.h),XASL_CACHE_ENTRY,XCACHE_RELATED_OBJECT,XASL_CLONE,EXECUTION_INFO(xasl_cache.h);xcache_hashmap_type,xcache_Global,xcache_*매크로,XCACHE_STATS,XCACHE_CLEANUP_CANDIDATE,xcache_Entry_descriptor. - 상태 플래그 / 상수.
XCACHE_ENTRY_MARK_DELETED,_TO_BE_RECOMPILED,_WAS_RECOMPILED,_SKIP_TO_BE_RECOMPILED,_CLEANUP,_RECOMPILED_REQUESTED,_FLAGS_MASK,_FIX_COUNT_MASK,_DELETED_BY_ME;XCACHE_ATOMIC_CAS_CACHE_FLAG. RT:XCACHE_RT_TIMEDIFF_IN_SEC,_MAX_THRESHOLD,_FACTOR,_CLASS_STAT_NEED_UPDATE. - Lifecycle.
xcache_initialize,xcache_finalize,xcache_entry_alloc/_free/_init/_uninit,xasl_cache_ent::xasl_cache_ent/~xasl_cache_ent,init_clone_cache. - 해시 / 키 비교.
xcache_hash_key,xcache_compare_key,xcache_copy_key.xasl.h의 매크로:XASL_ID_SET_NULL,XASL_ID_COPY,XASL_ID_EQ,OR_PACK_XASL_ID,OR_UNPACK_XASL_ID. - Find / insert / unfix.
xcache_find_sha1,xcache_find_xasl_id_for_execute,xcache_insert,xcache_unfix,xcache_entry_mark_deleted,xcache_entry_set_request_recompile_flag. - 무효화.
xcache_remove_by_oid,xcache_remove_by_sha1,xcache_drop_all,xcache_invalidate_qcaches,xcache_invalidate_entries,xcache_entry_is_related_to_oid,xcache_entry_is_related_to_sha1,xcache_check_recompilation_threshold. - Eviction.
xcache_cleanup,xcache_need_cleanup,xcache_compare_cleanup_candidates,XCACHE_CLEANUP_{RATIO,MIN_NUM_ENTRIES,NUM_ENTRIES}. - Plan clone.
xcache_uses_clones,xcache_retire_clone,xcache_clone_decache. - 사이즈 / 진단.
xcache_entry_get_entrysize,_get_clonesize,_get_one_clonesize;xcache_dump,xcache_get_entry_count,xcache_can_entry_cache_list,xcache_check_logging,xcache_log,xcache_log_error. - 서버 prepare/execute 통합.
xqmgr_prepare_query,xqmgr_execute_query,xqmgr_drop_all_query_plans,xqmgr_drop_query_plans_by_sha1,xqmgr_dump_query_plans,qmgr_clear_relative_cache_entries,qmgr_is_related_class_modified(모두query_manager.c). - 네트워크 통합.
sqmgr_prepare_query,sqmgr_execute_query,sqmgr_prepare_and_execute_query,sqmgr_drop_all_query_plans,sqmgr_drop_query_plans_by_sha1(network_interface_sr.cpp);net_prepare_query(network_interface_cl.c). - 파서 / executor 통합.
SHA1Compute와PT_NODE_PRINT_TO_ALIAS (.. CUSTOM_PRINT_4_SHA_COMPUTE | PT_PRINT_LOWER)inexecute_statement.c::do_execute_{select,update,delete,insert,merge};stx_map_stream_to_xasl(stream_to_xasl.c). - DDL hook(
xcache_remove_by_oid호출자).locator_sr.c:5674,:6297,:13933;serial.c:1422. - 세션별 prepared statement(자매 시스템).
PREPARED_STATEMENT(session.c),SESSION_STATE::statements,MAX_PREPARED_STATEMENTS_COUNT = 20,session_{create,get,delete,free,dump}_prepared_statement.
위치 힌트(2026-05-01 기준). 라인 번호는 시간이 지나면 어긋난다. 정식 앵커는 심볼 이름이며, 라인은 점차 부패하는 힌트로 다룬다.
| 심볼 | 파일 | 라인 |
|---|---|---|
XASL_ID 구조체 | src/storage/storage_common.h | 910 |
XASL_CACHE_ENTRY 구조체 | src/query/xasl_cache.h | 91 |
XCACHE_ENTRY_* 플래그 상수 | src/query/xasl_cache.c | 49–58 |
XCACHE_RT_* 상수 | src/query/xasl_cache.c | 217–222 |
xcache_Entry_descriptor | src/query/xasl_cache.c | 186 |
xcache_initialize | src/query/xasl_cache.c | 303 |
xcache_compare_key | src/query/xasl_cache.c | 614 |
xcache_hash_key | src/query/xasl_cache.c | 768 |
xcache_find_sha1 | src/query/xasl_cache.c | 818 |
xcache_find_xasl_id_for_execute | src/query/xasl_cache.c | 915 |
xcache_unfix | src/query/xasl_cache.c | 1136 |
xcache_entry_mark_deleted | src/query/xasl_cache.c | 1235 |
xcache_insert | src/query/xasl_cache.c | 1406 |
xcache_invalidate_entries | src/query/xasl_cache.c | 1801 |
xcache_remove_by_oid | src/query/xasl_cache.c | 1988 |
xcache_drop_all | src/query/xasl_cache.c | 2058 |
xcache_retire_clone | src/query/xasl_cache.c | 2239 |
xcache_cleanup | src/query/xasl_cache.c | 2311 |
xcache_check_recompilation_threshold | src/query/xasl_cache.c | 2575 |
xqmgr_prepare_query | src/query/query_manager.c | 1003 |
xqmgr_execute_query | src/query/query_manager.c | 1298 |
sqmgr_prepare_query | src/communication/network_interface_sr.cpp | 5107 |
net_prepare_query SHA-1 패킹 | src/communication/network_interface_cl.c | 6786 |
SHA1Compute (do_execute_update) | src/query/execute_statement.c | 9378 |
xcache_remove_by_oid (locator) | src/transaction/locator_sr.c | 5674, 6297, 13933 |
xcache_remove_by_oid (serial) | src/query/serial.c | 1422 |
PREPARED_STATEMENT / session_*_prepared_* | src/session/session.c | 92, 1751, 1863, 1942 |
compile_context | src/xasl/compile_context.h | 37 |
소스 검증 및 비교 노트
섹션 제목: “소스 검증 및 비교 노트”vs cubrid-server-session.md. 세션 레지스트리는
이름 → SHA-1 + statement 정보 를 매핑하고, XASL 캐시는
SHA-1 → plan stream 을 매핑한다. DEALLOCATE PREPARE stmt1 은 세션
엔트리만 비우고, 연결 종료는 레지스트리만 비우며 XASL 캐시는
손대지 않는다. XASL 캐시는 서버 재시작, DROP CACHED PLAN, DDL,
eviction이 발생해야만 죽는다.
vs cubrid-xasl-generator.md. Generator는 클라이언트에서
in-memory XASL_NODE 트리를 만들고, xts_map_xasl_to_stream 으로
직렬화해 캐시가 저장하는 byte buffer를 만든다. 캐시 적중 시
stx_map_stream_to_xasl 가 트리를 재구성한다. Generator와 캐시는
서로 분리되어 있다. 캐시는 stream을 SHA-1과 related_objects 를
가진 불투명한 바이트 묶음으로 다룰 뿐이다. 의존성 리스트는
generator가 채운다(각 ACCESS_SPEC이 자기 클래스 OID를 더한다)고
이해하면 된다. 그렇게 stream과 함께 송신된다.
vs cubrid-catalog-manager.md. DDL 무효화 hook은 카탈로그
매니저가 아니라 locator 에 산다. locator가 schema-modify lock을
들고 있는 chokepoint이며 따라서 이 OID가 방금 바뀌었다 를 가장
잘 아는 위치이기 때문이다. XASL 캐시는 locator가 schema-modify
lock을 풀기 전에 hook을 호출하는 규율에 의존한다. 그 규율이
무너지면 stale plan이 살아남게 된다.
소스 버전 drift. 위치 힌트 표의 라인 번호는 updated: 시점의
트리를 반영한다. 심볼 이름은 안정적이며, 라인 번호는 부패하는
힌트로 다룬다.
메모리 회계. Hard cap은 xcache_Soft_capacity * 1024 * 128,
soft cap은 hard의 80%, plan별 상한은 (hard - soft) / UNPACK_SCALE.
대부분의 plan이 KB 단위이므로, 메모리 압박이 eviction을 트리거하기
전까지는 보통 xcache_Soft_capacity 보다 훨씬 많은 엔트리를 들고
있게 된다.
두 개의 cooldown, 단위가 다르다. 엔트리당 RT cooldown
XCACHE_RT_TIMEDIFF_IN_SEC 는 360초 다. 캐시 단위 cleanup
cooldown xasl_cache_time_threshold_in_minutes 의 기본값은
360분(6시간) 이다. 숫자는 같지만 단위가 다르다. 코드를
훑을 때 흔히 헷갈리는 지점이다.
xcache_invalidate_qcaches vs xcache_invalidate_entries.
앞쪽은 OID에 닿는 엔트리들의 result-list 캐시 만 비운다
(qfile_clear_list_cache). commit 시점에 stale 결과를 떨어뜨리되
plan은 살리는 경로다. 뒤쪽이 실제로 XASL 엔트리를 삭제 표시한다.
가볍게 읽으면 둘을 혼동하기 쉽다.
열린 질문
섹션 제목: “열린 질문”- Parameter sniffing이나 파라미터 모양 변형은 다루지 않는다. 키는 정규화 SQL의 SHA-1 하나뿐이다. 같은 파라미터를 셀렉티 비티가 극단적으로 다른 두 실행이 한 plan을 공유한다. Oracle과 SQL Server는 parent/child cursor나 plan guide로 이를 감지하지만, CUBRID에는 시간에 따른 cardinality drift에서만 발동하는 RT 재컴파일밖에 없다. 실행별 파라미터 skew는 잡지 못한다.
- 테넌트 / 데이터베이스별 분리가 없다.
xcache_Global은 단 하나의 전역이다. 멀티 테넌트 환경에서는 캐시가 모든 테넌트에 걸쳐 공유되며, 워크로드 조합에 따라 cross-tenant eviction이나 RT poisoning이 일어날 여지가 있다. - Off-heap / NUMA-aware 캐시? 모든 엔트리는 malloc 메모리이며, 핫 패스에 NUMA 인지가 없다.
- 버킷 선택은
sha1.h[0](32비트)만 사용한다. 전체 비교는 160비트를 모두 사용하지만, 10만 엔트리 규모에서 prefix 충돌은 무시할 수준이 아니다. 그 부하 아래에서의 chain 길이는 저장소의 테스트로는 측정되어 있지 않다. time_stored의 해상도는 마이크로초. 같은 SHA-1를 같은 마이크로초에 두 번 재컴파일되는 이론적 엣지 케이스는 충돌한다. 재컴파일 루프는 엔트리당 동시 재컴파일이 한 건이라는 가정 위에서 돌아간다.- Filter-predicate 캐시와의 협조. 두 캐시는 DDL를 함께
무효화되지만 eviction은 독립이다. 실제 운영에서 stranded fpcache
엔트리가 발생하는지는
xasl_cache.c만 봐서는 알 수 없다.
코드: src/query/xasl_cache.{c,h}, query_manager.c,
execute_statement.c, xasl.h, serial.c;
src/storage/storage_common.h; src/xasl/compile_context.h;
src/communication/network_interface_{sr.cpp,cl.c};
src/session/session.c; src/transaction/locator_sr.c.
교과서 / 논문 참고:
- Petrov, Database Internals (O’Reilly, 2019), 12장.
- Hellerstein, Stonebraker, Hamilton, Architecture of a Database System (FnT Databases 1(2), 2007).
- Graefe, Query Evaluation Techniques for Large Databases (ACM CSUR 25(2), 1993).
- Mohan et al., ARIES (ACM TODS 17(1), 1992) — buffer manager reader-writer 프로토콜의 같은 가족.
- Fraser, Practical Lock-Freedom (Cambridge UCAM-CL-TR-579, 2004) — epoch 기반 회수.
비교 엔진: PostgreSQL
src/backend/utils/cache/plancache.c(백엔드 단위, 이름 키, relcache
콜백); Oracle Library Cache(서버 전역, 해시 키, parent/child cursor);
SQL Server plan cache(해시 키, parameter sniffing에 취약).
Knowledge-base 교차 참조:
cubrid-xasl-generator.md(xts_map_xasl_to_stream 이 이 캐시가
저장하는 stream을 만든다), cubrid-server-session.md(세션별
prepared 레지스트리), cubrid-catalog-manager.md(DDL hook이 발화
되는 카탈로그 경로), cubrid-query-optimizer.md(generator의 입력),
cubrid-page-buffer-manager.md(평행 캐시 패턴: 참조 카운트 엔트리,
지연 회수, binary heap LRU, lock-free 식별자 해시맵).