(KO) CUBRID Disk Manager와 File Manager — 볼륨, 섹터, 파일, 페이지 할당, 그리고 볼륨 확장
목차
학술적 배경
섹션 제목: “학술적 배경”서버 프로세스보다 오래 살아남는 데이터를 다루려는 RDBMS는 OS의 파일 추상만으로 끝낼 수 없다. Database Internals(Petrov) 3장 File Formats이 그 차이를 정리해 준다. OS는 엔진에게 바이트 흐름이 담긴 파일을 넘겨주지만, 엔진이 받고 싶은 모양은 테이블이나 인덱스 단위로 묶인 고정 크기 페이지의 집합이다. 이 집합은 필요할 때 키울 수 있어야 하고, 자리를 거두어 들일 때마다 syscall을 부르지 않아도 되어야 한다. 그래서 디스크 위에 데이터를 두는 모든 DBMS가 OS 파일 시스템 위에 자기만의 작은 파일 시스템을 다시 짠다.
이 작은 파일 시스템에 들어가야 할 부품은 교과서적으로 셋이다.
- 페이지 식별자 체계. I/O의 가장 작은 단위는 고정 크기의 페이지다. 엔진은 페이지마다 안정된 식별자를 매단다. 그래야 인덱스 항목, 로그 레코드, 그리고 버퍼 매니저가 OS 파일 어디 오프셋을 다시 따지지 않고도 페이지를 가리킬 수 있다. 식별자는 보통 어느 통이냐 + 그 안의 몇 번째 페이지냐로 나뉜다. PostgreSQL은
(relfilenode, blocknumber), Oracle은 DBA(파일 번호 + 블록 번호), CUBRID는VPID = (volid, pageid)로 적는다. - 페이지보다 큰 할당 단위. 페이지를 한 장씩 떼어 주면 테이블이 자랄수록 단편화가 쌓인다. 교과서가 내놓는 답은 익스텐트(extent)다. 한 번에 N장의 연속 페이지를 한 테이블에 묶어 주고, 통마다의 비트맵으로 그것을 잡아 둔다. Oracle, InnoDB, SQL Server 모두 익스텐트라는 이름을 쓴다(크기만 다르다). CUBRID는 그 단위를 섹터(sector)라 부르고, 기본값은 64페이지 × 16 KB = 1 MB다.
- 자라는 전략. 통이 가득 차면 그 통을 키우거나 새 통을 더해야 한다. 이 일은 크래시 앞에서도 안전해야 하고(자라다 끊겨도 가짜로 가득 찬 모양이 되어선 안 된다), 동시성 친화적이어야 한다(어떤 트랜잭션이 자라는 일을 하고 있다고 해서 같은 볼륨의 다른 테이블을 읽는 트랜잭션이 막혀선 안 된다). ARIES 식 nested top-action(Mohan et al. 1992)이 정석적인 답이다.
이 모델을 따르는 모든 디스크 기반 엔진에는 두 갈래의 결정이 따라붙는다.
- 영속 데이터와 일시 데이터를 어떻게 가를 것인가. 정렬 결과, 해시 파티션, 질의 중간 결과 같은 것들은 페이지를 필요로 하되 크래시 뒤에 살아남을 까닭이 없다. 이런 데이터를 일반 테이블 데이터와 같은 길로 보내면 WAL 비용을 헛으로 치르게 된다. 엔진들은 대개 별도의 임시 테이블스페이스를 둔다.
- 테이블마다의 할당 비트맵을 어디에 둘 것인가. PostgreSQL은 테이블마다의 FSM 포크를 별도 파일로 둔다. InnoDB는 테이블스페이스 단위의 관리 영역에 익스텐트 디스크립터를 모아 둔다. CUBRID는 그 비트맵을 파일(여기서는 CUBRID의 논리적 파일이다) 안쪽에 두 개의 표로 끼워 둔다. 가용 페이지가 남아 있는 섹터를 담는 Partial Sectors Table과, 모든 페이지가 잡힌 섹터를 담는 Full Sectors Table이 그것이다.
이 문서는 CUBRID가 위 부품들을 src/storage/disk_manager.{h,c}(볼륨/섹터 계층)과 src/storage/file_manager.{h,c}(파일/페이지 계층)에 어떻게 짜 넣었는지를 따라간다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서가 모델을 깔아 두면, 이 절은 PostgreSQL · Oracle · MySQL InnoDB · SQL Server · CUBRID 같은 디스크 기반 DBMS가 함께 쥐고 있는 공학 어휘를 짚는다. 다음 절에서 살펴볼 CUBRID의 구체적 선택은 이 공통의 설계 공간 위에 박힌 한 점이라고 보면 된다.
볼륨 = 페이지 다발이 든 OS 파일
섹션 제목: “볼륨 = 페이지 다발이 든 OS 파일”엔진의 가장 아래 계층은 OS 파일 하나하나를 볼륨으로 본다. 볼륨이란 엔진이 자기 마음대로 모양을 짤 수 있는, 고정 크기 페이지의 이어진 자리다. 0번 페이지는 볼륨 헤더로, 그 볼륨 자체의 메타데이터를 들고 있다. 그 뒤의 일정 구간은 어느 익스텐트가 쓰이고 있는지를 짚어 두는 비트맵이다. 나머지가 실제 데이터를 받아 주는 자리다. PostgreSQL은 릴레이션 포크마다 OS 파일 한 개, Oracle은 데이터 파일 한 개당 OS 파일 한 개, InnoDB는 테이블스페이스마다 OS 파일 한 개, CUBRID는 볼륨마다 OS 파일 한 개를 쓴다. CUBRID 데이터베이스 한 개는 적어도 영구 볼륨 한 개를 가지고, 운영 도중에 수십 개로 자랄 수 있다.
섹터 = 디스크 매니저의 할당 단위
섹션 제목: “섹터 = 디스크 매니저의 할당 단위”페이지 한 장씩 떼어 주는 길은 입출력과 락 비용이 너무 무겁다. 할당 한 번이 비트맵, 버퍼 풀, WAL을 동시에 건드려야 하기 때문이다. 익스텐트 단위로 떼어 주면 그 비용이 흩어진다. 익스텐트 크기는 손볼 수 있는 자리다. 작은 익스텐트(예: Oracle의 INITIAL 64K)는 작은 테이블에서 자리를 덜 버리지만 비트맵 항목이 늘어난다. 큰 익스텐트(예: InnoDB의 1 MB)는 비트맵 트래픽을 줄이지만, 한 익스텐트에도 못 미치는 작은 테이블에서는 그만큼 자리가 비어 버린다. CUBRID는 64페이지 × 16 KB = 1 MB를 기본값으로 잡는다.
파일 = 한 릴레이션이 들고 있는 섹터 다발
섹션 제목: “파일 = 한 릴레이션이 들고 있는 섹터 다발”릴레이션 단위 정체와 볼륨 단위 정체가 한 번에 들어맞는 일은 드물다. B-트리 한 그루는 여러 볼륨에 흩어진 수십 개 섹터에 걸쳐 있을 수 있고, 한 볼륨에는 수백 개의 릴레이션이 들어와 있을 수 있다. 그래서 논리적 파일 계층이 끼어든다. 릴레이션마다 번호가 매겨진 파일을 한 개 가지고, 파일은 자기가 받아 둔 섹터 목록을 들고 있으며, 그 섹터들은 한 볼륨이거나 여러 볼륨에 걸쳐 있다. PostgreSQL의 relfilenode, Oracle의 segment, InnoDB의 테이블별 .ibd, CUBRID의 VFID = (volid, fileid)가 모두 그 자리다. CUBRID에서 fileid 부분은 파일의 헤더 페이지의 page id와 같다. 그래서 파일 매니저는 자기 메타데이터의 자리를 O(1)에 짚을 수 있다.
영구 목적과 임시 목적의 갈라짐
섹션 제목: “영구 목적과 임시 목적의 갈라짐”정렬을 위해 페이지 한 장이 필요한 질의는 그 페이지가 바뀔 때마다 WAL 레코드를 남기고 싶지는 않다. 엔진은 저장소를 영구 목적(로깅과 내구성)과 임시 목적(로깅 없음, 크래시 뒤에는 버림)으로 가른다. Oracle의 PERMANENT / TEMPORARY 테이블스페이스, PostgreSQL의 temp_tablespaces, InnoDB의 innodb_temp_data_file_path가 같은 자리를 차지한다. CUBRID도 같은 두 목적을 두지만 한 가지가 더 있다. DBA가 addvoldb로 미리 잡아 둘 수 있는 영구 타입 / 임시 목적 볼륨이 그것이다. 정렬 자리를 미리 떼어 두어 임시 볼륨이 런타임에 알 수 없게 자라는 일을 막아 준다.
두 단계 할당 — 캐시에 먼저 자리 잡고 나서 비트맵을 적는다
섹션 제목: “두 단계 할당 — 캐시에 먼저 자리 잡고 나서 비트맵을 적는다”순박하게 짠 할당 루프는 볼륨마다의 비트맵을 다 훑어 빈 익스텐트를 찾고, 비트를 켜고, 익스텐트를 돌려준다. 동시성이 올라가면 세 가지가 어그러진다. 첫째, 모든 비트맵을 훑는 비용이 크다. 둘째, 두 트랜잭션이 같은 비트를 두고 다툰다. 셋째, 비트맵이 모자라 볼륨 확장으로 흘러 들어가면 수백 ms가 걸리는 일이 락을 잡은 채 도는 셈이라 다른 트랜잭션이 줄줄이 멎는다. 정석은 인메모리 가용 자리 캐시를 볼륨마다 두고, 비트맵을 건드리기 전에 그 캐시 위에서 먼저 자리를 잡는 것이다. 캐시는 스칼라 필드만 만지므로 임계 구역이 짧다. CUBRID는 이 캐시를 Disk Cache라 부르고 두 단계 프로토콜을 또렷이 갈라 둔다. disk_reserve_from_cache(Step 1)가 어느 볼륨에서 몇 개의 섹터를 가져올지 정하고, disk_reserve_sectors_in_volume(Step 2)이 섹터 표를 훑으며 비트를 켠다.
파일 안의 익스텐트 표 — Partial과 Full로 갈라 둔다
섹션 제목: “파일 안의 익스텐트 표 — Partial과 Full로 갈라 둔다”파일 안에서 매니저는 어느 섹터에 가용 페이지가 남아 있는가(다음 할당이 값싸다)와 어느 섹터는 가득 찼는가(들여다볼 까닭이 없다)를 갈라 두려고 한다. 엔진치고 그 갈라짐을 어떤 식으로든 안 두는 곳은 거의 없다. Oracle은 segment header 페이지에 따로 freelist 그룹을 두고, InnoDB는 XDES 익스텐트 디스크립터 페이지에 상태 비트를 적어 둔다. CUBRID는 파일 안에 확장형 표 둘을 두는 길을 고른다. 하나는 Partial Sectors Table로 항목이 (VSID, 페이지 비트맵)이고, 다른 하나는 Full Sectors Table로 항목은 VSID 하나뿐이다. 할당은 늘 Partial의 머리에서 일어난다. 섹터가 가득 차는 순간 항목이 Full로 옮겨가고, 페이지가 풀리면 다시 Partial로 돌아온다.
Numerable 파일 — 할당 순서로 닿는다
섹션 제목: “Numerable 파일 — 할당 순서로 닿는다”어떤 워크로드는 페이지를 할당 순서로 받기를 바란다. 디스크 위 순서가 아니다. 외부 정렬은 정렬 결과의 i번째 페이지가 i번째 쓰기로 나가야 하지, 그 페이지가 물리적으로 어느 섹터에 들어 있는지는 따지지 않는다. 엔진들은 그런 닿음을 위해 파일 위에 색인을 한 줄 더 둔다. CUBRID는 파일 플래그(FILE_FLAG_NUMERABLE)로 그것을 드러내고, 세 번째 표 User Page Table을 보탠다(항목은 할당 순서대로 적힌 VPID 하나하나다).
볼륨 확장 = nested top action
섹션 제목: “볼륨 확장 = nested top action”디스크 위에서 볼륨을 키우는 일은 여러 단계를 거친다. 새 헤더와 비트맵 자리를 적고, OS 파일을 늘리고, 디스크 캐시를 갱신하고, 볼륨 정보 파일을 갱신한다. 도중에 크래시가 나면 자칫 죽은 바이트가 남거나 캐시가 어그러진다. 교과서의 답(Liskov & Scheifler 1982; Mohan et al. 1992)은 nested top action이다. 바깥 트랜잭션의 커밋이나 롤백과 무관하게 따로 커밋되는 안쪽 트랜잭션이라는 뜻이다. 바깥이 롤백되어도 확장은 살아남는다. CUBRID에서는 disk_volume_expand와 disk_add_volume이 모두 log_sysop_start / log_sysop_attach_to_outer 시스템 op으로 둘러싸여 돈다.
이론과 CUBRID 사이의 이름 짝
섹션 제목: “이론과 CUBRID 사이의 이름 짝”위에서 짚은 교과서 개념을 CUBRID의 실제 이름과 짝지으면 다음과 같다. 다음 절은 표의 한 줄씩으로 천천히 들어가는 모양으로 짜여 있다.
| 이론 | CUBRID 이름 |
|---|---|
| 엔진 페이지를 담는 OS 파일 | 볼륨 — OS 파일마다 0번 페이지에 DISK_VOLUME_HEADER |
| 볼륨마다의 익스텐트 비트맵 | Sector Allocation Table(STAB), 섹터 한 개당 비트 한 개 |
| 페이지보다 큰 할당 단위 | 섹터 — 기본 64페이지 × 16 KB = 1 MB(DISK_SECTOR_NPAGES) |
| 페이지 식별자 | VPID = (volid, pageid). 섹터 식별자는 VSID = (volid, sectid) |
| 릴레이션 단위 논리적 파일 | VFID = (volid, fileid). fileid가 파일 헤더 페이지의 page id |
| 영구와 임시 목적의 갈라짐 | DB_VOLPURPOSE ∈ {DB_PERMANENT_DATA_PURPOSE, DB_TEMPORARY_DATA_PURPOSE} |
| 영구와 임시 타입의 갈라짐 | DB_VOLTYPE ∈ {DB_PERMANENT_VOLTYPE, DB_TEMPORARY_VOLTYPE} |
| 인메모리 가용 자리 캐시 | DISK_CACHE(disk_Cache 전역) |
| 목적별 익스텐트 모음 | DISK_PERM_PURPOSE_INFO / DISK_TEMP_PURPOSE_INFO |
| 두 단계 할당 프로토콜 | disk_reserve_sectors → disk_reserve_from_cache + disk_reserve_sectors_in_volume |
| 파일 안의 Partial / Full 익스텐트 표 | Partial Sectors Table + Full Sectors Table(file_header 안쪽) |
| 정렬 닿음을 위한 할당 순서 색인 | User Page Table + FILE_FLAG_NUMERABLE |
| 페이지 경계를 넘는 일반화 리스트 | FILE_EXTENSIBLE_DATA(파일의 세 표와 File Tracker가 모두 함께 쓴다) |
| DB 단위 파일 디렉터리 | File Tracker — 자체도 영구 파일이며 boot_Db_parm->trk_vfid로 자리 잡음 |
| Nested top action으로 짠 볼륨 확장 | disk_extend → disk_volume_expand / disk_add_volume(log_sysop_start 안쪽) |
| 트랜잭션 단위 임시 파일 다시 쓰기 캐시 | FILE_TEMPCACHE. cached / free 두 리스트 |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID는 위 어휘들을 두 매니저로 갈라 짠다. 디스크 매니저가 볼륨과 섹터를 들고, 파일 매니저가 파일과 페이지를 들고 있다. 둘 사이의 경계는 정확히 disk_reserve_sectors다. 짚어 둘 만한 설계 결정은 다음과 같다. 첫째, 디스크와 파일을 잇는 단 하나의 단위가 64페이지 섹터다. 파일 매니저는 결코 V 볼륨에서 페이지 한 장을 청하지 않는다. P 목적의 섹터 N개를 어디서든 좋으니 달라고 청한다. 둘째, 두 단계 예약 프로토콜이 가벼운 부분(섹터 카운터 만지기)을 캐시 뮤텍스로 줄지어 처리하고, 무거운 부분(비트맵 갱신)은 그 뮤텍스 바깥으로 빼낸다. 셋째, 캐시 안에서 영구와 임시 목적이 또렷이 갈려 있어 정렬 작업과 테이블 쓰기가 같은 카운터를 두고 다투지 않는다. 넷째, 세 가지 파일 표(Partial / Full / User Page) 짜임이 흔한 경우의 다음 할당 자리를 O(1)에 답한다. 다섯째, 볼륨 확장이 nested top action이라 도중에 도는 트랜잭션의 롤백이 데이터베이스의 자라남 자체를 무효로 만들지 않는다.
flowchart TB
classDef vol fill:#e0e7ff,stroke:#3730a3,color:#111
classDef sect fill:#fde68a,stroke:#b45309,color:#111
classDef pg fill:#bbf7d0,stroke:#166534,color:#111
classDef file fill:#fecaca,stroke:#991b1b,color:#111
subgraph PHYS["물리 계층 (디스크 매니저)"]
direction TB
VOL["볼륨 = OS 파일 한 개"]:::vol
SEC["섹터 = 이어진 페이지 64장<br/>VSID = (volid, sectid)"]:::sect
PG["페이지 = 16 KB<br/>VPID = (volid, pageid)"]:::pg
VOL -->|"포함"| SEC
SEC -->|"64장 포함"| PG
end
subgraph LOG["논리 시점 (파일 매니저)"]
FL["파일 = 섹터의 모음<br/>VFID = (volid, fileid)<br/>(여러 볼륨에 걸칠 수 있음)"]:::file
end
FL -.예약.-> SEC
Figure 1 — 네 단의 계층이다. 볼륨은 OS 파일 한 개로, 기본 16 KB의 고정 크기 페이지로 나뉘어 있다. 디스크 매니저는 그 가운데 64장을 묶어 한 섹터로 잡는다. 파일은 섹터들의 논리적 모음으로, 그 섹터들은 여러 볼륨에서 가져온 것일 수 있다. 인덱스 항목과 버퍼 풀은 페이지를 VPID = (volid, pageid)로, 디스크 매니저 비트맵은 섹터를 VSID = (volid, sectid)로, 파일 매니저는 파일을 VFID = (volid, fileid)로 가리킨다. 파일 매니저는 자기가 받아 둔 섹터 안쪽 페이지를 풀어 주고, 디스크 매니저는 자기가 모양 잡은 볼륨 안쪽 섹터를 풀어 준다. (출처: DFM1 deck, slide 2.)
페이지 할당 한 건의 끝까지 흐름
섹션 제목: “페이지 할당 한 건의 끝까지 흐름”sequenceDiagram
participant C as 호출자 (heap, btree, …)
participant FM as file_alloc
participant FH as 파일 헤더 페이지
participant DM as disk_reserve_sectors
participant DC as Disk Cache
participant ST as Sector Table (디스크)
participant FIO as fileio (OS)
C->>FM: file_alloc(vfid, f_init)
FM->>FH: 파일 헤더 fix, n_page_free 확인
alt n_page_free > 0
FM->>FH: Partial Sectors Table 첫 항목의 비트 1개 set
FH-->>FM: 할당된 페이지의 VPID
else 섹터가 더 필요한 경우
FM->>DM: disk_reserve_sectors(purpose, N)
DM->>DC: reserve mutex 획득, vols[] 순회
alt 캐시에 충분한 섹터가 있음
DC-->>DM: 볼륨별 분배표
else 캐시 부족
DM->>DC: extend mutex 획득
DC->>FIO: disk_volume_expand / disk_add_volume
FIO-->>DC: 새 섹터 생성
DC-->>DM: 볼륨별 분배표
end
DM->>ST: disk_reserve_sectors_in_volume — STAB 비트 set
ST-->>DM: 받아 둔 VSID 목록
DM-->>FM: VSID 배열
FM->>FH: Partial Sectors Table에 보태고 페이지 할당
FH-->>FM: 할당된 페이지의 VPID
end
FM->>C: 페이지 포인터 + VPID — f_init이 페이지를 초기화
다이어그램의 박스 하나하나는 아래 소절에서 차례로 풀어 본다.
볼륨 모양 — 헤더 페이지 + 섹터 할당 표
섹션 제목: “볼륨 모양 — 헤더 페이지 + 섹터 할당 표”볼륨은 OS 파일 한 개다. 0번 페이지가 볼륨 헤더이고, 1번부터 stab_npages 페이지까지가 섹터 할당 표(STAB)다. STAB는 섹터마다 비트 한 개로 예약 여부를 적어 둔 평평한 비트맵이다. 그 뒤의 페이지들이 섹터로 묶여 디스크 매니저가 풀어 줄 자리가 된다.

