콘텐츠로 이동

(KO) CUBRID Page Buffer Manager — BCB, 3-zone LRU, private quota, direct victim 인계, custom latch

목차

페이지 버퍼 (page buffer; 또는 buffer pool) 는 디스크와 DBMS 의 다른 모든 모듈 사이에 놓인 메모리 캐시다. 디스크 I/O 는 메모리 접근보다 몇 자릿수 더 느리다. 그래서 엔진은 최근에 건드린 페이지의 사본을 RAM 에 두고, 모든 읽기와 쓰기를 그 사 본으로 한다. Database System Concepts (Silberschatz, Korth, Sudarshan, 6판) 13장 Storage and File Structure 는 버퍼를 다른 모듈이 디스크 지연이 없는 척하게 해 주는 단위 로 설명 한다. 대부분의 페이지는 대부분의 시간 동안 메모리에 머문다. 그 환상을 유지하는 컴포넌트가 buffer manager 다.

교재가 정리한 버퍼 매니저의 네 가지 핵심 요소는 다음과 같다.

  1. 페이지 테이블 (page table). 디스크 페이지 식별자를 메 모리 슬롯으로 매핑하는 해시 테이블. 모든 페이지 읽기 경로 의 핫 패스다.
  2. 자유 / 교체 리스트 (free / replacement list). 요청된 페이지가 버퍼에 없을 때, 빈 슬롯을 찾거나 victim 을 골라 eviction 한다. 고전 알고리즘은 LRU 다. clock, ARC, 2Q, LRU-K 같은 변형은 정확도와 관리 비용을 trade-off 한 다.
  3. Fix / unfix 프로토콜. 페이지를 읽거나 쓰려는 스레드는 먼저 버퍼 슬롯을 fix 한다. fix 는 슬롯의 참조 카운터를 1 증가시키고 페이지 래치 (page latch; read 또는 write) 를 잡는 일을 함께 한다. fix 된 동안에는 슬롯이 evict 되지 않 는다. unfix 는 래치를 풀고 카운터를 1 줄이는 반대 동작이 다. 이 page latch 는 트랜잭션 잠금 (lock) 과 다르다 — 짧게만 잡히고, 슬롯에 내장되어 있고, 격리 (isolation) 와 무관하다 (cubrid-lock-manager.md 의 §“Lock vs latch separation” 참고).
  4. Write-Ahead Logging (WAL) 정합. 마지막 flush 이후 수정 된 dirty 페이지는 그 수정 내역을 담은 로그 레코드가 디스 크에 먼저 flush 된 뒤가 아니면 디스크에 쓸 수 없다. 표준 인용처는 Mohan et al. ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging (TODS 1992) 이다. buffer manager 와 log manager 사이에는 단방향 순서 제약이 있다 — log 가 먼저 디스크에 가고, page 는 그 다음에 간다.

여기에 더해 모든 현대 구현이 공유하는 두 가지 아키텍처 요소가 있다.

  • 고동시성 환경에서의 free-page 조정. 여러 스레드가 동시 에 victim 을 찾을 때 단일 락으로 LRU 리스트를 보호하면 모든 작업이 직렬화되어 버린다. 그래서 실제 시스템은 리스트를 여 러 작은 리스트로 쪼갠다. 각 리스트마다 별도의 락을 두거나, 아예 lock-free 로 운영한다.
  • Background flusher 와 double-write. 페이지는 evict 될 때만 flush 되는 것이 아니라 background daemon 에 의해서도 flush 된다 (PostgreSQL bgwriter, MySQL 의 innodb_io_capacity flusher, CUBRID 의 page flush daemon). 일부 엔진은 OS 의 디스크 페이지 크기보다 DB 의 페이지 크기 가 더 큰 환경에서 발생할 수 있는 torn write 를 복구하기 위해, 페이지를 실제 위치에 쓰기 전에 double-write buffer 에도 미리 써 둔다.

이 문서는 위 요소들을 CUBRID 가 src/storage/page_buffer.{h,c} 와 관련 src/storage/double_write_buffer.{hpp,cpp} 에서 어떻게 구현했 는지를 차례로 따라간다.

DBMS 공통 설계 패턴 (Common DBMS Design)

섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”

교재가 모델을 준다면, 이 섹션은 거의 모든 DBMS buffer manager 가 어떤 형태로든 채택하는 공학적 관행 의 이름을 모은다. PostgreSQL, Oracle, MySQL InnoDB, SQL Server, CUBRID 가 모두 이 패턴을 공유한다. 다음 섹션 ## CUBRID의 구현 은 발명이 아 니라, 이 공유 설계 공간 위에서 선택된 다이얼 조합으로 읽으면 된다.

Buffer Control Block (BCB) — 페이지 메타데이터 + 슬롯 자체

섹션 제목: “Buffer Control Block (BCB) — 페이지 메타데이터 + 슬롯 자체”

버퍼 풀은 고정 크기 슬롯의 배열이고, 슬롯 하나가 캐시된 페이 지 하나에 대응한다. 각 슬롯은 작은 control block 으로 감 싸여 있다. 이 블록에는 페이지 식별자, 래치 상태, fix count, dirty bit, LRU 위치, 그리고 슬롯이 참여하는 여러 리스트의 포 인터들이 들어 있다. 이 블록의 이름은 엔진마다 다르다 — PostgreSQL 은 BufferDesc, Oracle 은 buffer header, CUBRID 는 PGBUF_BCB 라 부른다. 실제 페이지 바이트가 들어가는 데이 터 영역은 메타데이터를 담는 컨트롤 영역과 분리되어 있다. 페 이지 본체를 OS 와 디스크의 페이지 크기에 맞춰 정렬해 I/O 효율 을 확보하기 위해서다.

Page table = VPID 에서 슬롯으로 가는 해시

섹션 제목: “Page table = VPID 에서 슬롯으로 가는 해시”

해시 테이블이 디스크 페이지 식별자를 BCB 로 매핑한다. CUBRID 는 키로 VPID = (volid, pageid) 를, PostgreSQL 은 BufferTag = (rel, fork, blocknum) 을 쓴다. 키의 모양은 본질 적으로 같다. 크기는 서버 시작 시점에 한 번 정해진다. CUBRID 의 경우 2²⁰ 버킷으로 고정되며, 충돌은 chaining 으로 처리한다. 이 테이블은 모든 페이지 접근의 핫 패스라서 컨텐션을 줄이려고 적극적으로 분할 (partitioning) 되는 것이 일반적이다.

Invalid (free) list = 비어 있는 슬롯 풀

섹션 제목: “Invalid (free) list = 비어 있는 슬롯 풀”

페이지가 매핑되어 있지 않은 슬롯들을 모아 둔 작은 단일 연결 리스트다. 새 요청은 이 리스트를 우선 쓴다. eviction (victim 선정) 은 이 리스트가 비었을 때만 동작한다. “사용되지 않은 슬 롯 경로와 누군가를 evict 하는” 경로를 따로 두는 것은 보편적 인 최적화다. 흔한 경우의 할당 비용을 한 자릿수 이상 줄여 주기 때문이다.

Multi-zone LRU = 최근성을 인지하는 eviction

섹션 제목: “Multi-zone LRU = 최근성을 인지하는 eviction”

순수 LRU 는 sequential flooding 에 취약하다. 큰 스캔 한 번 이 DB 의 모든 페이지를 건드리면, 핫 working set 이 통째로 밀 려난다. 그래서 실제 시스템은 LRU 를 두세 개의 zone 으로 나눈 다. 최근에 접근된 페이지는 hot zone 에 살고, 일정 기간 다시 접근되지 않으면 중간 warm zone 으로 떨어진다. 더 오래되면 차가운 cold zone 으로 내려간다. eviction 대상이 되는 것은 cold zone 까지 떨어진 페이지뿐이다. CUBRID 는 zone 을 셋으로 나눈다 (LRU 1 / 2 / 3 = top / middle / bottom). PostgreSQL 의 clock-sweep, MySQL InnoDB 의 mid-point insertion strategy, Oracle 의 touch-count 모두 같은 발상에 다른 이름을 붙인 것이 다.

