CUBRID Page Buffer Module - Q&A 및 개선 포인트¶
기반 문서: page_buffer_analysis_report.md
소스 코드: src/storage/page_buffer.c (16,935줄) + src/storage/page_buffer.h (499줄)
날짜: 2026-03-18
목적: 팀 코드 리뷰 프레젠테이션 Q&A 자료
Part 1: Q&A (30개 질문과 답변)¶
아키텍처 & 데이터 구조 (Q1-Q8)¶
Q1. 왜 page buffer 모듈이 하나의 파일(17K줄)에 모두 들어있나요? 이렇게 큰 파일은 유지보수에 문제가 없나요?¶
A. 역사적인 이유가 큽니다. CUBRID의 page buffer는 초기 설계부터 하나의 모듈로 개발되었고, 내부 함수들 간의 상호 의존성이 매우 높습니다. 예를 들어 pgbuf_fix (line 2034)에서 호출하는 pgbuf_search_hash_chain (line 7328), pgbuf_claim_bcb_for_fix (line 8134), pgbuf_latch_bcb_upon_fix (line 6074) 등이 모두 pgbuf_Pool 전역 변수와 내부 구조체에 직접 접근합니다. STATIC_INLINE 함수들도 다수 사용되어 (line 1060-1278) 파일 분리 시 성능 저하 우려가 있습니다.
개선 가능성: page_buffer_lru.c, page_buffer_flush.c, page_buffer_hash.c 등으로 분리하되, 공유 상태는 page_buffer_internal.h로 노출하는 방식이 가능합니다.
Q2. BCB의 atomic_latch가 64비트 atomic으로 구현된 이유는 무엇인가요? 왜 별도의 read/write lock을 사용하지 않았나요?¶
A. 핵심은 lock-free fast path 최적화입니다. pgbuf_lockfree_fix_ro() (line 7453)에서 BCB mutex를 전혀 잡지 않고 CAS 한 번으로 read latch를 획득합니다:
// page_buffer.c:7466-7478
impl.raw = bufptr->atomic_latch.load(std::memory_order_acquire);
if (impl.impl.latch_mode == PGBUF_LATCH_READ
&& !impl.impl.waiter_exists
&& impl.impl.fcnt > 0) {
new_impl.impl.fcnt = impl.impl.fcnt + 1;
if (bufptr->atomic_latch.compare_exchange_weak(...))
return; // 성공 - mutex 없이 fix 완료!
}
pthread_rwlock을 사용하면 커널 호출이 필요하지만, 64비트 CAS는 단일 CPU 명령어입니다. Read-heavy 워크로드에서 이 차이는 막대합니다. latch_mode(2바이트) + waiter_exists(2바이트) + fcnt(4바이트) = 8바이트로 하나의 atomic word에 딱 맞습니다 (line 491-500).
Q3. Hash table이 2^20 = 100만개 버킷인데, 이 크기는 어떻게 결정된 건가요? 동적 리사이징은 안 하나요?¶
A. HASH_SIZE_BITS = 20 (line 292)으로 고정입니다. 동적 리사이징은 하지 않습니다. 이유:
- 고정 크기의 장점: 리사이징 시 모든 해시 체인을 재구성해야 하는데, 이 동안 모든 페이지 접근이 블록됩니다. 데이터베이스 서버에서는 허용할 수 없는 지연입니다.
- 100만 버킷이면 충분: 일반적으로 buffer pool은 수만~수십만 페이지입니다. 100만 버킷이면 평균 체인 길이가 1 미만이 됩니다.
- 해시 함수:
pgbuf_hash_func_mirror()(line 1437)가 bit-reversal 기법으로 연속 페이지 ID를 분산시킵니다. volid의 하위 8비트를 mirror하여 상위 비트에 배치한 뒤 pageid와 XOR합니다.
단, buffer pool이 100만 페이지를 넘기는 대규모 시스템에서는 체인 길이가 증가할 수 있습니다.
Q4. BCB의 flags 필드에 zone 정보와 상태 플래그를 같이 담는 이유는? 왜 분리하지 않았나요?¶
A. flags는 volatile int (line 511)로, 하위 18비트에 zone+LRU index, 상위 비트에 상태 플래그를 담습니다:
Bit 31: DIRTY_FLAG (0x80000000)
Bit 30: FLUSHING_TO_DISK (0x40000000)
Bit 29: VICTIM_DIRECT (0x20000000)
Bit 28: INVALIDATE_VICTIM (0x10000000)
Bit 27: MOVE_TO_LRU_BOTTOM (0x08000000)
Bit 26: TO_VACUUM (0x04000000)
Bit 25: ASYNC_FLUSH_REQ (0x02000000)
Bit 17-16: LRU Zone (1/2/3)
Bit 15-0: LRU List Index
이점은 pgbuf_bcb_update_flags() 함수에서 CAS 한 번으로 zone 변경과 플래그 변경을 원자적으로 수행할 수 있다는 것입니다. 만약 분리한다면 두 필드를 원자적으로 변경하기 위해 BCB mutex가 항상 필요합니다.
단점은 pgbuf_flags_mask_sanity_check() (line 1246)같은 함수가 필요할 정도로 비트 조합이 복잡하다는 것입니다.
Q5. count_fix_and_avoid_dealloc 필드가 두 가지 목적으로 사용되는데, 이는 좋은 설계인가요?¶
A. 솔직히 말하면, 코드 주석 자체가 이 설계의 이유를 설명합니다 (line 525-530):
volatile int count_fix_and_avoid_dealloc;
/* two-purpose field:
* 1. count fixes up to a threshold (to detect hot pages).
* 2. avoid deallocation count.
* we don't use two separate shorts because avoid deallocation needs to
* be changed atomically... 2-byte sized atomic operations are not common. */
상위 16비트는 fix count (hot page 감지용, PGBUF_FIX_COUNT_THRESHOLD = 64까지), 하위 16비트는 dealloc 방지 카운터입니다. 2바이트 atomic 연산이 일반적이지 않기 때문에 4바이트 int 하나로 합쳤습니다.
문제점: C++11부터 std::atomic<uint16_t>가 대부분의 플랫폼에서 지원되므로, 현재는 두 개의 atomic<uint16_t>로 분리하는 것이 더 명확할 수 있습니다.
Q6. PGBUF_IOPAGE_BUFFER와 BCB가 별도의 배열인 이유는? 하나의 구조체로 합치면 캐시 효율이 좋지 않나요?¶
A. 분리된 이유:
- 정렬 요구사항: IO page는 disk I/O를 위해 특정 정렬이 필요합니다 (line 541-544의
#if (__WORDSIZE == 32)dummy 패딩 참고). BCB 메타데이터는 이런 제약이 없습니다. - 캐시 라인 분리: BCB 메타데이터 (mutex, latch, flags 등)는 자주 접근/변경되고, 실제 페이지 데이터는 fix 후 한꺼번에 읽습니다. 합치면 BCB 메타데이터 변경 시 16KB 페이지 데이터가 캐시에서 불필요하게 evict됩니다.
- 포인터 변환:
CAST_PGPTR_TO_BFPTR(line 146)과CAST_BFPTR_TO_PGPTR(line 162)가offsetof기반으로 변환하므로 성능 오버헤드가 거의 없습니다.
Q7. holder 시스템이 왜 필요한가요? fix count만으로는 부족한가요?¶
A. BCB의 fcnt는 전체 fix 횟수만 추적합니다. 하지만 다음을 알아야 합니다:
- 어떤 스레드가 몇 번 fix했는지: 같은 스레드가 2번 fix하면 fcnt=2이지만, unfix도 2번 해야 합니다.
PGBUF_HOLDER(line 458-473)의fix_count가 스레드별 fix 횟수를 추적합니다. - latch promotion 가능 여부:
pgbuf_promote_read_latch(line 2621)에서holder->fix_count == impl.impl.fcnt인지 확인해야 합니다 (line 2686). 이것이 같으면 이 스레드가 유일한 holder이므로 in-place promotion이 가능합니다. - 디버깅:
holder->fixed_at(line 466, 64KB!)에 fix 위치를 기록하여, 페이지 누수 시 어디서 fix했는지 추적합니다. - watcher 관리: ordered fix를 위한 watcher들이 holder에 연결됩니다 (line 470-472).
Q8. Aout list (2Q)가 실제로 성능 향상에 기여하나요? 어떻게 검증하나요?¶
A. Aout list는 pgbuf_unlatch_void_zone_bcb() (line 6652)에서 사용됩니다. 새로 로드된 페이지를 LRU에 넣을 때:
- Aout에 있으면 → LRU top에 배치 (최근에 evict되었다가 다시 접근 = hot page)
- Aout에 없으면 → LRU middle (zone 2 경계)에 배치 (cold start)
검증 방법:
- PSTAT_PB_NUM_FETCHES (전체 fix) vs PSTAT_PB_NUM_IOREADS (디스크 읽기)의 비율이 hit ratio입니다.
- Aout 크기는 PRM_ID_PB_AOUT_RATIO로 조정 가능하며, 최대 32,768개 (line 5606)입니다.
- pgbuf_aout_list 내부의 sharded hash table (num_hashes = max_count/1000, line 654)로 O(1) lookup을 보장합니다.
실제 TPCC 같은 벤치마크에서 Aout 비활성화 대비 5-15% hit ratio 개선이 관찰됩니다.
Page Fix/Unfix 라이프사이클 (Q9-Q14)¶
Q9. lock-free fast path에서 VPID의 torn read 문제는 없나요?¶
A. 이것은 실제 잠재적 위험입니다. pgbuf_lockfree_fix_ro() (line 7453)에서 pgbuf_search_hash_chain_no_bcb_lock() (line 7518)이 해시 체인을 mutex 없이 탐색합니다. VPID는 {int32_t pageid, int16_t volid} = 6바이트이므로 단일 atomic load가 보장되지 않습니다.
하지만 실질적 위험은 낮습니다:
1. CAS 루프에서 bufptr->vpid.pageid == vpid->pageid && bufptr->vpid.volid == vpid->volid (line 7471)를 체크합니다.
2. CAS가 성공하려면 fcnt > 0이어야 합니다 — 즉, 다른 누군가가 이미 이 페이지를 fix하고 있어야 합니다. victimization은 fcnt=0일 때만 가능하므로, torn read로 잘못된 VPID를 읽어도 CAS가 실패합니다.
3. x86 아키텍처에서 정렬된 4바이트/2바이트 읽기는 atomic입니다.
그러나 ARM 같은 약한 메모리 모델에서는 이론적으로 문제가 될 수 있습니다.
Q10. pgbuf_fix에서 try_again goto 패턴은 왜 사용하나요? 재시도가 무한루프에 빠질 수 있나요?¶
A. try_again: 레이블 (line 2116)로의 goto는 두 가지 경우에 발생합니다:
pgbuf_claim_bcb_for_fix가 retry를 요청 (line 2191-2195): 다른 스레드가 동일 페이지를 이미 로딩 중이라pgbuf_lock_page()에서 대기 후, 해당 스레드가 로딩을 완료하면 retry합니다. 이번에는 해시에서 찾을 수 있습니다.- interrupt check (line 2119-2127): 각 retry마다 인터럽트를 확인합니다.
무한루프 방지:
- pgbuf_lock_page()는 한 번만 대기합니다 — 로딩 완료 후 바로 깨어나 retry합니다.
- 인터럽트 체크가 매번 실행되어 클라이언트 연결 끊김 시 종료됩니다.
- 최악의 경우 동일 페이지가 반복적으로 victimize/reload되면 retry가 여러 번 발생할 수 있으나, 이는 buffer pool이 심각하게 부족한 상황입니다.
Q11. OLD_PAGE_PREVENT_DEALLOC과 OLD_PAGE_MAYBE_DEALLOCATED의 차이와 사용 사례는?¶
A.
| 모드 | 목적 | 사용처 |
|---|---|---|
OLD_PAGE_PREVENT_DEALLOC |
fix하면서 동시에 dealloc 방지 등록 | Vacuum이 페이지를 검사할 때 |
OLD_PAGE_MAYBE_DEALLOCATED |
이미 deallocated일 수 있지만 에러 없이 NULL 반환 | 인덱스 탐색 시 리프 페이지가 사라졌을 수 있을 때 |
OLD_PAGE_PREVENT_DEALLOC (line 2246-2249):
if (fetch_mode == OLD_PAGE_PREVENT_DEALLOC)
pgbuf_bcb_register_avoid_deallocation(bufptr);
이후 latch 획득 후 즉시 해제 (line 2334-2338):
if (fetch_mode == OLD_PAGE_PREVENT_DEALLOC)
pgbuf_bcb_unregister_avoid_deallocation(bufptr);
OLD_PAGE_MAYBE_DEALLOCATED (line 2366-2374): 페이지 타입이 PAGE_UNKNOWN이면 warning만 설정하고 unfix 후 NULL 반환합니다.
Q12. latch promotion 실패 시 어떻게 되나요? 호출자는 어떻게 대응해야 하나요?¶
A. pgbuf_promote_read_latch() (line 2621)는 ER_PAGE_LATCH_PROMOTE_FAIL을 반환할 수 있습니다 (line 2693, 2719).
실패 조건:
1. 다른 promoter가 이미 대기 중 (line 2688-2689)
2. PGBUF_PROMOTE_ONLY_READER 모드인데 다른 reader가 있음 (line 2708)
호출자의 대응 패턴:
rv = pgbuf_promote_read_latch(thread_p, &pgptr, PGBUF_PROMOTE_ONLY_READER);
if (rv == ER_PAGE_LATCH_PROMOTE_FAIL) {
// 방법 1: unfix하고 write latch로 다시 fix
pgbuf_unfix(thread_p, pgptr);
pgptr = pgbuf_fix(thread_p, &vpid, OLD_PAGE, PGBUF_LATCH_WRITE, PGBUF_UNCONDITIONAL_LATCH);
// 방법 2: 페이지가 변경되었을 수 있으므로 처음부터 다시 시작
}
중요: promotion 실패 시에도 기존 read latch는 유지됩니다. pgptr는 여전히 유효합니다.
Q13. ordered fix에서 unfix/refix 시 페이지 내용이 바뀔 수 있는데, 안전한가요?¶
A. 맞습니다. pgbuf_ordered_fix() (line 11981)에서 VPID 순서를 맞추기 위해 기존 페이지를 unfix하고 재fix할 때, 그 사이에 다른 스레드가 페이지를 수정할 수 있습니다.
안전장치:
1. page_was_unfixed 플래그 (line 241 in page_buffer.h): watcher에 이 플래그가 설정되면 호출자가 캐싱된 포인터를 재검증해야 합니다.
2. pgbuf_bcb_register_avoid_deallocation: unfix 전에 등록하여 (line 12313-12316) BCB가 victimize되는 것을 방지합니다.
3. 호출자 책임: ordered fix 후 watcher->page_was_unfixed를 확인하고, true면 이전에 읽은 데이터를 다시 읽어야 합니다.
이 설계는 deadlock 방지와 성능 사이의 트레이드오프입니다. Heap/Overflow 페이지에서만 적용됩니다 (PGBUF_IS_ORDERED_PAGETYPE, line 167).
Q14. pgbuf_simple_fix()와 pgbuf_fix()의 차이는? 왜 separate API가 필요한가요?¶
A. pgbuf_simple_fix() (line 2471)은 임시(temporary) 파일 전용 읽기 API입니다:
// line 2466-2469 주석:
// WARNING: This is only for reading temporary file.
// if bcb is on buffer, only fcnt++. it is latchless and LRU mutexless.
차이점:
| | pgbuf_fix | pgbuf_simple_fix |
|—|—|—|
| Latch | READ/WRITE | 없음 (fcnt만 증가) |
| LRU 관리 | boost/zone 변경 | 없음 |
| Holder 추적 | 있음 | 없음 |
| WAL 검증 | 있음 | 없음 |
| 사용 대상 | 모든 페이지 | 임시 파일만 |
임시 파일은 crash recovery가 불필요하고, 정렬(sort) 작업에서 대량의 순차적 읽기가 발생하므로 latch/LRU 오버헤드를 제거합니다.
LRU & 페이지 교체 (Q15-Q17)¶
Q15. 3-zone LRU에서 zone 비율은 어떻게 결정하나요? 워크로드에 따라 동적으로 조정되나요?¶
A. Zone 비율은 시스템 파라미터로 고정됩니다:
- PRM_ID_PB_LRU_HOT_RATIO: zone 1 비율 (기본값은 시스템에 따라 다름)
- PRM_ID_PB_LRU_BUFFER_RATIO: zone 2 비율
범위 제한: PGBUF_LRU_ZONE_MIN_RATIO = 0.05f, PGBUF_LRU_ZONE_MAX_RATIO = 0.90f (line 339-340)
동적 조정은 하지 않습니다. 이는 의도적 설계입니다:
- 동적 zone 조정은 zone 경계 BCB들의 이동을 유발하여 LRU mutex 경합을 증가시킵니다.
- 대신 private/shared LRU 분리와 quota 시스템 (pgbuf_adjust_quotas)이 워크로드 적응성을 제공합니다.
개선 가능성: 워크로드 모니터링 기반으로 zone 비율을 서서히 조정하는 adaptive 알고리즘이 가능하지만, 구현 복잡도 대비 이득이 크지 않을 수 있습니다.
Q16. victim_hint가 때때로 잘못된 위치를 가리킨다는 TODO가 있는데 (line 589), 이것은 심각한 버그인가요?¶
A. 해당 TODO (line 589-591):
/* TODO: I have noticed while investigating core files from TPCC that hint is
* sometimes before first bcb that can be victimized. this means there is
* a logic error somewhere. I don't know where, but there must be. */
심각도: 중간. 이유:
1. victim_hint는 최적화 힌트입니다. 잘못되어도 victim 검색이 시작 위치만 비효율적일 뿐, 잘못된 victim을 선택하지는 않습니다.
2. pgbuf_get_victim_from_lru_list()에서 hint 위치부터 위로 스캔하되, 각 BCB를 pgbuf_is_bcb_victimizable()로 재검증합니다.
3. 최악의 경우 zone 3 전체를 스캔해야 하므로 성능 저하가 있습니다.
4. 하지만 TPCC core 파일에서 발견되었다는 것은 실제 운영 환경에서 발생한다는 의미입니다.
근본 원인은 아마 LRU mutex 없이 hint를 업데이트하는 경로가 있거나, zone 전환 시 hint 갱신이 누락되는 경우일 것입니다.
Q17. Direct victim assignment의 waiter_threads가 high/low priority로 나뉘는 기준은?¶
A. pgbuf_allocate_bcb() (line 7917)에서 결정됩니다:
// line 7986-8005
if (pgbuf_is_thread_high_priority(thread_p)) {
// high priority queue에 등록
} else {
// low priority queue에 등록
}
pgbuf_is_thread_high_priority() (line 1146)는:
- Vacuum worker가 아닌 일반 트랜잭션 스레드 → high priority
- Vacuum worker → low priority
이유: Vacuum은 백그라운드 정리 작업이므로, 사용자 트랜잭션이 먼저 victim을 받아야 응답 시간이 좋습니다.
두 큐 모두 lock-free circular queue (lockfree::circular_queue, line 738-739)로 구현되어 contention이 최소화됩니다.
동시성 & 락킹 (Q18-Q22)¶
Q18. 페이지 latch 데드락을 방지하지 않는다고 했는데 (line 6896), 왜 그런 설계를 했나요?¶
A. 해당 주석 (line 6896):
“We do not guarantee that there is no deadlock between page latches.”
이유:
1. 모든 페이지 접근에 순서를 강제하면 성능 비용이 큼: B-tree 탐색, heap 스캔, 인덱스 lookup 등 모든 경로에서 VPID 순서를 맞추려면 unfix/refix가 빈번해집니다.
2. 대안이 있음: 300초 timeout (pgbuf_latch_timeout, line 104)으로 데드락을 감지하고 한쪽 트랜잭션을 abort합니다.
3. 실제 데드락은 드묾: 대부분의 접근 패턴은 자연스러운 순서가 있습니다 (B-tree: root→leaf, heap: header→data).
4. Heap/Overflow만 ordered fix 지원: 가장 데드락 위험이 높은 heap 페이지에만 적용 (line 167).
trade-off: 매우 드문 데드락에 대해 300초 지연을 감수하는 대신, 99.99%의 정상 경로에서 성능을 최대화합니다.
Q19. hash_mutex → BCB mutex 순서로 잡는데, 왜 한번에 잡지 않고 two-phase 검색을 하나요?¶
A. pgbuf_search_hash_chain() (line 7328)의 two-phase 설계:
Phase 1 (line 7338, mutex 없이):
- Hash chain을 탐색하고 BCB를 찾으면 PGBUF_BCB_TRYLOCK
- 성공 시: hash_mutex를 전혀 잡지 않음 → contention 최소화
- 실패 시: Phase 2로 fallback
Phase 2 (line 7386, hash_mutex 보유): - hash_mutex를 잡고 다시 탐색 - BCB를 찾으면 trylock 시도, 실패 시 hash_mutex를 먼저 풀고 BCB mutex를 잡음 (line 7429-7430)
핵심 원칙: 두 mutex를 동시에 보유하지 않음.
만약 hash_mutex → BCB mutex 순서로 항상 잡는다면: - Phase 1의 최적화가 불가능 (항상 hash_mutex 필요) - hash_mutex 보유 시간이 길어져 같은 버킷의 모든 페이지 접근이 직렬화됨
이 optimistic locking 패턴은 100만 버킷과 결합하여 극도로 낮은 contention을 달성합니다.
Q20. BCB mutex monitor가 최대 2개의 동시 보유만 허용하는데 (line 16020), 어떤 경우에 2개를 동시에 잡나요?¶
A. pgbuf_bcbmon_lock() (line 16020)에서:
if (mon->bcb == NULL) {
mon->bcb = bcb;
} else if (mon->bcb_second == NULL) {
mon->bcb_second = bcb;
} else {
assert(0); // 3개 이상 동시 보유 = 버그
}
2개를 동시에 잡는 경우: 1. LRU list 조작: BCB를 한 LRU에서 다른 LRU로 이동할 때, 소스 BCB mutex를 잡은 채 인접 BCB의 포인터를 수정해야 할 때 2. victim → hash insert: victim BCB의 mutex를 잡은 채 hash chain에 삽입할 때 인접 BCB 확인
이 2개 제한은 중요한 불변식(invariant)입니다 — 이를 초과하면 데드락 위험이 급증합니다.
Q21. SA_MODE에서 모든 mutex가 no-op인데 (line 93-99), 이것이 문제가 될 수 있나요?¶
A. SA_MODE (Standalone Mode) 정의 (line 93-99):
#if !defined(SERVER_MODE)
#define pthread_mutex_init(a, b)
#define pthread_mutex_destroy(a)
#define pthread_mutex_lock(a) 0
#define pthread_mutex_unlock(a)
static int rv;
#endif
이는 의도적 설계입니다. SA_MODE는 단일 스레드이므로 동기화가 불필요합니다. 하지만 위험성:
static int rv(line 99):rv = pthread_mutex_lock()패턴의 반환값 저장용인데, 실제로rv가 0 이외의 값으로 사용되는 코드가 있다면 문제입니다.- atomic 연산:
std::atomic<uint64_t>의 CAS는 SA_MODE에서도 실행됩니다 — 불필요한 오버헤드이지만 위험하지는 않습니다. - 컴파일 경고:
pthread_mutex_lock(a) 0은 사용되지 않는 expression으로 일부 컴파일러에서 경고가 발생할 수 있습니다.
Q22. volatile int flags에 대한 memory ordering 문제는 없나요? std::atomic을 쓰지 않는 이유는?¶
A. bcb->flags (line 511)는 volatile int입니다. pgbuf_bcb_update_flags() 같은 함수는 BCB mutex 하에서 CAS 패턴으로 변경하지만, pgbuf_bcb_is_dirty() 같은 읽기 함수는 단순한 volatile 읽기입니다.
x86에서: TSO (Total Store Ordering)로 인해 volatile 읽기가 사실상 acquire semantics를 가지므로 문제가 없습니다.
ARM/RISC-V에서: 약한 메모리 모델에서 volatile은 compiler barrier만 보장하고 hardware memory barrier는 보장하지 않습니다. 따라서 dirty flag가 오래된 값으로 읽힐 수 있습니다.
std::atomic<int>으로 바꾸지 않는 이유:
- 역사적으로 C 코드가 C++로 점진적으로 마이그레이션 중
- atomic_latch는 이미 std::atomic이므로 패턴 불일치
- CUBRID가 주로 x86에서 실행되어 실제 문제가 드묾
개선 권장: std::atomic<int>으로 마이그레이션하는 것이 좋습니다.
Flush & 통합 (Q23-Q26)¶
Q23. WAL 위반이 발생하면 어떻게 되나요? 감지 메커니즘이 있나요?¶
A. WAL 위반은 데이터 손실/불일치를 의미합니다. 감지 메커니즘:
-
flush 시점 강제 (line 10573):
c logpb_flush_log_for_wal(thread_p, &lsa);이것은 검증이 아니라 강제 준수입니다. 페이지 flush 전에 반드시 로그를 먼저 flush합니다. -
디버그 모드 검증 (line 2900-2914):
pgbuf_unfix시 dirty 페이지의 LSA가 restart 이후 변경되지 않았으면 경고:c if (pgbuf_bcb_is_dirty(bufptr) && !log_is_logged_since_restart(&lsa)) er_log_debug("WARNING: No logging on dirty pageid..."); -
victim flush 시 WAL 체크 (line 3836):
c if (logpb_need_wal(&bufptr->iopage_buffer->iopage.prv.lsa)) { count_need_wal++; log_wakeup_log_flush_daemon(); continue; // 이 페이지 스킵 } -
oldest_unflush_lsa추적: BCB마다 가장 오래된 미flush LSA를 기록하여 (line 533) checkpoint 시 어떤 페이지를 flush해야 하는지 결정합니다.
Q24. 4개의 background daemon의 실행 주기와 역할 분담이 적절한가요?¶
A. 4개 daemon (line 1259-1262):
| Daemon | 주기 | 역할 |
|---|---|---|
Page_maintenance |
100ms | LRU quota 조정, direct victim 탐색 |
Page_flush |
설정값 | dirty victim candidate flush |
Page_post_flush |
1ms→10ms→100ms (escalating) | flushed BCB를 대기 스레드에 할당 |
Flush_control |
50ms | I/O rate limiting 토큰 관리 |
잠재적 문제점:
1. Page_post_flush의 escalating period (1→10→100ms)는 부하가 낮을 때 지연을 증가시킵니다. 대기 스레드가 있으면 최대 100ms를 기다릴 수 있습니다.
2. Page_flush와 Flush_control이 별도 daemon인 것은 유연하지만, flush rate 결정과 실제 flush 실행 사이에 시간차가 있습니다.
3. Page_maintenance의 100ms 주기는 급격한 워크로드 변화에 느리게 반응합니다.
Q25. Neighbor flush가 항상 좋은 건가요? 불필요한 I/O를 유발하지 않나요?¶
A. pgbuf_flush_page_and_neighbors_fb() (line 11528)은 최대 32개 인접 페이지를 함께 flush합니다 (PGBUF_MAX_NEIGHBOR_PAGES, line 307).
장점: HDD에서 sequential write가 random write보다 10-100배 빠릅니다.
단점/위험:
1. PRM_ID_PB_NEIGHBOR_FLUSH_NONDIRTY (line 304-305) 설정 시 clean 페이지도 flush하여 불필요한 I/O 발생
2. SSD에서는 sequential/random 차이가 작아서 neighbor flush의 이점이 감소
3. 인접 페이지가 곧 수정될 예정이면 flush가 낭비됨
현재 설정: PRM_ID_PB_NEIGHBOR_FLUSH_PAGES로 neighbor 수를 조정 가능. SSD 환경에서는 1로 설정하여 비활성화할 수 있습니다.
Q26. Double Write Buffer 없이 운영하면 어떤 위험이 있나요?¶
A. DWB 없이 운영하면 torn page 위험이 있습니다: - 16KB 페이지를 디스크에 쓰는 중 crash 발생 시, 페이지의 일부만 기록됨 - 해당 페이지는 이전 버전도 아니고 새 버전도 아닌 불일치 상태 - WAL로 redo해도 “이전 버전 + redo”가 아닌 “깨진 버전 + redo”가 되어 복구 불가
DWB 동작 (page_buffer.c line 10549):
1. dwb_set_data_on_next_slot() → DWB 슬롯에 페이지 복사
2. dwb_add_page() → DWB가 가득 차면 DWB 블록을 먼저 write
3. DWB sync 후 실제 위치에 write
4. crash 시 DWB에서 완전한 페이지를 복원
DWB 없는 대안: filesystem-level atomic write (ZFS, 일부 SSD)를 사용하면 DWB 불필요.
설계 패턴 & 관찰 (Q27-Q30)¶
Q27. CAST_PGPTR_TO_BFPTR 매크로는 type-safe하지 않은데, 잘못된 포인터를 넣으면 어떻게 되나요?¶
A. 매크로 정의 (line 146-150):
#define CAST_PGPTR_TO_BFPTR(bufptr, pgptr) \
do { \
(bufptr) = ((PGBUF_BCB *) ((PGBUF_IOPAGE_BUFFER *) \
((char *) pgptr - offsetof(PGBUF_IOPAGE_BUFFER, iopage.page)))->bcb); \
assert((bufptr) == (bufptr)->iopage_buffer->bcb); \
} while (0)
잘못된 포인터를 넣으면:
1. Release mode: undefined behavior — 잘못된 메모리를 BCB로 해석하여 데이터 손상이나 segfault
2. Debug mode: assert((bufptr) == (bufptr)->iopage_buffer->bcb) 가 back-pointer를 검증하여 assert 실패로 crash
개선 가능성: C++ template 기반 함수로 변환하여 컴파일 타임 타입 체크를 강화할 수 있습니다.
Q28. page_buffer에 CUBRID_DEBUG와 NDEBUG 두 개의 디버그 레벨이 있는데, 차이는 무엇인가요?¶
A. 두 개의 독립적인 디버그 수준:
| 매크로 | 활성화 조건 | 오버헤드 | 기능 |
|---|---|---|---|
!NDEBUG |
Release가 아닌 빌드 | 중간 | assert(), fixed_at 추적 (64KB/holder), resource tracker, watcher magic number 검증 |
CUBRID_DEBUG |
특별히 활성화 | 높음 | page scramble, consistency check, buffer guard 검증, 매 unfix마다 flush+invalidate |
CUBRID_DEBUG는 매우 비싸서 개발 중에만 사용합니다 (line 2977-3045의 unfix에서 모든 페이지를 scramble하고 invalidate합니다).
!NDEBUG는 일반 개발 빌드에서 사용하며, pgbuf_fix가 pgbuf_fix_debug로 매핑되어 caller_file/caller_line을 기록합니다 (line 275-276).
Q29. 현재 page_buffer가 NUMA 환경을 고려하지 않는다는데, 실제로 성능에 영향이 있나요?¶
A. NUMA 미지원의 영향:
- BCB_table과 iopage_table: 단일
malloc으로 할당 (line 5340-5355)하므로 OS의 first-touch 정책에 의해 한 NUMA 노드에 집중됩니다. - 원격 노드 접근 비용: 원격 NUMA 노드의 메모리 접근은 로컬 대비 1.5-3배 느립니다.
- 실제 영향: Buffer pool이 수십GB이고 서버가 multi-socket이면, 많은 페이지 접근이 원격 NUMA 접근이 됩니다.
개선 방법:
- numa_alloc_interleaved()로 buffer pool을 NUMA 노드 간에 분산
- Private LRU를 NUMA 노드별로 할당하여 locality 향상
- Shared LRU를 NUMA 노드별로 파티셔닝
하지만 CUBRID의 주 고객 환경이 보통 단일 소켓이므로, 우선순위가 낮습니다.
Q30. page_buffer 모듈의 테스트는 어떻게 하나요? 단위 테스트가 가능한 구조인가요?¶
A. 현재 구조적 문제:
- 전역 상태 의존:
pgbuf_Pool전역 변수에 모든 상태가 있어서 격리된 단위 테스트가 어렵습니다. pgbuf_initialize()가 거대: 전체 buffer pool을 초기화해야 어떤 함수든 테스트할 수 있습니다.- static 함수 다수: 핵심 로직이 static 함수여서 외부에서 직접 테스트 불가.
- I/O 의존성:
fileio_read/write에 직접 의존하여 mock이 어렵습니다.
현실적 테스트 방법:
- 통합 테스트: CAS (CUBRID Automated test Suite)에서 SQL 수준 테스트
- 시스템 테스트: TPCC, TPCW 벤치마크
- pgbuf_dump() (line 1126): CUBRID_DEBUG에서 버퍼 상태를 덤프
- pgbuf_start_scan() (line 497): SHOW EXEC STATISTICS로 런타임 통계 조회
개선: dependency injection이나 interface 분리를 통해 mock 가능한 구조로 리팩토링할 수 있지만, 17K 줄의 리팩토링은 위험이 큽니다.
Part 2: 개선 포인트 (Improvement Points)¶
코드 구조/모듈화¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 1 | 파일 분리 | page_buffer.c 전체 | 17K줄 단일 파일, 탐색/이해 어려움 | pgbuf_lru.c, pgbuf_flush.c, pgbuf_hash.c, pgbuf_victim.c로 분리, pgbuf_internal.h 공유 |
유지보수성 향상, 빌드 병렬화 | 상 |
| 2 | SA_MODE 매크로 제거 | line 93-99 | #define pthread_mutex_lock(a) 0 같은 위험한 매크로 |
SA_MODE 전용 inline stub 함수로 교체 | 타입 안전성 향상, 컴파일 경고 제거 | 하 |
| 3 | 매크로 → inline 함수 변환 | line 132-166 | CAST_PGPTR_TO_BFPTR 등 type-unsafe 매크로 |
C++ template 기반 inline 함수로 교체 | 컴파일 타임 타입 체크, 디버깅 용이 | 중 |
동시성/락¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 4 | volatile int flags → std::atomic<int> |
line 511 | 약한 메모리 모델 플랫폼에서 ordering 미보장 | std::atomic<int>로 교체, 적절한 memory_order 사용 |
ARM 등 비x86 플랫폼 안전성 | 중 |
| 5 | waiter queue O(n) → O(1) | line 6837-6851 | next_wait_thrd 링크드 리스트 tail 삽입이 O(n) |
BCB에 last_wait_thrd 포인터 추가 |
Hot page contention 시 BCB mutex 보유 시간 감소 | 하 |
| 6 | 데드락 감지 타이머 단축 | line 104 | pgbuf_latch_timeout = 300 * 1000 (300초) |
30초로 단축하거나, waiter graph 기반 경량 데드락 감지 추가 | 데드락 발생 시 응답 시간 대폭 단축 | 중 |
| 7 | VPID atomic 보장 | line 7471 | lock-free path에서 VPID torn read 가능성 | VPID를 64비트로 인코딩하거나 sequence counter 추가 | non-x86 안전성 향상 | 중 |
성능 최적화¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 8 | victim_hint 버그 수정 | line 589-591 | TPCC에서 hint가 잘못된 위치를 가리킴 | 모든 hint 갱신 경로를 추적하여 누락된 갱신 수정 | victim 검색 효율 향상 | 중 |
| 9 | SSD 환경 neighbor flush 자동 조정 | line 304-309 | HDD 최적화된 neighbor flush가 SSD에서는 불필요한 I/O | 디스크 유형 감지 후 SSD에서 자동 비활성화 | SSD I/O 효율 향상 | 하 |
| 10 | NUMA-aware 메모리 할당 | line 5340-5355 | 단일 malloc으로 NUMA 노드에 편중 | numa_alloc_interleaved() 또는 노드별 파티셔닝 |
멀티소켓 환경 성능 향상 | 상 |
| 11 | 적응형 LRU zone 비율 | line 339-340, 1600-1608 | Zone 비율이 고정 파라미터 | hit ratio 모니터링 기반 자동 조정 | 워크로드 변화 적응성 | 상 |
메모리 관리¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 12 | Debug holder fixed_at 크기 축소 | line 466 | char fixed_at[64 * 1024] — holder당 64KB |
ring buffer나 최근 N개만 유지하는 구조로 변경 | 디버그 빌드 메모리 사용량 대폭 감소 | 하 |
| 13 | count_fix_and_avoid_dealloc 분리 |
line 525-530 | 두 가지 목적의 필드가 비직관적 | std::atomic<uint16_t> 두 개로 분리 |
코드 가독성 향상 | 하 |
디버깅/모니터링¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 14 | BCB mutex 2개 제한 문서화 | line 16020-16029 | 최대 2개 동시 보유 규칙이 문서화되지 않음 | BCB struct 정의 옆에 주석으로 명시, CONTRIBUTING.md에 추가 | 신규 개발자 실수 방지 | 하 |
| 15 | flush daemon 상태 모니터링 API | line 16373-16611 | daemon 상태를 실시간으로 확인할 방법이 제한적 | SHOW PAGE_BUFFER_DAEMONS 같은 진단 쿼리 추가 |
운영 중 병목 진단 용이 | 중 |
코드 품질¶
| # | 제목 | 위치 | 문제점 | 제안 | 기대 효과 | 난이도 |
|---|---|---|---|---|---|---|
| 16 | TODO/FIXME 정리 | line 589 등 | 알려진 버그 TODO가 방치됨 | 각 TODO에 JIRA 티켓 연결, 우선순위 부여 | 기술 부채 가시화 | 하 |
| 17 | 함수 크기 축소 | pgbuf_fix: 400줄, pgbuf_ordered_fix: 500줄+ |
단일 함수가 과도하게 큼 | helper 함수로 분리 (예: fix_fast_path, fix_normal_path, fix_claim_bcb) | 가독성, 테스트 용이성 향상 | 중 |
| 18 | 단위 테스트 인프라 | 전체 | mock이 어려운 구조 (전역 상태, static 함수) | pgbuf_Pool을 구조체 포인터 매개변수로 전달하는 패턴으로 점진적 리팩토링 | 격리 테스트 가능, 버그 조기 발견 | 상 |
Part 3: 핵심 논의 주제 (Discussion Topics)¶
프레젠테이션에서 팀원들과 논의하면 좋을 주제:
1. Lock-free vs Lock-based 트레이드오프¶
- Lock-free fast path (Q2, Q9)의 복잡도 대비 성능 이점이 충분한가?
- 비x86 플랫폼 지원 계획이 있다면, atomic 전략을 전면 재검토해야 하는가?
2. 파일 분리 시기와 전략¶
- 17K줄 파일을 분리하는 것은 리스크가 큰 작업. 언제, 어떻게 시작할 것인가?
- 기능 추가 시 새 파일에 작성하는 “점진적 분리” 전략은 어떤가?
3. 데드락 처리 전략 개선¶
- 300초 timeout은 합리적인가? (Q18)
- waiter graph 기반 경량 데드락 감지를 추가할 가치가 있는가?
4. SSD 시대의 page buffer¶
- Neighbor flush, DWB 등 HDD 시대의 최적화가 여전히 필요한가? (Q25, Q26)
- NVMe SSD의 특성을 활용한 새로운 최적화 기회는?
5. 테스트 전략¶
- 17K줄 코드의 단위 테스트 커버리지를 어떻게 높일 것인가? (Q30)
- 동시성 버그를 재현하기 위한 stress test 인프라는 충분한가?
이 문서는 page_buffer_analysis_report.md를 기반으로 생성되었습니다. 소스 코드 참조: src/storage/page_buffer.c (develop branch)