Figure 2 — 볼륨은 첫 섹터를 시스템 페이지 자리로 떼어 두는 OS 파일이다. 0번 페이지의 볼륨 헤더, 그리고 stab_first_page부터 시작하는 이어진 STAB 페이지들이 그 자리다. 그 다음 섹터부터가 디스크 매니저가 disk_reserve_sectors로 풀어 줄 일반 섹터다. STAB 페이지 수는 모양 잡을 때 nsect_max(볼륨이 자랄 수 있는 한도)에 맞춰 잡혀 있다. 그래서 갓 모양 잡은 볼륨이 헤더 배치를 다시 짜지 않고도 자랄 수 있다. (출처: DFM2 deck, slide 1.)
헤더는 짜임에 관한 사실(페이지 크기, 섹터 크기, 볼륨 ID, nsect_total, nsect_max)과 복구에 필요한 사실(chkpt_lsa — 마지막 체크포인트가 이 볼륨을 들여다본 LSN)을 모두 담는다.
// DISK_VOLUME_HEADER (condensed) — src/storage/disk_manager.cstruct disk_volume_header{ char magic[CUBRID_MAGIC_MAX_LENGTH]; /* "magic" + name for file(1) */ INT16 iopagesize; INT16 volid; INT8 db_charset; DB_VOLPURPOSE purpose; /* permanent or temporary purpose */ DB_VOLTYPE type; /* permanent or temporary type */ DKNPAGES sect_npgs; /* pages per sector (64) */ DKNSECTS nsect_total; /* current sectors */ DKNSECTS nsect_max; /* max growth, set at format */ SECTID hint_allocsect; /* where to start the next bitmap walk */ DKNPAGES stab_npages; /* STAB size in pages */ PAGEID stab_first_page; PAGEID sys_lastpage; /* ... */ LOG_LSA chkpt_lsa; /* recovery anchor */ HFID boot_hfid; /* boot heap (system params) */ /* ... */ INT16 next_volid; /* link to next volume */ /* variable-length region for full-path strings */ char var_fields[1];};STAB는 비트맵이지만, 디스크 매니저는 비트를 한 개씩 읽지 않는다. 비트맵을 64비트 워드 단위인 유닛(unit)으로 끊고, disk_stab_iterate_units라는 콜백 모양의 함수로 훑는다. 그 덕에 한 유닛에서 가용 섹터 수를 묻는 일이 bit64_count_zeros 한 번으로 끝나고, 비트 켜기도 메모리를 여덟 번 건드릴 일 없이 레지스터 한 번이면 된다. 비트맵을 함수로 다루는 이 무늬는 파일 매니저 쪽에서도 file_extdata_apply_funcs라는 이름으로 다시 모습을 보인다.
// DISK_STAB_CURSOR (condensed) — src/storage/disk_manager.ctypedef UINT64 DISK_STAB_UNIT;struct disk_stab_cursor{ const DISK_VOLUME_HEADER *volheader; PAGEID pageid; /* page in the STAB */ int offset_to_unit; /* unit offset in page */ int offset_to_bit; /* bit offset in unit */ SECTID sectid; PAGE_PTR page; /* fixed STAB page */ DISK_STAB_UNIT *unit; /* pointer to current unit */};볼륨의 타입과 목적
섹션 제목: “볼륨의 타입과 목적”볼륨에 매달리는 속성은 둘이다.
- 타입(
DB_VOLTYPE) — 볼륨 자체가 영구냐 임시냐. 영구 타입은 서버 재시작 뒤에도 살아남고, 임시 타입은 시작 시점에 모두 거두어진다. - 목적(
DB_VOLPURPOSE) — 그 안에 들어 있는 데이터가 영구냐 임시냐. 영구 목적은 테이블, 인덱스, 시스템 데이터를 받고, 임시 목적은 정렬 결과, 해시 파티션, 질의 중간 결과를 받는다.
네 가지 짝 가운데 셋이 실제로 자리한다.
| 타입 \ 목적 | DB_PERMANENT_DATA_PURPOSE | DB_TEMPORARY_DATA_PURPOSE |
|---|---|---|
DB_PERMANENT_VOLTYPE | 일반 테이블/인덱스 볼륨 | DBA가 addvoldb --purpose temp로 미리 잡아 둔 볼륨. 재시작 뒤에도 살아남지만 임시 데이터를 받아 준다 |
DB_TEMPORARY_VOLTYPE | (자리 없음) | 정렬 spill 같은 일을 자동으로 받아 주는 볼륨. 재시작 시 거두어진다 |
세 번째 짝(영구 타입 + 임시 목적)이 흥미롭다. DBA가 임시 볼륨이 런타임에 모르게 자라는 일을 막고 싶을 때 고르는 길이다. 디스크를 통째로 미리 떼어 두고 정렬에만 쓰게 한다. 임시 목적 청이 들어오면 disk_reserve_from_cache는 먼저 이 영구-타입-임시-목적 볼륨에서 섹터를 꺼내 보려고 한다. DBA가 이미 잡아 둔 자원이라 더 들 비용이 0이기 때문이다. 모자랄 때만 임시 타입 볼륨을 키우거나 새로 만든다.
Disk Cache — 인메모리 가용 자리 모음
섹션 제목: “Disk Cache — 인메모리 가용 자리 모음”Disk Cache는 전역 구조체 한 개다. 두 가지를 들고 있다. 하나는 볼륨마다의 가용 섹터 수다. 다른 하나는 두 개의 모음 구조체로, 각 목적의 모든 볼륨을 합쳐 본 정보와 그 목적의 볼륨을 키우는 데 필요한 파라미터를 담는다.
// DISK_CACHE / DISK_EXTEND_INFO (condensed) — src/storage/disk_manager.cstruct disk_cache{ int nvols_perm; int nvols_temp; DISK_CACHE_VOLINFO vols[LOG_MAX_DBVOLID + 1]; /* per-volume free counts */
DISK_PERM_PURPOSE_INFO perm_purpose_info; /* rollup for permanent purpose */ DISK_TEMP_PURPOSE_INFO temp_purpose_info; /* rollup for temporary purpose */
pthread_mutex_t mutex_extend; /* serializes volume growth */};
struct disk_extend_info{ volatile DKNSECTS nsect_free; /* total free sectors of this purpose */ volatile DKNSECTS nsect_total; volatile DKNSECTS nsect_max; /* growth ceiling across all volumes */ volatile DKNSECTS nsect_intention; /* requested-but-unsatisfied counter */
pthread_mutex_t mutex_reserve; /* short critical section */
DKNSECTS nsect_vol_max; /* per-volume growth cap */ VOLID volid_extend; /* the only volume that may auto-grow */ DB_VOLTYPE voltype;};DISK_TEMP_PURPOSE_INFO는 영구-타입-임시-목적 볼륨을 따로 좇기 위한 카운터 둘(nsect_perm_total, nsect_perm_free)을 더 들고 있다.
핫 패스에서 인메모리 뮤텍스가 필요한 자료 구조는 사실상 Disk Cache 하나뿐이다. 이 뮤텍스를 짧게 잡고 빠르게 푸는 것이 두 단계 프로토콜의 핵심이다. Step 1은 mutex_reserve 안에서 스칼라 카운터만 만지고 빠져나온다. Step 2는 그 뮤텍스를 잡지 않고 도는데, 디스크 STAB 페이지의 페이지 래치만으로도 동시성이 지켜진다. Step 1이 이미 이 호출 자리에는 이 섹터를 주기로 했다고 카운터를 깎아 두었기 때문이다. Step 2는 그 약속을 비트맵에 옮기기만 하면 된다.
두 단계 섹터 예약
섹션 제목: “두 단계 섹터 예약”// disk_reserve_sectors (sketch) — src/storage/disk_manager.cintdisk_reserve_sectors (THREAD_ENTRY *thread_p, DB_VOLPURPOSE purpose, VOLID volid_hint, int n_sectors, VSID *reserved_sectors){ DISK_RESERVE_CONTEXT context; /* ... condensed: arg checking, sysop start ... */
/* init context: the running ledger of "how many sectors I still need" */ context.nsect_total = n_sectors; context.n_cache_reserve_remaining = n_sectors; context.vsidp = reserved_sectors; /* OUT: sorted list of VSIDs */ context.n_cache_vol_reserve = 0; context.purpose = purpose;
/* STEP 1: pre-reserve in cache (may extend volumes) */ error_code = disk_reserve_from_cache (thread_p, &context, &did_extend); if (error_code != NO_ERROR) goto error;
/* STEP 2: commit to bitmap, one volume at a time */ for (iter = 0; iter < context.n_cache_vol_reserve; iter++) { error_code = disk_reserve_sectors_in_volume (thread_p, iter, &context); if (error_code != NO_ERROR) goto error; } /* ... condensed: sysop attach to outer, return ... */}DISK_RESERVE_CONTEXT가 Step 1과 Step 2 사이의 장부 노릇을 한다. Step 1이 채워 두는 cache_vol_reserve[]는 어느 볼륨에서 몇 개의 섹터를 가져갈지를 적어 두고, Step 2가 차례로 그것을 비워 간다. 남은 섹터 수 카운터는 Step 1이 끝나는 그 자리에서 정확히 0이 된다. Step 2에서 볼륨을 도는 일은 서로 묶이지 않은 채 굴러간다. 한 볼륨의 디스크 STAB만 들여다보고, Step 1이 약속한 만큼의 비트만 켠다.
까다로운 자리는 Step 2 안쪽에서 일어나는 실패다. 캐시 카운터는 이미 깎여 있기 때문이다. 롤백은 disk_unreserve_ordered_sectors_without_csect가 비트맵과 캐시를 한 모양으로 되돌리는 식이다.
볼륨 확장 — nested top action
섹션 제목: “볼륨 확장 — nested top action”Step 1이 캐시가 모자라다는 사실을 알아채면(임시 목적 청이라면 영구-타입-임시-목적 볼륨까지 들여다본 뒤에도 모자라면) 확장 뮤텍스를 잡는다. 그 사이에 누가 이미 키워 두지 않았는지 가용 카운터를 한 번 더 본 뒤 disk_extend를 부른다.
flowchart LR
classDef perm fill:#dbeafe,stroke:#1e40af,color:#111
classDef temp fill:#fee2e2,stroke:#991b1b,color:#111
classDef noext fill:#f3f4f6,stroke:#374151,color:#374151,stroke-dasharray:4 3
classDef ext fill:#dcfce7,stroke:#166534,color:#111
PR["영구 목적 요청"]:::perm
TR["임시 목적 요청"]:::temp
subgraph PERM_INFO["영구 extend-info 모음"]
PP["영구-타입<br/>영구-목적 볼륨"]:::ext
end
subgraph TEMP_INFO["임시 extend-info 모음"]
direction TB
PT["영구-타입 임시-목적<br/>(DBA 미리 잡음, 자동 확장 없음)"]:::noext
TT["임시-타입<br/>임시-목적 볼륨"]:::ext
end
PR -->|"키우기 / 더하기"| PP
TR -->|"1. 먼저 확인"| PT
PT -->|"2. 떨어짐"| TT
TR -.->|"3. 키우기 / 더하기"| TT
Figure 3 — 확장 길이 목적에 따라 갈린다. 디스크 캐시는 목적마다 두 개의 extend-info 모음을 들고 있다. 영구 목적 청이 모자라면 영구-타입-영구-목적 볼륨을 키우거나 더한다. 임시 목적 청은 먼저 영구-타입-임시-목적 볼륨(DBA가 미리 잡아 둔 것)을 들여다보고, 그래도 모자라면 임시-타입-임시-목적 모음으로 떨어져 임시 타입 볼륨을 키우거나 더한다. 영구-타입-임시-목적 줄은 자동 확장 대상이 아니다. 사용자가 잡아 둔 만큼만 쓰고 더 늘어나지 않는다. (출처: DFM4 deck, slide 1.)
disk_extend는 두 가지 정책을 차례로 적용한다.
- 마지막으로 더해진 볼륨을
nsect_vol_max까지 키운다(disk_volume_expand). 한 시점에 한 목적에서 자랄 수 있는 볼륨은 한 개뿐이며, 그 ID가 캐시 필드volid_extend에 들어 있다. 더하기보다 키우기를 먼저 시도하는 까닭은 OS 파일 수가 늘어나는 것을 줄이기 위함이다. - 새 볼륨을 더한다(
disk_add_volume). 마지막 볼륨이 천장에 닿은 뒤에야 도는 길이다.disk_format으로 새 OS 파일에 헤더 + STAB + 첫 페이지를 적고,boot_Db_parm,_vinf레지스트리, 캐시에 새 볼륨을 끼워 넣는다.
두 길은 모두 log_sysop_start / log_sysop_attach_to_outer 안에서 돈다. 그래서 바깥 트랜잭션의 커밋이나 롤백과 무관하게 따로 커밋되는 nested top action이 된다. 그렇게 하지 않으면 어떤 트랜잭션이 갓 자란 볼륨의 섹터를 한 번이라도 건드린 순간, 자라게 한 트랜잭션이 롤백될 때 그 섹터를 함께 쓴 다른 트랜잭션도 모두 롤백되어야 한다. nested top action이 이 의존을 끊어 준다.
// disk_extend (condensed) — src/storage/disk_manager.cstatic intdisk_extend (THREAD_ENTRY *thread_p, DISK_EXTEND_INFO *extend_info, DISK_RESERVE_CONTEXT *reserve_context){ /* what is the desired remaining free after expand? */ target_free = MAX ((DKNSECTS) (total * 0.01), DISK_MIN_VOLUME_SECTS); /* expand at least enough to cover the unsatisfied intention */ nsect_extend = MAX (target_free - free, 0) + intention; if (nsect_extend <= 0) return NO_ERROR;
if (total < max) { /* first expand last volume up to its capacity */ to_expand = MIN (nsect_extend, max - total); log_sysop_start (thread_p); error_code = disk_volume_expand (thread_p, extend_info->volid_extend, voltype, to_expand, &nsect_free_new); /* ... condensed: pre-reserve from the new sectors, sysop attach ... */ }
/* if still short, add new volumes */ while (nsect_extend > 0) { log_sysop_start (thread_p); error_code = disk_add_volume (thread_p, &volext, &volid_new, &nsect_free_new); /* ... condensed: pre-reserve, sysop attach ... */ } return NO_ERROR;}키우기 → 더하기의 순서는 의도한 정책이다. 데이터를 적은 수의 OS 파일에 모아 두면 재시작 시점의 open(2) 트래픽이 줄고, 백업 도구가 들여다봐야 할 헤더 수도 적어진다.
섹터에서 파일로 — file_create
섹션 제목: “섹터에서 파일로 — file_create”disk_reserve_sectors가 줄세운 N개의 VSID를 돌려준 뒤부터는 파일 매니저의 일이다. file_create는 다음 다섯 가지를 한다.
- 데이터 + 최악의 경우의 파일 표를 합쳐 섹터 수를 짐작하고
disk_reserve_sectors를 부른다. - 받아 둔 섹터 가운데 한 자리의 첫 페이지를 파일 헤더 페이지로 잡고, 그 VPID를 파일의
VFID로 둔다. - 파일 헤더(
FILE_HEADER)를 첫 모양으로 잡는다. 파일 타입, 디스크립터, tablespace 정책, 페이지/섹터 카운터가 들어간다. - 파일 헤더 페이지 안에 세 가지 파일 표(Partial / Full / 그리고 Numerable이라면 User Page)를
offset_to_partial_ftab/offset_to_full_ftab/offset_to_user_page_ftab자리부터 적는다. 한 표가 헤더 페이지를 넘어서면 따로 잡힌 FTAB 페이지로FILE_EXTENSIBLE_DATA링크를 따라 이어진다. - 영구 파일이라면 File Tracker에 등록한다. 그래야 다른 모듈이 정해진 타입의 파일을 줄세워 볼 수 있다.
// FILE_HEADER (condensed) — src/storage/file_manager.cstruct file_header{ INT64 time_creation; VFID self; /* (volid, fileid = header page id) */ FILE_TABLESPACE tablespace; /* growth policy */ FILE_DESCRIPTORS descriptor; /* per-type descriptor (heap, btree, …) */
int n_page_total, n_page_user, n_page_ftab, n_page_free; int n_page_mark_delete;
int n_sector_total, n_sector_partial, n_sector_full, n_sector_empty;
FILE_TYPE type; /* FILE_HEAP / FILE_BTREE / FILE_TEMP / … */ INT32 file_flags; /* FILE_FLAG_NUMERABLE | _TEMPORARY | … */
VOLID volid_last_expand; INT16 offset_to_partial_ftab; INT16 offset_to_full_ftab; INT16 offset_to_user_page_ftab;
VPID vpid_sticky_first; /* survives dealloc — file-type-specific root */
/* temporary-file optimisation: cursor into Partial Sectors Table */ VPID vpid_last_temp_alloc; int offset_to_last_temp_alloc;
/* numerable-file optimisations */ VPID vpid_last_user_page_ftab; VPID vpid_find_nth_last; int first_index_find_nth_last; /* ... reserved fields ... */};파일 짜임새 — 비트맵 위에 세 표
섹션 제목: “파일 짜임새 — 비트맵 위에 세 표”파일의 메타데이터 영역은 둘로 나뉜다. 시스템 페이지(파일 헤더 페이지와 FTAB 오버플로 페이지들)와 유저 페이지(실제 heap 레코드, btree 노드, catalog 레코드 등)다.
flowchart TB
classDef fileA fill:#fecaca,stroke:#991b1b,color:#111
classDef fileB fill:#bfdbfe,stroke:#1e40af,color:#111
classDef fileC fill:#fde68a,stroke:#b45309,color:#111
classDef free fill:#ffffff,stroke:#9ca3af,color:#374151
classDef tab fill:#e9d5ff,stroke:#6b21a8,color:#111
subgraph V1["볼륨 V1"]
direction LR
V1P1["A"]:::fileA
V1P2["B"]:::fileB
V1P3["A"]:::fileA
V1P4["·"]:::free
V1P5["C"]:::fileC
V1P6["B"]:::fileB
end
subgraph V2["볼륨 V2"]
direction LR
V2P1["·"]:::free
V2P2["A"]:::fileA
V2P3["C"]:::fileC
V2P4["A"]:::fileA
V2P5["B"]:::fileB
V2P6["·"]:::free
end
subgraph FA["파일 A — 세 표 시점 (파일 헤더 페이지 안)"]
direction TB
TAB_PART["Partial Sectors Table"]:::tab
TAB_FULL["Full Sectors Table"]:::tab
TAB_USER["User Page Table (Numerable 파일에만)"]:::tab
end
FA -.색인.-> V1P1
FA -.색인.-> V1P3
FA -.색인.-> V2P2
FA -.색인.-> V2P4
Figure 4 — 볼륨과 파일이 만나는 모양이다. 볼륨(회색)마다는 이어진 페이지의 묶음이고, 디스크 매니저는 그 페이지들 위에 여러 파일의 예약(빨강·파랑 등)을 함께 새겨 둔다. 한 파일이 받아 둔 페이지들은 물리적으로 이어져 있지 않다. 디스크 매니저가 그때그때 찾아낸 섹터들로 흩어져 있을 뿐이다. 파일 매니저가 자기 섹터들에 대한 인메모리 표 모양을 깔아 그 흩어짐을 가린다. 호출자는 파일 F의 다음 페이지만 청하면 되고 볼륨 단위 배치는 들여다보지 않는다. (출처: DFM5 deck, slide 1.)
파일 안에서는 세 가지 표가 그 섹터들과 그 안의 페이지들을 색인한다.
- Partial Sectors Table — 항목은
FILE_PARTIAL_SECTOR = (VSID, FILE_ALLOC_BITMAP)이며, 비트맵은 64비트 워드 한 개다. 비트 한 개당 그 섹터 안의 페이지 한 장. 섹터 안에 비어 있는 페이지가 한 장이라도 남아 있으면 이 표에 머문다. - Full Sectors Table — 항목은 VSID 한 개. 비트맵이 모두 1이 되면 Partial에서 Full로 옮겨가고, 페이지 한 장이 풀리면 다시 Partial로 돌아온다.
- User Page Table(Numerable 파일에만) — 항목은 VPID 한 개. 할당 순서대로 적힌다.
file_numerable_find_nth가 외부 정렬용으로 받아 쓴다.
세 표 모두 FILE_EXTENSIBLE_DATA의 인스턴스다. 페이지 경계를 넘는 여러 페이지에 걸친, 항목 수가 달라지는 리스트를 다루는 일반화된 단방향 연결 리스트라고 보면 된다. 파일 트래커도 이 자료 구조를 그대로 다시 쓴다.
// FILE_EXTENSIBLE_DATA (condensed) — src/storage/file_manager.cstruct file_extensible_data{ VPID vpid_next; /* next page or VPID_NULL */ INT16 max_size; /* component capacity in bytes */ INT16 size_of_item; INT16 n_items;};항목들은 헤더 바로 뒤에 size_of_item 크기로 줄지어 있다. 연산 가운데 file_extdata_apply_funcs가 디스크 매니저의 disk_stab_iterate_units와 짝을 이룬다는 점이 눈에 들어온다. 컴포넌트 페이지마다 f_extdata를, 그 안의 항목마다 f_item을 적용한다. 줄세워진 표에서의 검색은 컴포넌트 안에서는 binary search, 컴포넌트 사이는 한 줄씩 따라가는 식이다.
파일 헤더 안 표의 크기는 파일 종류마다 다르다
섹션 제목: “파일 헤더 안 표의 크기는 파일 종류마다 다르다”세 표가 파일 헤더 페이지를 함께 쓰지만, 그 안에서 차지하는 비율은 (a) 파일이 영구냐 임시냐, (b) Numerable이냐 아니냐에 따라 달라진다.
| 파일 종류 | Partial Sectors | Full Sectors | User Page Table |
|---|---|---|---|
| 영구 + 비-Numerable | 1/2 | 1/2 | — |
| 영구 + Numerable | 1/3 | 1/3 | 1/3 |
| 임시 + 비-Numerable | 헤더 전체 | — | — |
| 임시 + Numerable | 1/2 | — | 1/2 |
Figure 5 — 파일 헤더 페이지의 네 가지 변종이다. 영구 + 비-Numerable은 헤더 페이지를 Partial과 Full 둘이 절반씩 나눈다(User Page Table은 없다). 영구 + Numerable은 셋이 약 삼분의 일씩 차지한다. 임시 + 비-Numerable은 Partial만 둔다. 임시 파일은 페이지를 절대 풀지 않으므로 섹터가 Partial을 떠날 일이 없다. 임시 + Numerable은 Partial과 User Page가 헤더 페이지를 나눠 쓴다. 그림의 숫자는 헤더 페이지 안에서 표마다 받은 상대적 바이트 비율이다. 한 표가 가득 차면 FILE_EXTENSIBLE_DATA.vpid_next를 따라 새 FTAB 페이지로 흘러 넘친다. (출처: DFM6 deck, slide 1.)
영구 파일의 페이지 할당
섹션 제목: “영구 파일의 페이지 할당”영구 파일의 할당 루프는 Partial Sectors Table의 머리만 본다.
// file_perm_alloc (high-level sketch) — src/storage/file_manager.c//// 1. fhead->n_page_free == 0 이면 file_perm_expand 호출:// disk_reserve_sectors 로 섹터를 더 받고, 그 결과를// Partial Sectors Table 끝에 보탠다.// 2. 헤더 안의 Partial Sectors Table 이 비어 있다면, 다음 FTAB// 페이지의 항목을 헤더 안으로 끌어 온다// (file_table_move_partial_sectors_to_header). 그래야 머리 항목이// 늘 헤더 페이지 안쪽에 자리한다.// 3. Partial Sectors Table 의 첫 항목은 정의상 "가용 페이지가 한 장// 이상 남은 섹터" 다. 그 비트맵의 비트 한 개를 켠다// (file_partsec_alloc). 비트의 오프셋이 곧 페이지 ID 다.// 4. 켠 결과 비트맵이 모두 1 이 되면 그 항목을 Full Sectors Table 로// 옮긴다. 다음 Partial 컴포넌트가 통째로 비었다면 그 페이지까지// 함께 풀어 준다.// 5. Numerable 파일이면 새 페이지의 VPID 를 User Page Table 끝에 보탠다.머리만 보면 되는 까닭을 보장하는 두 불변식이 이 설계의 핵심이다.
- 머리 섹터에는 늘 가용 페이지가 한 장 이상 있다 — 4단계가 보장한다. 가득 찬 그 자리에서 Partial을 떠나기 때문이다.
n_page_free > 0이면 헤더 안에 Partial 항목이 적어도 한 개 있다 — 2단계가 보장한다. 헤더 안 Partial이 비고 다음 페이지 Partial이 남아 있다면 그 항목을 헤더 안으로 끌어 온다.
임시 파일의 페이지 할당
섹션 제목: “임시 파일의 페이지 할당”임시 파일은 페이지를 풀지 않는다. 그 한 가지 사실에서 모든 단순화가 흘러나온다.
- Full 표가 없다. Partial만 있다.
- 할당이 차례차례 일어난다. 파일 헤더가
(vpid_last_temp_alloc, offset_to_last_temp_alloc)을 들고 있어 다음에 어느 Partial 항목에서 떼어 줄지를 짚어 둔다.file_temp_alloc한 번이 그 커서를 한 칸씩 옮긴다. - 그래서 할당당 비용이 O(1)이고, 로깅도 없다.
영구 파일이 짊어지는 Partial-Full 오감과 WAL 레코드 흐름이 임시 파일에는 통째로 빠져 있다.
페이지 풀기
섹션 제목: “페이지 풀기”영구 파일의 페이지 풀기는 영구 할당의 거꾸로다. 다만 두 가지가 다르다.
log_append_postpone으로 미룬다.file_dealloc자리에서 비트가 곧장 꺼지지 않는다. postpone 로그 레코드로 적혀 두었다가 트랜잭션이 커밋될 때 다시 돈다. 까닭은 섹터 예약 풀기와 같다. 풀려난 페이지가 그 트랜잭션이 커밋되기 전에 다른 트랜잭션에 의해 다시 잡히면 안 되기 때문이다.- 버퍼 풀에 슬롯 회전 힌트. 풀기 뒤에
pgbuf_dealloc_page가 페이지 타입을PAGE_UNKNOWN으로 갈고 BCB를 LRU 3 zone으로 옮긴다. 그래야 버퍼 매니저가 그 슬롯을 빠르게 다시 쓸 수 있다. 자세한 내용은cubrid-page-buffer-manager.md§Zones — five-state classification을 보라.
파일 부수기와 File Tracker
섹션 제목: “파일 부수기와 File Tracker”file_destroy는 Partial / Full 표를 모두 훑어 파일이 들고 있던 모든 VSID를 모은다. 그리고 모든 FTAB와 유저 페이지를 버퍼 풀에서 거두어 낸 뒤, 마지막으로 disk_unreserve_ordered_sectors로 섹터를 디스크 매니저에 돌려준다. 페이지 풀기와 마찬가지로 파일 부수기도 log_append_postpone으로 커밋 시점까지 미뤄진다.
File Tracker는 데이터베이스마다 한 개씩 자리한 영구 파일이며, 본문은 한 덩어리의 FILE_EXTENSIBLE_DATA다. 항목은 FILE_TRACK_ITEM = (volid, fileid, type, metadata)다. 영구 파일을 만드는 일마다 등록이 일어나고, 영구 파일을 부수는 일마다 등록 해제가 일어난다. 트래커 자체의 자리는 boot_Db_parm->trk_vfid에 적혀 있고, 서버 재시작 시점에 file_Tracker_vpid라는 전역에 적재되어 O(1)에 닿을 수 있다. 외부에서는 file_tracker_map / file_tracker_interruptable_iterate로 정해진 타입의 파일을 줄세워 본다. heap 파일 다시 쓰기, 백업, vacuum의 dropped-files 훑기가 모두 그 자리에서 출발한다.
임시 파일 캐시
섹션 제목: “임시 파일 캐시”임시 파일은 회전이 빠르다. 모든 질의의 모든 정렬이 임시 파일 한 개를 만들었다가 버린다. 그때마다 새로 만드는 것은 순박한 정책이고, CUBRID의 FILE_TEMPCACHE는 갓 풀려난 임시 파일들을 free 리스트 모양으로 보관한다. 헤더 모양이 다르므로 numerable과 비-numerable이 따로 묶여 관리된다.
상태는 다음과 같다.
- Create — 캐시 리스트를 먼저 본다. 들어맞으면 그 파일을 그대로 쓴다. 빗나가면 새로 만들어 지금 트랜잭션의 tempcache 항목 리스트에 잇는다.
- Retire(트랜잭션 끝) —
log_commit_local/log_abort_local자리에서 트랜잭션마다의 리스트를 비운다. 파일마다 유저 페이지를 비운 뒤 캐시 리스트로 들어가거나(캐시가 가득 차 있다면 그대로 부숴 버린다). - Preserve — 어떤 임시 파일이 트랜잭션보다 더 오래 살기를 바라면
file_temp_preserve를 부른다. 트랜잭션 리스트에서 빠져나와 전역 질의 매니저가 들고 있는 리스트로 옮겨간다. - Retire(preserved) — 결과 셋을 다 쓰고 나면 질의 매니저가
file_temp_retire_preserved로 그 임시 파일을 부순다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”닻은 줄 번호가 아니라 심볼 이름이다. CUBRID 소스는 자주 바뀐다. 함수 이름이나 struct/enum 태그는 어지간한 리팩터에도 살아남는다. 지금 자리는
git grep -n '<symbol>' src/storage/로 짚는다. 이 절 끝의 위치 힌트 표는 본 문서의updated:시점에 본 줄 번호이며, 빠른 길잡이일 뿐이다.
타입 정의 — 디스크 측 (src/storage/disk_manager.{h,c})
섹션 제목: “타입 정의 — 디스크 측 (src/storage/disk_manager.{h,c})”struct disk_volume_header(disk_manager.c) — 디스크 위 볼륨 헤더. magic, ID들, 섹터 카운트, STAB 오프셋, 체크포인트 LSA.enum DB_VOLPURPOSE(storage_common.h) —PERMANENT,TEMPORARY,UNKNOWN.enum DB_VOLTYPE—PERMANENT_VOLTYPE,TEMPORARY_VOLTYPE.struct disk_cache(disk_manager.c) — 전역disk_Cache의 본체.struct disk_extend_info— 목적마다의 확장 카운터와 뮤텍스.struct disk_perm_info/struct disk_temp_info— 목적 태그가 매달린disk_extend_info래퍼. temp 변종은 영구-타입-임시-목적 볼륨 카운터(nsect_perm_total/nsect_perm_free)를 더 들고 있다.struct disk_cache_volinfo— 볼륨마다의(purpose, nsect_free).struct disk_reserve_context— Step 1과 Step 2 사이의 장부.struct disk_cache_vol_reserve— Step 2가 훑을(volid, nsect)줄.typedef DISK_STAB_UNIT—UINT64. 비트맵 만지기의 단위.struct disk_stab_cursor— 비트맵 훑기 손잡이.
타입 정의 — 파일 측 (src/storage/file_manager.{h,c})
섹션 제목: “타입 정의 — 파일 측 (src/storage/file_manager.{h,c})”enum FILE_TYPE(file_manager.h) — 14가지 파일 타입(FILE_TRACKER / FILE_HEAP / FILE_HEAP_REUSE_SLOTS / FILE_BTREE / FILE_BTREE_OVERFLOW_KEY / FILE_EXTENDIBLE_HASH / FILE_EXTENDIBLE_HASH_DIRECTORY / FILE_CATALOG / FILE_DROPPED_FILES / FILE_VACUUM_DATA / FILE_QUERY_AREA / FILE_TEMP / FILE_MULTIPAGE_OBJECT_HEAP / FILE_UNKNOWN_TYPE).struct file_header(file_manager.c) — 파일마다의 메타데이터.struct file_tablespace(file_manager.h) — 첫 크기와 자라남 정책(expand_ratio/expand_min_size/expand_max_size).union file_descriptors— 타입 태그가 붙은 파일별 디스크립터(heap, btree 등). ABI 호환을 위해 64바이트로 못 박혀 있다.struct file_partial_sector—(VSID, FILE_ALLOC_BITMAP). 64비트 비트맵.typedef FILE_ALLOC_BITMAP—UINT64.struct file_extensible_data— 페이지 경계를 넘는 일반화 리스트의 헤더.- 파일 플래그 —
FILE_FLAG_NUMERABLE,FILE_FLAG_TEMPORARY,FILE_FLAG_ENCRYPTED_AES,FILE_FLAG_ENCRYPTED_ARIA.
라이프사이클 (disk_manager.c / file_manager.c)
섹션 제목: “라이프사이클 (disk_manager.c / file_manager.c)”disk_manager_init/disk_manager_final— 모듈 켜기와 끄기. 디스크 위 볼륨 정보로부터disk_Cache를 다시 적재한다.disk_cache_init/disk_cache_final/disk_cache_load_all_volumes/disk_cache_load_volume— 캐시를 다시 짜는 길.file_manager_init/file_manager_final.disk_format_first_volume— 첫createdb길.disk_format— 새 볼륨에 헤더 + STAB 적기.disk_unformat— 볼륨의 OS 파일을 거두기(데이터베이스 삭제 길).
섹터 예약 (disk_manager.c)
섹션 제목: “섹터 예약 (disk_manager.c)”disk_reserve_sectors— 바깥 진입점. 시스템 op 안쪽에서 두 단계 프로토콜의 바깥 드라이버가 도는 자리.disk_reserve_from_cache— Step 1. 캐시를 들여다보고 모자라면disk_extend를 부른다.disk_reserve_from_cache_vols— 볼륨들을 훑으며 후보를 골라낸다.disk_reserve_from_cache_volume— 볼륨 한 개에 미리 예약을 시도(캐시 카운터를 깎고cache_vol_reserve[]에 적는다).disk_reserve_sectors_in_volume— Step 2. 디스크 STAB의 비트를 실제로 켜는 자리.disk_unreserve_ordered_sectors/disk_unreserve_ordered_sectors_without_csect— 풀기 길(file_destroy가 부른다).
볼륨 확장 (disk_manager.c)
섹션 제목: “볼륨 확장 (disk_manager.c)”disk_extend— 확장 라우터. 키우기 → 더하기.disk_volume_expand— 마지막 볼륨을nsect_max까지 키운다.disk_add_volume—disk_format으로 OS 파일을 새로 만들고 캐시에 잇는다.disk_add_volume_extension—addvoldb와 부팅 시점의 미리 잡기 길의 바깥 진입점.
STAB 훑기 (disk_manager.c)
섹션 제목: “STAB 훑기 (disk_manager.c)”disk_stab_iterate_units— 유닛 단위 map 함수. 부르는 쪽이DISK_STAB_UNIT_FUNC을 넘긴다.disk_stab_unit_reserve/disk_stab_unit_unreserve— 비트 켜기/끄기 프리미티브.disk_stab_count_free— 한 유닛의 0-비트 수(디스크 일관성 검사와disk_rv_volhead_extend_redo가 받아 쓴다).disk_stab_cursor_set_at_sectid/disk_stab_cursor_set_at_start/disk_stab_cursor_set_at_end— 커서 자리 잡기.
파일 만들기 / 부수기 / 좇기 (file_manager.c)
섹션 제목: “파일 만들기 / 부수기 / 좇기 (file_manager.c)”file_create— 일반 진입점.FILE_TYPE,tablespace, numerable 플래그를 받는다.file_create_heap/file_create_temp/file_create_temp_numerable/file_create_query_area/file_create_ehash/file_create_ehash_dir— 타입마다의 래퍼.file_destroy/file_postpone_destroy— 파일 부수기(커밋까지 미룬다).file_temp_retire/file_temp_retire_preserved/file_temp_preserve/file_temp_truncate— 임시 파일 라이프사이클.file_tracker_create/file_tracker_load/file_tracker_register/file_tracker_unregister/file_tracker_map/file_tracker_interruptable_iterate— 데이터베이스 단위 파일 디렉터리.
페이지 할당과 풀기 (file_manager.c)
섹션 제목: “페이지 할당과 풀기 (file_manager.c)”file_alloc— 바깥 진입점.FILE_IS_TEMPORARY로 갈라진다.file_perm_alloc— Partial Sectors Table 머리에서 영구 파일 페이지 할당.file_temp_alloc— 파일 헤더의 커서를 한 칸씩 옮기는 임시 파일 할당.file_perm_expand— 가용 페이지가 모자랄 때disk_reserve_sectors로 섹터를 더 받고 Partial 끝에 보태는 일.file_table_move_partial_sectors_to_header— 다음 페이지 Partial의 항목을 헤더 안쪽으로 끌어와 머리 항목 불변식을 지키는 일.file_partsec_alloc—FILE_PARTIAL_SECTOR비트맵에서 비트 한 개를 켜고 페이지 오프셋을 돌려준다.file_perm_dealloc— 비트를 다시 끄고, 필요하면 Full → Partial로 옮긴다.file_dealloc/file_rv_dealloc_on_postpone— 미뤄진 풀기 길.file_alloc_sticky_first_page/file_get_sticky_first_page— 모든 풀기 길에서 살아남는 파일 타입마다의 루트 페이지.
Numerable 파일 (file_manager.c)
섹션 제목: “Numerable 파일 (file_manager.c)”file_numerable_add_page— 할당 시 User Page Table에 보태기.file_numerable_find_nth— n번째 유저 페이지 찾기.(vpid_find_nth_last, first_index_find_nth_last)캐시를 받아 쓴다.file_numerable_truncate— User Page Table의 꼬리를 잘라 내기.
확장형 데이터 프리미티브 (file_manager.c)
섹션 제목: “확장형 데이터 프리미티브 (file_manager.c)”file_extdata_init/file_extdata_max_size/file_extdata_size/file_extdata_is_full/file_extdata_item_count/file_extdata_remaining_capacity— 인라인 접근자들.file_extdata_append/file_extdata_insert_at/file_extdata_remove_at/file_extdata_merge— 변경 연산.file_extdata_apply_funcs— 페이지·항목 단위 map.file_extdata_func_for_search_ordered/file_extdata_item_func_for_search— binary search 훅.
임시 파일 캐시 (file_manager.c)
섹션 제목: “임시 파일 캐시 (file_manager.c)”file_tempcache_get/file_tempcache_put— 캐시한 임시 파일을 받거나 돌려주는 일.file_tempcache_drop_tran_temp_files—log_commit_local/log_abort_local시점에 부른다.file_get_tran_num_temp_files— 디버그·모니터링.
복구 (disk_manager.c + file_manager.c)
섹션 제목: “복구 (disk_manager.c + file_manager.c)”disk_rv_redo_dboutside_newvol/disk_rv_undo_format/disk_rv_redo_format/disk_rv_redo_volume_expand/disk_rv_volhead_extend_redo/disk_rv_volhead_extend_undo— 볼륨 만들기와 키우기의 복구.disk_rv_reserve_sectors/disk_rv_unreserve_sectors— STAB 비트 복구.disk_rv_undoredo_link— 다중 볼륨 연결 복구.file_rv_destroy/file_rv_perm_expand_redo/file_rv_perm_expand_undo/file_rv_partsec_set/file_rv_partsec_clear/file_rv_extdata_set_next/file_rv_extdata_add/file_rv_extdata_remove/file_rv_extdata_merge/file_rv_fhead_alloc/file_rv_fhead_dealloc/file_rv_dealloc_on_undo/file_rv_dealloc_on_postpone/file_rv_user_page_mark_delete/file_rv_user_page_unmark_delete_logical/file_rv_user_page_unmark_delete_physical/file_rv_tracker_unregister_undo/file_rv_tracker_mark_heap_deleted/file_rv_tracker_mark_heap_deleted_compensate_or_run_postpone/file_rv_tracker_reuse_heap— 파일 측 복구.
위치 힌트 (이 개정 시점)
섹션 제목: “위치 힌트 (이 개정 시점)”이 줄 번호는 본 문서가 마지막으로 updated:된 시점에 본 값이다. 다른 줄에 닿았다면 위 심볼 이름이 정답이고, 지나가는 길에 표를 갱신해 주면 된다. disk_manager.c는 약 6,100줄, file_manager.c는 약 11,000줄이라, 심볼 단위 git grep이 권장 검색 길이다.
| 심볼 | 파일 | 줄 |
|---|---|---|
struct disk_volume_header | disk_manager.c | 75 |
struct disk_cache | disk_manager.c | 194 |
struct disk_extend_info | disk_manager.c | 162 |
struct disk_perm_info | disk_manager.c | 180 |
struct disk_temp_info | disk_manager.c | 186 |
struct disk_stab_cursor | disk_manager.c | 229 |
struct disk_reserve_context | disk_manager.c | 281 |
disk_format | disk_manager.c | 512 |
disk_extend | disk_manager.c | 1633 |
disk_volume_expand | disk_manager.c | 1904 |
disk_add_volume | disk_manager.c | 2117 |
disk_add_volume_extension | disk_manager.c | 2326 |
disk_cache_load_volume | disk_manager.c | 2567 |
disk_cache_init | disk_manager.c | 2627 |
disk_cache_load_all_volumes | disk_manager.c | 2714 |
disk_stab_iterate_units | disk_manager.c | 3665 |
disk_reserve_sectors_in_volume | disk_manager.c | 4066 |
disk_reserve_sectors | disk_manager.c | 4290 |
disk_reserve_from_cache | disk_manager.c | 4463 |
disk_reserve_from_cache_vols | disk_manager.c | 4612 |
disk_reserve_from_cache_volume | disk_manager.c | 4666 |
disk_unreserve_ordered_sectors | disk_manager.c | 4703 |
disk_format_first_volume | disk_manager.c | 5062 |
enum FILE_TYPE | file_manager.h | 38 |
struct file_tablespace | file_manager.h | 142 |
struct file_partial_sector | file_manager.h | 161 |
struct file_header | file_manager.c | 89 |
struct file_extensible_data | file_manager.c | 231 |
file_extdata_apply_funcs | file_manager.c | 1886 |
file_create | file_manager.c | 3311 |
file_destroy | file_manager.c | 4121 |
file_perm_expand | file_manager.c | 4644 |
file_table_move_partial_sectors_to_header | file_manager.c | 4772 |
file_perm_alloc | file_manager.c | 5166 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”각 항목은 지금 소스에 대한 사실이다. 1차 자료가 없어도 그대로 읽힐 수 있도록 진하게 박은 주장이 먼저 오고, 그 뒤에 검증 길과 (관련이 있다면) 옛 자취가 따라온다. Open questions는 큐레이터가 그어 둔 미해결 자리들이다. 알려진 버그가 아니라 다음 사람이 출발점으로 삼을 자리들이다.
검증된 사실
섹션 제목: “검증된 사실”- 섹터는 정확히 64페이지다.
DISK_SECTOR_NPAGES는 컴파일 타임 상수다. 기본 16 KB 페이지와 곱하면 1 MB 섹터가 나온다. 2026-05-01에DISK_SECTS_NPAGES와disk_reserve_sectors_in_volume안쪽 불변식으로 확인했다. 런타임 파라미터가 아니므로 빌드 시점에 바꾸려면 로그 포맷 호환성을 함께 봐야 한다. - STAB 유닛은 늘 64비트다.
DISK_STAB_UNIT은UINT64다. 주석에는 유닛 타입을 바꾸면 자동으로 처리되어야 한다라고 적혀 있지만, 받아 쓰는 쪽은bit64_count_zeros,disk_stab_unit_*같이 64비트로 못 박혀 있다. 2026-05-01에 본 그대로다. DFM2 deck도 같은 숨은 결합을 짚었고, 그대로 남아 있다. - 섹터 예약은 목적마다 한 개의 공유 뮤텍스를 쓰는 두 단계 프로토콜이다.
disk_reserve_from_cache가extend_info->mutex_reserve를 잡고disk_Cache->vols[]를 훑으면서nsect_free를 깎아 두고 풀어 준다. Step 2인disk_reserve_sectors_in_volume은 그 뮤텍스를 잡지 않고 도는데, 디스크 STAB 페이지의 페이지 래치만으로도 동시성이 지켜진다. 2026-05-01에disk_reserve_sectors본문을 읽어 확인했다. - 캐시와 STAB가 잠깐 어긋나는 구간이 있다. Step 1과 Step 2 사이의 짧은 동안 캐시는 그 섹터를 받은 것으로 보고, 비트맵은 여전히 빈 것으로 보인다. 안전한 까닭은 둘이다. (a) Step 1이 잡아 둔
cache_vol_reserve[]가 비트의 임자를 호출 자리에 묶어 두고, (b) 예약 순서가캐시 → 비트맵, 풀기 순서가비트맵 → 캐시로 늘 한 방향이다. 그래서 캐시는 가용 섹터 수를 적게 보고하지 많게 보고하지는 않는다. 2026-05-01에disk_reserve_sectors_in_volume과disk_unreserve_sectors_from_volume을 읽어 확인했다. DFM3 deck의 풀이 그대로 살아 있다. - 볼륨 확장은 nested top action이다.
disk_extend가disk_volume_expand와disk_add_volume을 모두log_sysop_start/log_sysop_attach_to_outer안에서 부른다. 2026-05-01 확인. - 자동 확장 대상 볼륨은 마지막 한 개뿐이다.
extend_info->volid_extend가 그 볼륨의 ID를 들고 있다. 새 볼륨은 그 볼륨이nsect_vol_max에 닿은 뒤에야 더해진다. 2026-05-01에disk_extend와disk_add_volume으로 확인했다. - 영구-타입-임시-목적 볼륨은 사용자만 만들 수 있다.
addvoldb --purpose temp가 그것을 만드는 단 하나의 길이다. 자동 확장 길(임시 목적의disk_extend)은 임시 타입 볼륨만 키우거나 더한다.disk_reserve_from_cache가 임시 목적 청을 받을 때nsect_perm_free > 0을 먼저 보고 그 다음에야 임시-타입 모음으로 떨어지는 코드를 읽어 확인했다. - 섹터 풀기는 영구 목적에서만 postpone된다.
disk_unreserve_ordered_sectors가 영구 목적이면log_append_postpone을 부르고, 임시 목적이면 곧장 돈다. 2026-05-01 확인. 까닭은 또렷하다. 커밋된 트랜잭션이 풀어 준 섹터를 그 트랜잭션이 커밋되기 전에 다른 트랜잭션이 가져가면 안 되기 때문이다. 임시 데이터는 어차피 로깅되지 않는다. disk_Cache의 페이지 표 크기는LOG_MAX_DBVOLID + 1이다.struct disk_cache의vols[]가LOG_MAX_DBVOLID로 잡혀 있다. 로그 매니저가 같은 상수로 볼륨 ID를 다루기 때문에, 이 값을 넘는 볼륨 수는 로그도 함께 깨뜨린다. 2026-05-01 확인.vpid_sticky_first는 유저 페이지 풀기 길에서 살아남는다.file_alloc_sticky_first_page가fhead->vpid_sticky_first를 박아 두고,file_dealloc/file_perm_dealloc이 그 VPID를 단락(short-circuit)시킨다. 2026-05-01 확인. 파일 타입마다 sticky 페이지를 자기만의 루트(heap 헤더, btree 루트 등)로 쓴다.file_temp_alloc은 헤더의 커서를 한 칸씩 옮길 뿐, 풀지 않는다. 2026-05-01에 임시 할당 길을 읽어 확인했다. DFM6 deck이 임시 파일에 Full Sectors Table이 없는 까닭으로 짚어 둔 불변식이 그대로 살아 있다.- File Tracker는 헤더가 따로 없는 영구 파일이다. 본문이 한 덩어리의
FILE_EXTENSIBLE_DATA다. 항목은FILE_TRACK_ITEM이며, 트래커 자체의 자리는 디스크에서는boot_Db_parm->trk_vfid, 메모리에서는 재시작 뒤file_Tracker_vpid에 들어간다. 2026-05-01에file_tracker_create와 부팅 길을 읽어 확인했다.
미해결 질문
섹션 제목: “미해결 질문”- 자동 볼륨 확장 데몬. 손으로 적은
disk_manager.md가disk_auto_volume_expansion_daemon과disk_manager_init이prm_get_integer_value(PRM_ID_BOSR_MAXTMP_PAGES)를 읽는 코드를 가리킨다. 그러나 2026-05-01 기준 소스에는 그 데몬이 자리 잡고 있지 않다.git grep이disk_auto_volume_expansion_daemon_init이나disk_auto_volume_expansion_daemon_destroy를 잡지 못한다. 어느 시점에 거두어진 셈이다. 추적 — 그 거두는 커밋을 좇고, 자동 확장 루프가 이제 전적으로 포그라운드 예약 길(특히nsect_intention모음)에 기대고 있는지 확인한다. disk_Temp_max_sects가 예약과 어떻게 맞물리는가.PRM_ID_BOSR_MAXTMP_PAGES로 첫 모양을 잡고 음수면SECTID_MAX로 잘리는 전역이다. 그런데 정확히 어디에서 예약을 막는가.disk_reserve_from_cache는 직접 들여다보지 않는 듯하다. 추적 —git grep disk_Temp_max_sects로 받아 쓰는 자리들을 좇는다.hint_allocsect의 실제 효용.volheader->hint_allocsect는disk_reserve_sectors_in_volume이 마지막으로 잡아 둔 VSID 다음 자리로 갱신된다. 다음 예약이 거기서 이어지길 바라는 의도다. 그러나 동시성이 높고 풀기도 잦은 워크로드에서는 0번부터 훑기와 별 차이가 없을 수 있다. 추적 — TPC-C 류와 정렬 위주 워크로드에서 힌트 적중률을 재 본다.DISK_VOLUME_HEADER/FILE_HEADER의reserved0..3. 두 구조체 모두 장래 확장을 위한INT324 워드를 두었지만, TDE / OOS 같은 새 기능이 일부를 이미 가져갔을 가능성이 높다. 추적 — git blame과 OOS 디자인 문서를 들여다보고 어느 reserved 비트가 살아 있는지 확인한다.FILE_FLAG_ENCRYPTED_AES/_ARIA와vpid_sticky_first의 관계. 본 문서는 암호화를 디스크/파일 계층에 투명한 것으로 다루었지만, 파일 플래그가 암호화 상태를 들고 있고 파일 매니저는file_get_tde_algorithm/file_apply_tde_algorithm을 드러낸다. 예약/할당 길은 영향이 없지만,file_perm_dealloc의 풀기 전에 0으로 채우기가 암호화 파일을 어떻게 도는지는 따로 봐 둘 만하다. 추적 —file_apply_tde_algorithm을 받아 쓰는 자리들과FILE_IS_TDE_ENCRYPTED갈래를 좇는다.file_temp_truncate가 실제로 무엇을 하는가. 헤더에 선언만 있고,disk_manager.md와 DFM6/DFM7 deck도 그것을 짚지 않는다. 추적 — 본문을 읽고 질의 매니저 안의 호출자를 찾는다.- 복구 도중의 동시 볼륨 모양 잡기. 복구 길은
disk_rv_redo_dboutside_newvol로 복구 시작 시점에는 없던 볼륨을 다시 만든다. 즉 OS를 건드리는 redo 핸들러가 있다는 뜻이다. 추적 —disk_format도중 부분 크래시(헤더만 적힘, STAB 일부만 적힘, 데이터 미기록)를 가정하고 redo의 멱등성을 확인한다.
CUBRID 너머 — 비교 설계와 연구 동향
섹션 제목: “CUBRID 너머 — 비교 설계와 연구 동향”분석이 아니라 길잡이다. 항목 하나하나가 후속 문서를 출발시킬 손잡이며, 깊이는 일부러 얕다.
- PostgreSQL relfilenode + FSM fork. PostgreSQL은 릴레이션 포크마다 OS 파일 한 개(
base/<dboid>/<relnode>)를 쓴다. 페이지 단위 가용 자리를 좇는 별도의 FSM 파일과, 가시성을 좇는 VM 파일을 릴레이션마다 따로 둔다. 익스텐트 계층이 없고, 할당은 페이지 단위로 storage manager(smgr_extend)를 거친다. CUBRID의 섹터 + Partial/Full 짜임은 PostgreSQL이_fsm,_vm, 본 포크에 흩어 둔 정보를 한 자리에 모아 둔 답이다. 비교 글은 절충(CUBRID: 메타데이터 적음, 락 다툼 큼; PostgreSQL: 파일 많음, 할당당 손맞춤 적음)을 정리해 둘 만하다. - Oracle tablespace / segment / extent / block. Oracle의 네 단 계층은 CUBRID의 목적 / 파일 / 섹터 / 페이지와 거의 한 줄로 짝이 맞지만 두 가지가 다르다. 첫째, Oracle의 segment header가 세그먼트 첫머리에 자리하면서 세션 부류마다의 freelist 그룹을 두어 할당 다툼을 흩어 둔다. 둘째, bigfile 테이블스페이스는 한 테이블스페이스가 곧 익스텐트들의 한 세그먼트인 모양이다. CUBRID의 단일 Partial Sectors Table 머리 정책과 견주면 워크로드별 우위를 정리하기 좋다.
- InnoDB tablespace / file space / extent / page. InnoDB의
XDES익스텐트 디스크립터(세그먼트 inode 페이지 안)는 페이지마다의 상태 비트(free / clean / dirty)를 들고 있고, 세그먼트는 inode 페이지로 자기 익스텐트들을 가리킨다. 세그먼트 inode / 파일 헤더 / 익스텐트 디스크립터의 갈라짐은 CUBRID의file_header/ Partial / Full 갈라짐과 매우 비슷하다. 추천 자료는 MySQL InnoDB Storage Engine 문서의 §InnoDB On-Disk Structures. - SQL Server filegroup / file / extent / page. SQL Server의 uniform extent(8페이지를 한 객체가 다 갖는다)와 mixed extent(8페이지를 작은 객체 여럿이 나눠 쓴다)는 작은 테이블 밀도와 큰 테이블 속도 사이의 절충이다. CUBRID에는 사실상 uniform extent에 해당하는 길만 있다. 작은 테이블이 수천 개인 워크로드에서 mixed extent를 어떻게 흉내 낼지가 흥미로운 사고 실험거리다.
- NVMe 친화적 할당. 디바이스 안쪽 블록이 16 KB 또는 32 KB인 매체 위에서는 Double Write Buffer(
cubrid-page-buffer-manager.md참고)가 막아 주던 torn-write 위험이 상당 부분 사라진다. Bittman et al., Don’t Stack Your Log On My Log(USENIX HotStorage 2014)와 LeanStore(Leis et al., ICDE 2018)가 엔진이 익스텐트 단위로 할당을 다뤄야 하는가를 다시 묻는다. CUBRID의 섹터 크기를 요즘 SSD 기준으로 다시 재 볼 만하다. - Persistent memory / zero-copy storage. Optane PMEM을 노린 엔진(예: FB의 RocksDB-PMem, MS의 FishStore)은 버퍼 매니저 자체를 건너뛰고 저장소를 메모리로 다룬다. 디스크 매니저 추상이 곧 할당기로 줄어든다. CUBRID가 PMEM을 떠받친다면 가장 먼저 손이 가야 할 계층이 볼륨/파일 계층이다.
- Lock-free 가용 자리 좇기. Sadoghi et al., LeanStore(ICDE 2018)와 FineLine 라인(Sauer et al., VLDB 2020)이 익스텐트 단계 메타데이터를 lock-free 자료 구조로 둔다. CUBRID의 목적별
mutex_reserve는 32코어 너머에서 알려진 병목이다. 비교는 그 사이를 잰 결과로 끝맺을 수 있다. - 세그먼트 단위 체크포인트(FineLine). FineLine은 페이지 단위 LSN 순서를 버리고 세그먼트 단위 체크포인트로 옮긴다. CUBRID가 그것을 받아들인다면
DISK_VOLUME_HEADER의chkpt_lsa가 다시 짜여야 한다. 출발점은 Sauer et al., FineLine: Log-structured Transactional Storage and Recovery(VLDB 2020).
이 절의 의도는 다음 문서를 띄우는 것이지 분석 자체가 아니다. 항목마다 자기 차례에 자기만의 큐레이션 노트가 되도록 두자.
원본 분석 (raw/code-analysis/cubrid/storage/disk_manager/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/storage/disk_manager/)”DFM1_overview.pdf— 어휘(볼륨/섹터/파일/페이지), 디스크 매니저와 파일 매니저의 갈라짐, 볼륨 타입과 목적.DFM2_volume_architectur.pdf— 볼륨 헤더, 섹터 할당 표, STAB 유닛/커서 훑기 무늬.DFM3_sector_reservation.pdf— 두 단계 프로토콜,DISK_RESERVE_CONTEXT,DISK_CACHE,DISK_EXTEND_INFO, 풀기 길, log_append_postpone 인계.DFM4_volume_extention.pdf— 목적마다의 확장 라우팅, 키우기-그다음-더하기 정책, nested top action의 까닭,disk_volume_expand/disk_add_volume/disk_add_volume_extension.DFM5_file_architecture.pdf— 파일과 볼륨, 파일 타입, 페이지 종류(FTAB / 유저 / 시스템), Numerable 속성, FILE_HEADER 필드.DFM6_page_allocation.pdf— 세 가지 파일 표, FileExtendibleData의 모양과 연산, 영구/임시 할당 길, 페이지 풀기.DFM7_file_creation.pdf— 파일 만들기/부수기, File Tracker, Temp Cache 상태 머신.Disk Manager 1주차 분석 QnA.pdf—PRM_ID_BOSR_MAXTMP_PAGES,disk_Temp_max_sects,boot_sr.c의미 정리.disk_manager_presentation_material.pdf— 위 일곱 주제를 모은 발표 자료.[코드분석]volume_file.pptx— 본 문서의 그림 일부의 원본 PPTX.disk_manager.md— 더 이른 시점의 한국어 산문(12.5 KB). 어휘 사전 측면에서 교차 확인.
Notion (CUBRID DEV WIKI)
섹션 제목: “Notion (CUBRID DEV WIKI)”- Storage – Concurrency 코드 분석 — 모듈 단위 자리 잡기. 디스크 매니저와 파일 매니저는 heap, btree, catalog, log가 모두 그 위에 올라앉는 받침층이다.
교재 챕터 (knowledge/research/dbms-general/)
섹션 제목: “교재 챕터 (knowledge/research/dbms-general/)”- Database Internals(Petrov), 1장 Introduction and Overview — 페이지 기반 저장소와 버퍼/저장소 경계.
- Database Internals(Petrov), 3장 File Formats — 슬롯 페이지, 페이지 식별자, 가용 자리 다루기, 세그먼트와 익스텐트 할당.
- Database System Concepts(Silberschatz, Korth, Sudarshan, 6판), 13장 Storage and File Structure — 파일 짜임새, 고정 길이와 가변 길이 레코드.
- Mohan et al., ARIES: A Transaction Recovery Method(TODS 1992) — nested top action.
log_sysop_*의 이론적 받침. - Liskov & Scheifler, Guardians and Actions(POPL 1982) — nested top action에 대한 더 이른 형식적 풀이.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/storage/disk_manager.hsrc/storage/disk_manager.csrc/storage/file_manager.hsrc/storage/file_manager.csrc/storage/storage_common.h(DB_VOLPURPOSE / DB_VOLTYPE / VFID / VPID / VSID)src/transaction/log_manager.h(log_sysop_start/log_append_postpone이 본 문서에서 가리킴)