Worker 별 (private) LRU — 스캔 flooding 에 대한 방어

섹션 제목: “Worker 별 (private) LRU — 스캔 flooding 에 대한 방어”

워커 스레드가 동시 다발로 돌면 단일 공유 LRU 리스트는 컨텐션 병목이자 정보 병목 이 된다. 한 워커의 풀 테이블 스캔이 다른 워커의 hot working set 을 망쳐 버리기 때문이다. 해결책은 스 레드 단위 (private) LRU 리스트 를 두는 것이다. 각 워커는 우 선 자기 LRU 리스트 안에서만 페이지를 promote 하고 evict 한 다. 페이지가 그 워커의 로컬 교체 압력을 견디고 살아남았을 때 에만 shared LRU 리스트로 옮겨 간다. CUBRID 의 private LRU 에 는 최근 hit 활동에 따라 적응적으로 조정되는 quota 가 추 가로 붙는다.

버퍼가 압박을 받을 때는 여러 스레드가 동시에 victim 을 찾는 다. 이 탐색을 단일 락으로 조정하면 코어 수가 늘수록 재앙이 된다. 표준 해법은 lock-free circular queue 다. victim 후 보가 한 개 이상 있는 LRU 리스트들만 모아 둔 큐다. 스레드는 큐에서 리스트를 꺼내 스캔하고, 경합이 발생하면 다음 리스트로 넘어간다. CUBRID 에서는 이를 LFCQ 라고 부른다. 같은 자료 구조 가 여러 엔진에서 반복적으로 등장하는 패턴이다.

private LRU 와 LFCQ 를 갖춰도 즉시 가용한 후보가 없는 순간은 여전히 발생한다. 단순한 답은 spin 을 돌거나 전역 condition variable 위에서 sleep 하는 방법이다. 그보다 더 나은 답이 direct victim 인계 다. 슬롯을 해제 한 스레드 (예: flush 가 끝난 BCB 를 들고 있는 스레드) 가 sleep 중인 다른 스레드에 게 그 슬롯을 직접 넘기는 방식이다. 인계 지점은 정적 배열의 스레드 단위 슬롯이다. CUBRID 는 이를 우선순위 큐 두 개 (High / Low) 로 구현해서, vacuum worker 와 재시도 중인 alloc 에게 우선권을 준다.

OS mutex 는 빠른 경로에서도 수백 나노초가 걸린다. 모든 페이지 읽기와 쓰기마다 잡히는 page latch 에서는 그 비용이 지배적이 다. 그래서 실제 엔진은 atomic counter 위에 사용자 정의 read / write latch 를 직접 만든다. 보통 holder 와 waiter 리스트, 그리고 명시적 promotion 지원이 함께 따라온다. PostgreSQL 에는 LWLock, MySQL InnoDB 에는 rw_lock_t, CUBRID 에는 BCB latch 가 있다. 모두 같은 세 가지 기본 동작을 갖춘다 — read / write 호환성 판정, FIFO waiter wake, read → write promotion.

다중 페이지 deadlock 회피를 위한 ordered fix

섹션 제목: “다중 페이지 deadlock 회피를 위한 ordered fix”

heap 연산이 home page, forwarded page, overflow page 세 곳을 모두 fix 해야 하는 경우가 있다. 이때 서로 다른 스레드 가 페이지를 다른 순서로 잡으면 데드락이 발생할 수 있다. 교재 의 해법은 ordered fix 다. 모든 페이지에 숫자 rank 를 매기고 (예: heap-header < heap-normal < overflow), 항상 rank 오름차순으로 fix 하는 방식이다. CUBRID 는 이를 pgbuf_ordered_fixPGBUF_WATCHER 구조체로 노출한다. PostgreSQL 은 같은 문제를 인덱스 / 힙 프로토콜 (_bt_findinsertloc 류 흐름) 에서 해결한다. 근본 제약은 같 다.

Background flush daemon + double-write buffer

섹션 제목: “Background flush daemon + double-write buffer”

foreground 워커와 함께 여러 daemon 스레드가 돈다.

  • Page Flush Daemon: 주기적으로 dirty 페이지를 디스크로 flush.
  • Page Post-Flush Daemon: flush 가 끝난 페이지의 후처리. 가능하면 direct victim 으로 인계.
  • Page Maintenance Daemon: 최근 활동에 따라 private LRU 의 quota 를 조정.

OS 의 디스크 블록 크기보다 DB 의 페이지 크기가 더 큰 환경 (예: OS 4 K vs DB 16 K) 에서는 페이지 한 장의 일부만 디스크에 기록되는 torn page 가 발생할 수 있다. 이를 복구하기 위한 장치가 double-write buffer 다. 페이지를 실제 위치에 쓰기 전에 DWB 에 먼저 써 두는 방식이다. 순서는 — (1) 순차 DWB 영 역에 페이지를 쓴다. 그 다음 (2) 실제 페이지 위치에 쓴다. 크 래시 복구 시점에는 손상된 페이지를 DWB 의 깨끗한 사본으로 복 원한 뒤 WAL 을 다시 적용한다. MySQL InnoDB 와 CUBRID 모두 DWB 를 구현하고, PostgreSQL 은 대신 full-page-image WAL 레코 드로 같은 보호를 제공한다.

§학술적 배경 의 교재 개념과 CUBRID 의 명명된 엔티티가 다음 과 같이 대응된다. ## CUBRID의 구현 은 각 행을 차근차근 따라 들어간다.

이론 (Theory)CUBRID 명칭
Buffer control blockPGBUF_BCB (page_buffer.c)
디스크 페이지의 물리 포맷FILEIO_PAGE (LSA + ptype + page contents)
페이지 식별자VPID = (volid, pageid)
Page table (해시)pgbuf_Pool.buf_HT[] (2²⁰ 버킷, chaining)
Free / invalid listpgbuf_Pool.buf_invalid_list
3-zone LRULRU 1 / 2 / 3 = top / middle / bottom (LRU 리스트 내부)
Worker 별 LRUprivate LRU 리스트, 워커당 하나
Private 리스트별 적응형 quotapgbuf_adjust_quotas (Page Maintenance Daemon)
Lock-free victim 조정LFCQ (big private / private / shared)
Direct victim 인계pgbuf_assign_direct_victim / pgbuf_get_direct_victim
사용자 정의 page latchBCB latch (PGBUF_LATCH_READ / _WRITE / _FLUSH)
Alloc 중 VPID 단위 잠금PGBUF lock
다중 페이지 deadlock 회피용 ordered fixpgbuf_ordered_fix + PGBUF_WATCHER + PGBUF_ORDERED_RANK
Background flushPage Flush / Post-Flush / Maintenance daemon 3개
Torn-write 복구Double Write Buffer (double_write_buffer.{hpp,cpp})

CUBRID 는 위의 관행을 단일 전역 pgbuf_Pool 구조체로 구체화 한다. 이 구조체 안에 고정 크기 PGBUF_BCB 배열, 다섯 개의 상 태 zone, 세 종류의 LFCQ, 세 개의 background daemon, 그리고 사용자 정의 BCB latch 가 모두 들어 있다. 차별화된 선택은 여섯 가지다. (1) 3-zone LRU (1 / 2 / 3). 기본 임계값은 5 % 이 고, boost 시점에는 old enough 게이트를 둔다. (2) 적응형 quota 를 가진 워커별 private LRU, 그리고 private 의 교체 압력을 견딘 페이지를 받는 shared LRU. (3) over-quota 정도 가 큰 리스트를 우선 탐색하도록 big-private / private / shared 로 계층화된 lock-free circular queue. (4) 스레드 별 슬롯 bcb_victims[] 을 통한 direct victim 인계, 그리 고 우선순위 큐 두 개. (5) 명시적 promotion 과 별도 FLUSH 대기 상태를 가진 사용자 정의 BCB latch. (6) 동일 페이지에 대한 동시 alloc 을 직렬화하는 VPID 단위 PGBUF lock.

Page fix 가 흐르는 방식 (How a page fix flows)

섹션 제목: “Page fix 가 흐르는 방식 (How a page fix flows)”
flowchart LR
  A["pgbuf_fix(VPID, fetch_mode,\nlatch_mode, condition)"] --> B{"page table에 있나?"}
  B -- "있음" --> L["BCB latch 호환성 검사"]
  L -- "호환" --> F["fcnt++, PAGE_PTR 반환"]
  L -- "비호환" --> W["waiter list에 등록, sleep"]
  W --> L
  B -- "없음" --> A1["pgbuf_claim_bcb_for_fix:\nBCB 할당"]
  A1 --> I{"invalid list\n비었나?"}
  I -- "안 비었음" --> G["free BCB 하나 가져옴"]
  I -- "비었음" --> V["pgbuf_get_victim:\nprivate/shared LFCQ 스캔"]
  V --> V2{"victim 찾았나?"}
  V2 -- "예" --> G
  V2 -- "아니오" --> D["direct-victim queue에서 sleep\n(High / Low priority)"]
  D --> G
  G --> R{"fetch_mode\n== NEW_PAGE?"}
  R -- "아니오" --> RD["DWB → 디스크 순으로 read"]
  R -- "예" --> Z["zero-fill"]
  RD --> P["page table에 등록,\nlatch 잡고 PAGE_PTR 반환"]
  Z --> P
  F --> END
  P --> END[" "]

각 박스는 아래 서브섹션에서 풀어 본다.

Figure — BCB allocation flow

Figure (BCB allocation) — The right half of the Mermaid above, drawn with the deck’s own conventions. Step 1 searches three places in order: (1.1) the Invalid List of free BCBs, (1.2) the worker’s Private LRUs (via the LFCQ), (1.3) the Shared LRUs (via the LFCQ); on miss, (1.4) the thread joins a direct-victim LFCQ and sleeps. Step 2 — once a BCB is in hand and fetch_mode != NEW_PAGE — the page bytes are loaded by checking (2.1) the DWB first, then (2.2) the actual Storage if the DWB has no copy. (Source: deck Figure 4.)

BCB 하나가 버퍼 슬롯 하나를 감싸는 구조다. 페이지 식별자, fix count, latch 상태, dirty bit, LRU 포인터, 해시 체인 포인터, 그리고 실제 페이지 바이트 (PGBUF_IOPAGE_BUFFER) 포인터를 한 자리에 모아 들고 있다.

// PGBUF_BCB (condensed) — src/storage/page_buffer.c
struct pgbuf_bcb
{
pthread_mutex_t mutex; /* per-BCB mutex */
VPID vpid; /* (volid, pageid) of resident page */
int fcnt; /* fix count */
PGBUF_LATCH_MODE latch_mode; /* current latch mode */
THREAD_ENTRY *next_wait_thrd; /* head of waiter list */
volatile int flags; /* zone | LRU_index | BCB flags */
PGBUF_BCB *hash_next; /* hash chain (page table) */
PGBUF_BCB *prev_BCB; /* LRU chain — back */
PGBUF_BCB *next_BCB; /* LRU chain — forward */
int tick_lru_list; /* LRU bookkeeping */
int tick_lru3;
int hit_age;
volatile int count_fix_and_avoid_dealloc;
LOG_LSA oldest_unflush_lsa; /* oldest LSA not yet on disk */
PGBUF_IOPAGE_BUFFER *iopage_buffer; /* the page bytes */
};

flags 필드는 여러 종류의 상태를 32 비트 워드 하나에 묶어 저 장한다. 구체적인 비트 배치는 아래 §BCB flags 를 참고한다. 데이터 영역은 iopage_buffer 에 있다. 그 안의 FILEIO_PAGE 첫 16 바이트에는 페이지의 LOG_LSA (가장 최근 갱신 LSN) 와 ptype (volume header / heap / btree / …) 이 들어 있다. page-write 코드가 WAL 순서를 강제할 때 이 두 값을 참조한다.

// pgbuf_Pool.buf_HT[] — src/storage/page_buffer.c (sketch)
// 1 << 20 = 1,048,576 buckets, chaining, ~16M slot capacity.
struct pgbuf_buffer_hash
{
pthread_mutex_t hash_mutex; /* per-bucket mutex */
PGBUF_BCB *hash_next; /* head of BCB chain */
PGBUF_BUFFER_LOCK *lock_next; /* per-VPID lock chain (PGBUF lock) */
};

각 버킷에는 두 종류의 체인이 따로 매달려 있다. 하나는 BCB 체 인이다. 이 버킷으로 해싱되는 모든 BCB 의 연결 리스트다. 다른 하나는 PGBUF lock 체인이다. 어떤 스레드가 alloc 중이라 해 당 VPID 에 대한 BCB 가 아직 없는 상태에서 사용된다. PGBUF lock 의 역할은 두 스레드가 동시에 같은 VPID 의 BCB 를 새로 allocate 하는 것을 막는 것이다. 먼저 잡은 쪽이 이기고, 늦게 온 쪽은 대기하다가 깨어난다. 깨어난 뒤 이미 등록된 BCB 를 통 해 일반 hash-hit 경로로 진행한다.

flowchart LR
  S["서버 시작"] --> I["모든 BCB가 invalid_list에"]
  I --> AL["pgbuf_get_bcb_from_invalid_list\n(head를 꺼내옴)"]
  AL --> U["BCB가 VOID zone에 잠시 머무름\n(transient)"]
  U --> P["fix + unfix 후 LRU 2로 들어감"]
  P --> Z["BCB는 이후 평생 어떤 zone에 산다"]
  Z -. "에러 시" .-> I

페이지가 매핑되지 않은 BCB 들을 모아 둔 단일 연결 리스트다. 서버 시작 시점에는 모든 BCB 가 이 리스트에 들어 있다. 이 리스 트는 새로운 BCB 가 필요할 때만 참조된다. victim 탐색은 이와 는 별도의 메커니즘이다. alloc 도중 에러가 발생하면 BCB 는 이 리스트로 되돌아 간다.

각 LRU 리스트는 BCB 의 양방향 연결 리스트다. 두 임계값 카운터 (threshold_lru1, threshold_lru2, 기본 5 % 씩) 에 의해 세 zone 으로 나뉜다. top (LRU 1), middle (LRU 2), bottom (LRU 3) 이 그 셋이다.

Figure 1 — Three-zone LRU + victim candidate criteria

Figure 1 — A single LRU list with three zones. Black squares are victim candidates — only BCBs in LRU 3 (bottom) are eligible, and even there a BCB is excluded if it is currently being flushed, dirty, already chosen as a direct victim, or marked as an invalid direct victim. Promotions move a BCB toward LRU 1 on hit; demotions push it toward LRU 3 over time. Eviction always picks from the cold end. (Source: original Buffer Manager analysis deck, Figure 6.)

그림의 네 가지 제외 조건은 BCB 의 flag 에 대응한다.

  • PGBUF_BCB_FLUSHING_TO_DISK_FLAG — flush 가 진행 중인 상 태.
  • PGBUF_BCB_DIRTY_FLAG — 마지막 flush 이후 변경된 상태. WAL 이 디스크에 도달하기 전에는 evict 할 수 없다.
  • PGBUF_BCB_VICTIM_DIRECT_FLAG — 이미 sleep 중인 어떤 스레 드에게 direct hand-off 로 넘어가기로 약속된 상태.
  • PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG — 한 번 약속되었 으나 그 사이에 다시 fix 되어 더 이상 가용하지 않게 된 상 태.
flowchart TB
  subgraph PRIVATE["Private LRU 리스트들 (워커당 하나)"]
    P1["worker 1 LRU\n(LRU 1/2/3 zones)"]
    P2["worker 2 LRU"]
    Pn["worker N LRU"]
  end
  subgraph SHARED["Shared LRU 리스트들"]
    S1["shared LRU 1"]
    S2["shared LRU 2"]
    Sm["shared LRU M"]
  end
  PMD["Page Maintenance Daemon\n(매 100 ms)"]
  PMD -- "활동량 측정\n(리스트당 hit 수)" --> P1
  PMD -- "재분배" --> P2
  PMD -- "shared 임계값" --> S1
  P1 -- "hot이면서 old enough\n(fix ≥ 64), 또는\n다른 스레드가 unfix" --> S1
  P2 -- "..." --> S2

워커 스레드는 각각 자기 소유의 private LRU 리스트를 갖는다. 그 워커가 새로 할당하는 BCB 는 우선 자기 private LRU 리스트로 들어간다. 이후 두 조건 중 하나가 맞아 떨어지면 BCB 가 shared LRU 리스트로 옮겨진다. 첫째는 BCB 가 old enough 이면서 hot 인 경우다. old enough 는 LRU 2 의 절반 이상이 그 BCB 보다 더 hot 한 위치로 밀려난 상태를 가리키고, hot 은 fix count 가 64 이상인 상태를 가리킨다. 둘째는 fix 한 스레드와 다 른 스레드가 unfix 한 경우다. shared 리스트는 자기 소유 워커의 eviction 압력을 견딘 페이지들이 모이는 자리다.

적응형 quota 로직은 Page Maintenance Daemon (pgbuf_adjust_quotas) 이 100 ms 마다 수행한다.

  1. 지난 구간 동안의 모든 private LRU 전체 hit 수를 합산한다.
  2. private 비율 = (private hit) / (total hit) 을 계산한다.
  3. all_private_quota = (전체 버퍼 - invalid) * private 비 율.
  4. all_private_quota 를 각 private 리스트의 활동량 비율에 따라 분배한다.
  5. 새 quota 에 맞춰 리스트별 threshold_lru1 / threshold_lru2 를 다시 계산한다.
  6. 남은 버퍼 수는 shared 리스트들에 균등 분배해서, shared 리 스트의 임계값 베이스로 쓴다.

quota 는 victim 탐색 단계에서 의미를 가진다. private 리스트 는 BCB 수가 quota 를 넘어야만 victim 대상이 될 수 있다. 이 규칙 덕분에 각 워커의 hot 페이지가 quota 안에 머물고, quota 를 초과하는 워커 (와 그에 속한 페이지) 에 eviction 압력이 모 이게 된다.

BCB 할당자가 evict 해야 할 때는 lock-free circular queue 들을 순회한다. 각 큐에는 victim 후보가 한 개 이상 있는 LRU 리스트들만 들어간다.

Figure 2 — Victim selection across Big-Private / Private / Shared LFCQs

Figure 2 — Three LFCQs partition the victim search. Big Private LRUs (top) hold private lists whose BCB count exceeds 2× their quota — these are searched first because they are over-quota and likely have many candidates. Private LRUs (middle) hold private lists with at least one candidate but BCB count ≤ 2× quota. Shared LRUs (bottom) are scanned last. Each list joins the LFCQ only when it has at least one victim candidate; lists with no candidate are skipped entirely. (Source: deck Figure 9.)

// pgbuf_get_victim (sketch) — src/storage/page_buffer.c
PGBUF_BCB *
pgbuf_get_victim (THREAD_ENTRY *thread_p)
{
/* 1. Try this worker's own private LRU list first. */
/* 2. Try big-private LFCQ (#BCBs > 2 * quota). */
/* 3. Try private LFCQ. */
/* 4. Try shared LFCQ. */
/* 5. If none found → caller will sleep on direct-victim queue. */
}

위 네 단계 LFCQ 탐색에 모두 실패한 스레드는 재시도하지 않는 다. 대신 인계받을 때까지 자기 슬롯에서 sleep 한다.

Figure 3 — Direct victim assign / get

Figure 3 — Two priority LFCQs of waiting threads (High and Low, 75 / 25 weight in selection). When a producer (any of the three flushing daemons, a normal flush completion, or maintenance work) finds a now-victimizable BCB, it picks one waiter, finds that waiter’s slot in the static bcb_victims[] array, writes the BCB into that slot, and wakes the waiter. The waiter, when scheduled, reads its own slot and proceeds. If between assign and get the BCB gets re-fixed, the get path observes PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG and returns to sleep. (Source: deck Figure 13.)

high-priority 큐는 vacuum worker 와, 직전에 인계받은 direct victim 이 invalidate 되어 다시 기다리는 스레드를 위해 따로 마 련된 자리다. producer 는 셋이다.

  • Page Flush Daemon (class pgbuf_page_flush_daemon_task) — dirty BCB 를 flush 하다가 victim 가능한 BCB 를 만나면 인 계한다. 세 daemon 가운데 유일하게 전용 task class 로 구현 되어 있다.
  • Page Post-Flush Daemon (pgbuf_page_post_flush_execute) — flush 가 끝난 BCB 가 victim 후보가 되면 인계한다. 별도 class 가 아니라 cubthread::entry_callable_task 에 묶여 등록된다.
  • Page Maintenance Daemon (pgbuf_page_maintenance_execute) — quota 를 조정하면서 동 시에 명시적으로 direct victim 을 찾아 인계한다. 등록 패턴 은 post-flush 와 같은 callable-task 방식이다.

모든 BCB 는 다음 다섯 zone 중 정확히 하나에 속한다.

Figure 4 — Zone transitions

Figure 4 — Zone transitions. INVALID is the initial pool; VOID is the transient state during allocation/movement; LRU 1 / 2 / 3 are the active LRU zones. New BCBs are claimed out of INVALID into VOID; after fix+unfix they land in LRU 2 (or LRU 1 for vacuum workers / aout-hit boost). On error during alloc the BCB falls back to INVALID. Deallocation pushes a BCB straight into LRU 3 (waiting to be flushed and reused), regardless of where it currently is. (Source: deck Figure 10.)

flags.zone 필드가 이 상태를 들고 있다. zone 과 자료 구조는 일대일로 대응된다. INVALID 는 invalid list 위에 있는 상태다. VOID 는 어떤 리스트에도 들어 있지 않은 상태다. LRU 1 / 2 / 3 은 특정 LRU 리스트의 해당 zone 영역에 있는 상태다.

BCB flags — 상태를 한 워드에 묶기

섹션 제목: “BCB flags — 상태를 한 워드에 묶기”

flags 필드는 세 종류의 상태를 32 비트 워드 하나에 묶어 저장 한다.

+-------------+----------+--------+--------------------+
| BCB_FLAGS | unused | ZONE | LRU_INDEX (16) |
| (7) | (5) | (4) | |
+-------------+----------+--------+--------------------+

BCB-specific flag 는 워드의 상위 7 비트 를 차지한다. 다만 #define 된 일곱 개 마스크가 연속 (contiguous) 되지 않는 다. 사용 비트는 31..25 — DIRTY 0x80000000, FLUSHING 0x40000000, VICTIM_DIRECT 0x20000000, INVALIDATE_DIRECT_VICTIM 0x10000000, MOVE_TO_LRU_BOTTOM 0x08000000, TO_VACUUM 0x04000000, ASYNC_FLUSH_REQ 0x02000000 — 이며, flag 영역 안의 0x01000000 비트는 예약 / 미사용이다. 항목별 의미는 다음과 같 다.

  • PGBUF_BCB_DIRTY_FLAG — 마지막 flush 이후 변경됨. 페이지 갱신 연산마다 set 된다. flush 시작 시 unset (실패 시 다시 set), invalidate 시 unset.
  • PGBUF_BCB_FLUSHING_TO_DISK_FLAG — flush 가 진행 중. direct victim producer 가 인계 시 unset 한다.
  • PGBUF_BCB_ASYNC_FLUSH_REQ — flush 요청은 들어왔으나 BCB 가 write latch 로 fix 되어 있는 상태. 해당 fixer 가 unfix 할 때 flush 를 해 달라는 요청.
  • PGBUF_BCB_VICTIM_DIRECT_FLAG — sleep 중인 스레드에게 인계 됨.
  • PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG — 인계받은 BCB 가 그 사이에 다시 fix 되어 무효화됨. waiter 는 다시 큐로 돌아 가야 한다.
  • PGBUF_BCB_MOVE_TO_LRU_BOTTOM_FLAG — dealloc 시 set. unfix 경로가 일반 LRU promotion 로직을 건너뛰고 BCB 를 LRU 3 로 직행시키게 한다.
  • PGBUF_BCB_TO_VACUUM_FLAG — vacuum 의 검사 대상으로 큐잉된 페이지에 set 된다. Producer: pgbuf_notify_vacuum_follows (page_buffer.c:15579). Consumer: pgbuf_bcb_is_to_vacuum (page_buffer.c:15591). 이 비트는 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK (line 258) 에도 포함되어 있어, vacuum 대기 중인 BCB 는 victim 탐색에서 제외 된다. unfix 경로와 vacuum 주도의 pgbuf_fix_if_not_deallocated 흐름에서 pgbuf_bcb_update_flags 로 unset 된다 (line 2454, 8395).

모든 fix 는 BCB 에 latch 를 잡는 동작을 동반한다. 헤더가 선언 하는 PGBUF_LATCH_MODE 값은 다섯 가지다 — PGBUF_NO_LATCH, PGBUF_LATCH_READ, PGBUF_LATCH_WRITE, PGBUF_LATCH_FLUSH, 그리고 PGBUF_LATCH_INVALID (sentinel; 실제 fix 시점에는 관 찰되지 않는다). 실제 상태는 앞의 네 가지뿐이다. 이 가운데 FLUSH 모드는 condition variable 처럼 사용된다. 어떤 스레드가 진행 중인 flush 가 끝나기를 기다릴 때 쓰는 모드이고, 실제로 latch 가 잡히는 상태는 아니다.

Figure 5 — BCB latch compatibility decision tree

Figure 5 — pgbuf_latch_bcb_upon_fix decision tree. R/R is compatible by default, but if any thread is already waiting (e.g., a write request) the new R yields and blocks to prevent starvation. W/R and R/W block by default; a thread that already holds the latch can pass W→R because it already holds the stronger latch. The R→W upgrade in-place was deprecated (CUBRIDSUS-10294) and re-added later as a separate pgbuf_promote_read_latch call (CUBRIDSUS-15376). (Source: deck Figure 15.)

waiter 리스트는 BCB.next_wait_thrd 에 들어 있다. unlatch 시 점이 되면 pgbuf_wakeup_read_writer 가 리스트를 순회하며 다 음 규칙을 적용한다.

  • 첫 번째 FLUSH waiter 는 건너뛴다. FLUSH waiter 는 별도로 pgbuf_wake_flush_waiters 가 처리하기 때문이다.
  • 첫 번째 NO_LATCH waiter 는 정리한다. timeout 으로 깨어나서 실패 처리되어야 하는 스레드를 가리킨다.
  • 첫 번째 READ waiter 를 만나면 연속된 READ waiter 모두 를 깨운다. read 는 병렬로 진행할 수 있기 때문이다.
  • 첫 번째 WRITE waiter 를 만나면 자기 자신만 깨운다. write 는 단독으로 실행되어야 하기 때문이다.

holder 는 스레드 단위로 PGBUF_HOLDER 리스트에 추적된다. 덕 분에 내가 이 BCB 를 이미 잡고 있는가? 라는 질문에 O(local) 로 답할 수 있다.

PGBUF lock — VPID 단위의 alloc 잠금

섹션 제목: “PGBUF lock — VPID 단위의 alloc 잠금”

fix 가 page table 에서 miss 되어 새 BCB 를 할당해야 하는 경 로에서, 같은 VPID 를 두고 두 스레드가 경쟁하면 양쪽 모두 alloc 을 시도하게 된다. PGBUF lock 은 이 경쟁을 막기 위한 장치다. alloc 에 해당 VPID 를 lock 을 잡고, BCB 가 page table 에 등록되면 풀어 준다.

flowchart LR
  T1["스레드 A:\nfix VPID 100"] --> H1["page table miss"]
  H1 --> L1["pgbuf_lock_page(VPID 100)"]
  L1 --> A1["BCB 할당,\n디스크에서 read,\npage table에 등록"]
  A1 --> U1["pgbuf_unlock_page(VPID 100)"]

  T2["스레드 B:\nfix VPID 100\n(좀 늦게 시작)"] --> H2["page table miss"]
  H2 --> L2["pgbuf_lock_page(VPID 100)\n→ A 뒤에서 block"]
  L2 -. "U1 후 wake" .-> RT["재시도 → page table hit"]
  RT --> DONE["일반 hash-hit 경로로 진행"]

lock 체인은 BCB 체인과 같은 해시 버킷에 매달려 있다. 경쟁에 서 진 쪽 스레드는 깨어난 뒤 page table 에 이미 들어 있는 BCB 를 발견하고, 일반 hash-hit 경로로 진행한다.

Ordered fix — 다중 페이지 deadlock 회피

섹션 제목: “Ordered fix — 다중 페이지 deadlock 회피”

한 연산이 여러 페이지를 동시에 fix 해야 할 때가 있다. 예를 들 어 REC_RELOCATIONREC_NEWHOME 두 heap 페이지를 함께 잡 아야 하거나, REC_BIGONE 과 overflow 페이지를 함께 잡아야 하 는 경우다. 이때 서로 다른 연산이 같은 페이지들을 다른 순서로 잡으면 데드락에 빠진다. CUBRID 의 답은 ordered fix 다. 모 든 페이지에 숫자 rank 를 매기고, 항상 rank 오름차순으로 fix 하는 방식이다.

// PGBUF_ORDERED_RANK — src/storage/page_buffer.h
typedef enum
{
PGBUF_ORDERED_HEAP_HDR = 0, /* heap header 페이지 먼저 */
PGBUF_ORDERED_HEAP_NORMAL, /* 그 다음 일반 heap 페이지 */
PGBUF_ORDERED_HEAP_OVERFLOW, /* 그 다음 overflow 페이지 */
PGBUF_ORDERED_RANK_UNDEFINED,
} PGBUF_ORDERED_RANK;
// PGBUF_WATCHER — ordered fix를 지원하는 page latch handle
struct pgbuf_watcher
{
PAGE_PTR pgptr;
PGBUF_WATCHER *next;
PGBUF_WATCHER *prev;
PGBUF_ORDERED_GROUP group_id; /* group(HEAP header)의 VPID */
unsigned latch_mode:7;
unsigned page_was_unfixed:1; /* refix가 일어났는가? */
unsigned initial_rank:4;
unsigned curr_rank:4;
/* (debug fields elided) */
};

pgbuf_ordered_fix 는 요청된 rank 가 현재 잡고 있는 watcher 들의 rank 가운데 어느 것보다도 더 큰지 검사한다. 더 작거나 같 다면, 순서가 어긋난 페이지들을 일단 unfix 하고 정렬한 뒤 rank 순서로 다시 fix 한다. 이때 page_was_unfixed = true 는 caller 에게 그 사이에 페이지 상태가 바뀌었을 수 있다 라고 알려 주는 신호다.

server mode 에서 foreground worker 와 별도로 세 daemon 이 돈 다.

Daemon주기하는 일
Page Flush Daemonadaptivedirty 페이지 flush; victimizable이면 인계
Page Post-Flush Daemonevent-drivenflush 완료 후처리; victimizable이면 인계
Page Maintenance Daemon100 msprivate LRU quota 조정; direct victim 검색

flush daemon 의 flush 속도는 pgbuf_flush_control_from_dirty_ratio 가 dirty 비율에 따라 조정한다. dirty 비율이 높아질수록 flush 가 빨라진다. daemon 의 활성도는 boolean 두 개 (pgbuf_keep_victim_flush_thread_running, pgbuf_assign_flushed_pages) 가 게이팅한다.

Double Write Buffer (DWB) — torn page 방어

섹션 제목: “Double Write Buffer (DWB) — torn page 방어”

DB 의 페이지 크기가 OS 의 디스크 블록 크기보다 클 때 (예: DB 16 K vs OS 4 K), 페이지 한 장의 일부만 디스크에 기록된 상태에 서 크래시가 일어날 수 있다. 이를 torn page 라고 한다. 이 경우 디스크 위의 DB 페이지는 앞부분에는 새 데이터가, 뒷부분에 는 옛 데이터가 섞인 frankenstein 상태가 된다. WAL 은 일관된 옛 상태를 가정하기 때문에 WAL 재적용만으로는 이런 페이지를 복 구할 수 없다.

DWB 는 이 문제를 풀기 위한 메커니즘이다. 모든 페이지는 두 번 에 걸쳐 디스크에 쓰인다. 먼저 (1) 순차적인 DWB 영역에 쓰고, 그 다음 (2) 실제 페이지 위치에 쓴다. 크래시 복구 시점에는 다 음 단계를 거친다.

  • checksum 이 깨진 DB 페이지마다 DWB 를 조회한다.
  • 그 VPID 의 깨끗한 사본이 DWB 에 있으면 그 사본으로 복원한 다.
  • 복원된 페이지 위에 WAL 을 다시 적용한다.

CUBRID 의 DWB 는 src/storage/double_write_buffer.{hpp,cpp} (약 4 200 줄) 에 자리한다. page buffer 의 fix 경로는 cache miss 시점에 디스크 read 보다 먼저 DWB 를 조회한다.

// fix path on page-table miss → claim BCB
// if (fetch_mode != NEW_PAGE)
// // 먼저 DWB부터 시도
// if (DWB has VPID)
// read from DWB into the BCB's iopage_buffer
// else
// read from storage

이 덕분에 DWB hit 가 일어난 cache miss 는 디스크 read 를 한 번 절약하게 된다.

심볼명을 anchor 로 삼는다 — 라인 번호가 아니다. CUBRID 소스는 시간이 지나면 변한다. 그에 비해 함수명·struct 태그· enum 태그 같은 심볼은 잘 변하지 않는 안정된 식별자다. 현재 위치는 git grep -n '<symbol>' src/storage/ 로 찾으면 된 다.

타입 정의 (src/storage/page_buffer.{h,c})

섹션 제목: “타입 정의 (src/storage/page_buffer.{h,c})”
  • struct pgbuf_bcb (page_buffer.c) — buffer control block.
  • struct pgbuf_iopage_buffer (page_buffer.c) — 페이지 바이 트 carrier; FILEIO_PAGE 를 임베드.
  • struct pgbuf_buffer_hash (page_buffer.c) — page-table 버킷: BCB 체인 + PGBUF lock 체인 + bucket mutex.
  • struct pgbuf_buffer_lock (page_buffer.c) — BCB allocation 시 사용되는 VPID 단위 잠금.
  • struct pgbuf_holder (page_buffer.c) — 한 스레드가 들고 있는 latch 정보.
  • struct pgbuf_lru_list (page_buffer.c) — LRU 리스트 한 개.
  • struct pgbuf_pool (pgbuf_Pool, page_buffer.c) — buffer manager 전역 상태.
  • enum PAGE_FETCH_MODE (page_buffer.h) — 7 가지 모드 (OLD_PAGE / NEW_PAGE / OLD_PAGE_IF_IN_BUFFER / OLD_PAGE_PREVENT_DEALLOC / OLD_PAGE_DEALLOCATED / OLD_PAGE_MAYBE_DEALLOCATED / RECOVERY_PAGE).
  • enum PGBUF_LATCH_MODE (page_buffer.h) — 5 가지 모드.
  • enum PGBUF_LATCH_CONDITION (page_buffer.h) — UNCONDITIONAL / CONDITIONAL.
  • enum PGBUF_PROMOTE_CONDITION (page_buffer.h) — ONLY_READER / SHARED_READER (R → W promotion).
  • enum PGBUF_ORDERED_RANK (page_buffer.h) — ordered fix 용 4 단계 rank.
  • struct pgbuf_watcher (page_buffer.h) — ordered fix 용 handle.

라이프사이클 (src/storage/page_buffer.c)

섹션 제목: “라이프사이클 (src/storage/page_buffer.c)”
  • pgbuf_initialize — 모듈 초기화.
  • pgbuf_finalize — 모듈 해제.
  • pgbuf_daemons_init / pgbuf_daemons_destroy — 세 flush daemon 생성 / 해제.
  • pgbuf_thread_variables_init — 워커별 초기화 (private LRU 인덱스 할당).
  • pgbuf_fix (debug, release variants) — 공개 진입점.
  • pgbuf_fix_with_retry — 일시적 실패 시 재시도 wrapper.
  • pgbuf_simple_fix / pgbuf_simple_unfix — temporary file 용 최소 경로.
  • pgbuf_ordered_fix / pgbuf_ordered_unfixPGBUF_WATCHER 를 쓴 다중 페이지 deadlock-free fix.
  • pgbuf_promote_read_latch — R → W 승격.
  • pgbuf_unfix / pgbuf_unfix_all.
  • pgbuf_fix_if_not_deallocated — deallocation 을 인지하는 변형.

BCB allocation (src/storage/page_buffer.c)

섹션 제목: “BCB allocation (src/storage/page_buffer.c)”
  • pgbuf_claim_bcb_for_fix — 외부 allocate-and-bind 루프.
  • pgbuf_allocate_bcb — 할당 (invalid → victim → direct- victim sleep).
  • pgbuf_get_bcb_from_invalid_list.
  • pgbuf_get_victim — LFCQ 스캔.
  • pgbuf_assign_direct_victim / pgbuf_get_direct_victim.
  • pgbuf_lru_add_bcb_to_top / _to_middle / _to_bottom.
  • pgbuf_move_bcb_to_bottom_lru.
  • pgbuf_lru_boost_bcb — LRU 1 로 promote.
  • pgbuf_lru_adjust_zones — 임계값 변경 후 재균형.
  • pgbuf_should_move_private_to_shared.
  • pgbuf_adjust_quotas — Page Maintenance Daemon 이 100 ms 마다 수행.
  • PGBUF_IS_BCB_OLD_ENOUGH — boost gate.

Page table + PGBUF lock (src/storage/page_buffer.c)

섹션 제목: “Page table + PGBUF lock (src/storage/page_buffer.c)”
  • pgbuf_hash_func_mix — VPID 해싱.
  • pgbuf_hash_chain_lookup — bucket walk.
  • pgbuf_lock_page / pgbuf_unlock_page — VPID 단위 alloc 잠금.
  • pgbuf_latch_bcb_upon_fix — 호환성 결정.
  • pgbuf_block_bcb — waiter 로 등록 후 sleep.
  • pgbuf_wakeup_read_writer — unlatch 시 waiter 깨우기.
  • pgbuf_wake_flush_waiters — flush 완료 시 FLUSH waiter 깨 우기.
  • pgbuf_promote_read_latch_release / _debug — R → W 승 격.

src/storage/double_write_buffer.cpp)

  • pgbuf_flush / pgbuf_flush_with_wal — BCB 1 개 flush.
  • pgbuf_flush_victim_candidates — Page Flush Daemon 본체.
  • pgbuf_flush_checkpoint — checkpoint flush.
  • pgbuf_flush_all / _unfixed / _unfixed_and_set_lsa_as_null.
  • pgbuf_bcb_flush_with_wal — flush 전후의 flag 조작.
  • dwb_* — double write buffer 진입 / lookup / recovery.

Daemon (src/storage/page_buffer.c, server mode 전용)

섹션 제목: “Daemon (src/storage/page_buffer.c, server mode 전용)”
  • class pgbuf_page_flush_daemon_task — Page Flush Daemon 본체 (세 daemon 중 유일한 전용 task class).
  • pgbuf_page_post_flush_execute — Page Post-Flush Daemon 의 callable. cubthread::entry_callable_task 에 묶여 등록 된다.
  • pgbuf_page_maintenance_execute — Page Maintenance Daemon 의 callable. 등록 방식은 post-flush 와 같다.
  • pgbuf_page_flush_daemon_init / pgbuf_page_post_flush_daemon_init / pgbuf_page_maintenance_daemon_init — daemon 기동.
  • pgbuf_keep_victim_flush_thread_running.
  • pgbuf_assign_flushed_pages.
  • pgbuf_direct_victims_maintenance.
심볼파일라인
enum PAGE_FETCH_MODEpage_buffer.h172
enum PGBUF_LATCH_MODEpage_buffer.h190
enum PGBUF_ORDERED_RANKpage_buffer.h222
struct pgbuf_watcherpage_buffer.h234
PGBUF_FIX_COUNT_THRESHOLDpage_buffer.c106
PGBUF_BCB_DIRTY_FLAGpage_buffer.c224
PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASKpage_buffer.c258
PGBUF_HASH_SIZEpage_buffer.c296
pgbuf_initializepage_buffer.c1518
pgbuf_fix_releasepage_buffer.c2041
pgbuf_latch_bcb_upon_fixpage_buffer.c6073
pgbuf_lock_pagepage_buffer.c7718
pgbuf_claim_bcb_for_fixpage_buffer.c8133
pgbuf_get_victimpage_buffer.c8805
pgbuf_adjust_quotaspage_buffer.c13639
pgbuf_assign_direct_victimpage_buffer.c14809
pgbuf_page_maintenance_executepage_buffer.c16375
class pgbuf_page_flush_daemon_taskpage_buffer.c16396
pgbuf_page_post_flush_executepage_buffer.c16450

page_buffer.c 는 약 17 000 줄이라 심볼 단위 git grep 이 권장되는 lookup 방식이다.

각 항목은 현재 소스에 대한 사실이고, 원본 분석 자료를 함께 보지 않아도 그 자체로 읽힌다. 끝의 부연은 어떻게 검증 되었는지를 적고, 관련이 있을 때는 역사적 드리프트나 검증의 한계도 적는다. 미해결 질문 은 큐레이터가 해결을 미루고 기록 해 둔 갭이다 — 차후 독자는 이를 알려진 버그가 아니 라 시작점으로 다룬다.

  • page-table 해시는 2²⁰ = 1 048 576 버킷, 서버 시작 시점에 고정. pgbuf_initialize / pgbuf_initialize_buffer_hash_table 에서 2026-04-29 검증. 각 버킷이 자기 hash_mutex, BCB 체인, PGBUF-lock 체인을 들고 있다. 하드코딩 — 런타임 파라미터 아님.

  • PGBUF lock 체인은 해시 버킷 단위 로 존재한다 — VPID 단 위가 아니다. pgbuf_lock_page / pgbuf_unlock_page 를 읽어서 2026-04-29 검증. lock 객체는 버킷 구조체 안에 산다 — 즉, BCB alloc 경쟁을 버킷 단위로 직렬화한다.

  • LRU 는 정확히 세 zone 을 가진다 (top / middle / bottom = LRU 1 / 2 / 3), 기본 임계값 5 %. page_buffer.c 의 zone 상수에서 2026-04-29 검증. evict 가능한 BCB 는 LRU 3 에 있는 것뿐이다. LRU 1, 2 는 보호된다.

  • victim 후보 자격을 박탈하는 네 가지 조건. BCB 는 다음 중 하나라도 해당하면 후보가 아니다 — flush 중 (PGBUF_BCB_FLUSHING_TO_DISK_FLAG), dirty (PGBUF_BCB_DIRTY_FLAG), 이미 direct victim (PGBUF_BCB_VICTIM_DIRECT_FLAG), invalidate 된 direct victim (PGBUF_BCB_INVALIDATE_DIRECT_VICTIM_FLAG). 이 네 비트는 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK 로 묶여 있다. 2026-04-29 검증.

  • direct victim 선택은 우선순위 큐 두 개 (75 / 25 가중치) 를 사용한다. BCB producer 는 high-priority LFCQ 에서 low-priority 의 3 배 확률로 선택한다. pgbuf_assign_direct_victim 을 읽어서 2026-04-29 검증. high priority 는 vacuum worker, 그리고 invalidate 후 재시도 중인 스레드에 할당된다.

  • server mode 에서 background daemon 셋이 돈다. Page Flush, Page Post-Flush, Page Maintenance — pgbuf_daemons_init 에 선언되어 있다. 2026-04-29 검증.

  • quota 조정 주기는 100 ms. Page Maintenance Daemon 의 pgbuf_adjust_quotas 가 100 ms 마다 돈다. daemon task 를 읽어서 2026-04-29 검증. quota 값은 history 와 보간되어 smoothing 된다 — 한 구간의 raw 활동량을 그대로 쓰지는 않는 다.

  • boost 용 old enough 기준 = LRU 2 의 절반 이상이 그 BCB 보다 더 hot 한 위치로 밀려난 상태. PGBUF_IS_BCB_OLD_ENOUGH 매크로에 구현되어 있다. 2026-04-29 검증. 짧게 살았다 사라지는 BCB 가 단 한 번의 fix-unfix 로 LRU 1 까지 boost 되는 것을 막는다.

  • private → shared 이동 트리거. 다음 둘 중 하나면 이동된 다 — (a) BCB 가 hot 이면서 (fix count ≥ 64) old enough 이 거나, (b) fix 한 스레드와 다른 스레드가 unfix 한 경우. pgbuf_should_move_private_to_shared 에서 2026-04-29 검 증.

  • R → W in-place 승격은 폐기되었다가 별도로 다시 추가되었 다. 원래 동작은 잡고 있는 read latch 를 pgbuf_latch_bcb_upon_fix 에서 자동으로 write 로 승격하는 것이었다. CUBRIDSUS-10294 에서 제거되었고, 이후 CUBRIDSUS-15376 에서 별도의 pgbuf_promote_read_latch 호출 로 다시 추가되었다. 현재 결정 트리는 in-place 승격 경로가 도달 불가능함을 assert 한다. BCB latch 결정 로직을 읽어서 2026-04-29 검증.

  • DWB 는 page-table miss 시 디스크 read 보다 먼저 조회된 다. BCB claim 경로 (pgbuf_claim_bcb_for_fixpgbuf_read_page → DWB 조회) 에서 2026-04-29 검증. DWB hit 는 디스크 read 한 번을 절약하고, miss 면 볼륨 read 로 fall through 한다.

  1. PGBUF_BCB_TO_VACUUM_FLAG 는 무엇인가? 2026-05-01 해소. Producer 는 pgbuf_notify_vacuum_follows (page_buffer.c:15579), consumer 는 pgbuf_bcb_is_to_vacuum (page_buffer.c:15591) 이다. 이 비트는 PGBUF_BCB_INVALID_VICTIM_CANDIDATE_MASK (line 258) 에 포함되어 있어 vacuum 대기 중인 BCB 는 victim 으로 선택될 수 없다. unfix 경로의 pgbuf_bcb_update_flags 가 unset 한다 (line 2454, 8395). 남은 후속 과제 — pgbuf_notify_vacuum_follows 의 caller (heap / vacuum 서 브시스템) 전체 열거. page-buffer 문서의 범위 밖이다.

  2. page-table 크기가 왜 2²⁰ 으로 하드코딩인가? 해시 크기 는 pb_buffer_capacity 파라미터와 무관하게 고정이다. 매 우 큰 buffer pool 을 쓰는 워크로드에서는 체인 길이가 길어 질 수 있다. 추적 경로 — 메모리가 큰 워크로드에서 bucket 체인 길이를 계측. 해시 크기를 buffer pool 용량에 비례시켜 야 하는지 검토.

  3. OLD_PAGE_PREVENT_DEALLOC / OLD_PAGE_DEALLOCATED / OLD_PAGE_MAYBE_DEALLOCATED 가 TBU 로 적혀 있다. 헤더 에는 PAGE_FETCH_MODE 값으로 등록되어 있지만 발표 자료가 의 미를 적어 두지 않았다. 추적 경로 — 각 변형의 호출지를 읽 어서 사용 패턴에서 의미를 역추론.

  4. direct-victim 인계 시점에 PGBUF_BCB_FLUSHING_TO_DISK_FLAG 를 왜 unset 하는가? 발 표 자료가 명시적으로 “왜 끄는지는 모르겠음. 플러시를 중지 시키는 것도 아니고” 라고 적었다. unset 이 실제로 flush 를 취소하지는 않는다. 추적 경로 — 인계 시점에 flag 를 끄는 것이 어떤 consumer 에게 어떤 영향을 주는지 (아마 “이 BCB 가 아직 flush 대상인가?” 를 보는 다른 코드 경로) 추적.

  5. direct-victim slot 의 starvation 가능성. 어떤 스레드가 반복적으로 invalidate (assign → re-fix → invalidate) 되면 high-priority 큐에 계속 머물게 된다. 병리적 워크로드에서 다른 high-priority waiter 를 굶길 수 있는가? 추적 경로 — 고경합 워크로드에서 re-queue 횟수를 계측.

  6. flush daemon 의 적응형 속도 (pgbuf_flush_control_from_dirty_ratio) 는 burst 워크로드 에서 실제로 어떻게 동작하는가? 발표 자료는 dirty ratio 기반이라는 점만 언급한다. smoothing 동작은 적혀 있지 않 다. 추적 경로 — 함수 본체를 읽고 입력값을 추적.

  7. TDE (Transparent Data Encryption) 와의 상호작용. 헤더 가 pgbuf_set_tde_algorithm 을 선언하고 BCB 가 페이지별 TDE 상태를 들고 있지만, 발표 자료가 암호화를 다루지 않는 다. 추적 경로 — flush 와 read 경로에서 encrypt / decrypt 경계를 추적.

CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)

섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향 (Beyond CUBRID — Comparative Designs & Research Frontiers)”

분석이 아닌 포인터 (pointers). 각 항목은 후속 문서의 시작 점이고, 깊이는 의도적으로 얕다.

  • PostgreSQL clock-sweep + buffer 파티션. PostgreSQL 은 LRU 가 아니라 clock 알고리즘을 BufferDesc 배열 위에서 돌 린다. BufFreelistLockBufMappingLock 은 해시별로 파 티션된다. per-worker LRU 는 없다. 비교로 CUBRID 의 private LRU 복잡도가 정당한 비용인지, 아니면 공유 캐시 패턴 에 대한 과도한 정교화인지를 가늠할 수 있다.
  • MySQL InnoDB 의 midpoint LRU + 적응형 flush. InnoDB 의 LRU 에는 head 로부터 3 / 8 지점에 midpoint 가 있다 — 새 페 이지는 head 가 아니라 midpoint 로 들어가서 scan flooding 에 저항한다. 적응형 flush 는 innodb_io_capacity 와 redo-log 지연 (lag) 에 따라 페이지 out 속도를 조절한다. CUBRID 의 3-zone LRU + dirty-ratio flush 제어와 대응된다. 나란히 비 교한 글이 도움이 될 것이다.
  • Oracle 의 multi-pool buffer cache. Oracle 은 KEEP, RECYCLE, DEFAULT 의 세 풀을 노출해서 DBA 가 hot 테이블을 KEEP 에 고정하고 스캔을 RECYCLE 로 보낼 수 있게 한다. CUBRID 에는 이런 풀 분할이 없다. 비슷한 역할을 워커 주도 private LRU 가 한다. trade-off 비교 (DBA control vs adaptive) 가 가치 있을 듯.
  • HyPer / Hekaton — buffer pool 자체가 없는 경우. in-memory 엔진은 buffer manager 를 통째로 건너뛴다. 이 비 교의 의미는, 우리가 디스크 거주 (disk-resident) 라는 이 유로 지불하는 비용을, 그 비용을 지불하지 않는 엔진과 함께 측정해 보는 것에 있다.
  • WBL 과 SSD. persistent memory 와 SSD-aware page management 에 대한 최근 연구는 16 K 페이지라는 가정 자체를 의문에 부친다. CUBRID 의 DWB 는 HDD 시대의 torn-page 방어 책으로 설계되어 있고, 최신 NVMe 위에서의 비용 / 효과는 재평 가가 필요하다.
  • Lock-free buffer manager. Sadoghi et al. LeanStore (ICDE 2018), Hekaton (VLDB 2013) 는 핫 패스에서 OS mutex 를 완전히 제거한다. CUBRID 의 BCB latch 는 사용자 정의지 만, 상태 변경에는 여전히 BCB 별 pthread_mutex_t 를 쓴다. 비교가 도달 가능한 latency 하한선을 분명히 해 준다.
  • 관련된 최근 연구 흐름. Mohan et al., ARIES (TODS 92) — WAL / buffer 프로토콜; Stoica & Ailamaki, Enabling Efficient OS Paging for Main-Memory OLTP Databases (DaMoN 13) — buffer / OS 상호작용; 본 지식 베이스의 OOS feature 설계 문서 — 최근 CUBRID buffer 관련 변경의 최신 동향.

이 섹션의 의도는 다음 문서의 씨앗을 뿌리는 것이지, 여기서 분석을 마치는 것이 아니다. 각 항목은 차례가 오면 자체 큐레이 트 노트가 되어야 한다.

원본 분석 (raw/code-analysis/cubrid/storage/buffer_manager/)

섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/buffer_manager/)”
  • CodeAnal_BM.docx — 메인 텍스트. 9 개 섹션이 BCB, fix / unfix, page table, invalid list, private / shared LRU
    • LFCQ, zone, direct victim, BCB flag, BCB latch + PGBUF lock 을 다룬다. 이 DOCX 에 임베드된 18 개 figure 가 본 문서 의 6 개 임베드 이미지의 출처다.
  • CodeAnal_BM_pt_1.pdf — 같은 내용의 PDF 렌더.
  • DesignDoc-PageBuffer_page_quota.pdf — private LRU quota 시스템의 설계 노트 (pgbuf_adjust_quotas 의 근거).
  • Storage – Concurrency 코드 분석 — Page Buffer 가 다른 모 든 모듈 (heap, index, catalog) 아래에 깔려서 I/O 경로가 모 두 통과하는 관계를 module 단위로 보여 주는 컨텍스트.

교재 챕터 (knowledge/research/dbms-general/)

섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”
  • Database System Concepts (Silberschatz, Korth, Sudarshan, 6판), 13장 Storage and File Structure — buffer manager, page replacement.
  • Database Internals (Petrov), 4장 Implementing B-Trees, 5장 “Transaction Processing and Recovery” — buffer / log 상호작용.
  • Mohan et al., ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging (TODS 1992) — buffer manager 의 WAL 순서 invariant.

CUBRID 소스 (/data/hgryoo/references/cubrid/)

섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”
  • src/storage/page_buffer.h
  • src/storage/page_buffer.c
  • src/storage/double_write_buffer.hpp
  • src/storage/double_write_buffer.cpp