본문으로 건너뛰기

1.2 Copy-on-Write와 Thin Clone 계보

Copy-on-write는 새로운 기술이 아닙니다. 프로세스 fork는 메모리 page를 공유하다가 쓰기 시점에 복사합니다. 파일시스템은 스냅샷을 만들 때 블록을 복사하지 않고 참조만 늘립니다. 모두 같은 원리입니다. 데이터베이스 branching 제품들은 이 원리를 서로 다른 계층에 적용했습니다. copy-on-write를 적용하는 계층이 제품의 성격을 결정합니다. 따라서 이 장은 그 계층을 분류축으로 삼아 계보를 정리합니다.

공유 후 쓰기 시점의 분리

copy-on-write는 두 단계로 동작합니다. 복사본을 만들 때는 원본을 가리키는 참조만 생성합니다. 이때 실제 데이터는 하나입니다. 이후 원본이나 복사본에서 쓰기가 발생하면 해당 단위(블록, page, 청크)만 새로 기록합니다. 쓰기가 발생한 쪽의 참조도 새 위치로 바꿉니다. 쓰기가 발생하지 않은 부분은 계속 공유합니다.

Aurora 문서의 설명이 이 동작을 잘 보여 줍니다. 원본 클러스터 A에 page 1, 2, 3, 4가 있고 clone B를 만들면 B는 같은 page 네 개를 가리킵니다. A가 page 1을 수정하면 Aurora는 원본을 덮어쓰지 않습니다. 새 page 1[A]를 만들고 A의 참조만 새 위치로 바꿉니다. B가 page 4를 수정하면 4[B]가 생기고 B의 참조만 새 위치로 바뀝니다. 결과적으로 A는 1[A], 2, 3, 4를 참조합니다. B는 1, 2, 3, 4[B]를 참조합니다. 저장 공간에는 기존 네 page 외에 두 page만 추가됩니다.

이 동작을 적용하는 계층에 따라 장단점이 갈립니다.

아래로 갈수록 범용적이며 데이터베이스를 수정하지 않습니다. 위로 갈수록 데이터베이스의 의미(LSN, 트랜잭션, row)를 활용하지만 엔진을 수정해야 합니다.

파일시스템 계층

ZFS는 스냅샷과 clone을 파일시스템 기능으로 제공합니다. zfs snapshot은 현재 블록 트리에 이름을 붙이는 작업이므로 즉시 끝납니다. zfs clone은 그 스냅샷을 쓰기 가능한 새 데이터셋으로 만듭니다. PostgreSQL의 데이터 디렉터리가 ZFS 데이터셋 위에 있다고 가정합니다. clone된 디렉터리에서 두 번째 PostgreSQL을 실행하면 thin clone이 됩니다. 다만 포트만 바꿔서는 충분하지 않습니다. clone에 남은 postmaster.pid를 지워야 합니다. port와 unix socket 디렉터리는 원본과 분리해야 합니다. archive_command나 replication 설정처럼 원본을 가리키는 항목도 해제해야 합니다. PGDATA와 pg_wal, tablespace가 다른 데이터셋에 있다면 같은 시점의 스냅샷으로 함께 묶어야 합니다. 절차는 5.1 ZFS Snapshot과 Clone에서 다룹니다.

LVM snapshot은 볼륨 관리자 계층에서 같은 작업을 수행합니다. 스냅샷 볼륨에 예외 영역을 두고, 원본에 쓰기가 발생하면 원래 블록을 예외 영역에 먼저 복사합니다. btrfs는 subvolume 스냅샷으로 ZFS와 비슷한 모델을 제공합니다.

파일시스템 계층의 장점은 PostgreSQL을 전혀 수정하지 않는다는 점입니다. 어떤 버전과 extension도 그대로 사용합니다. 대신 스냅샷은 파일 단위의 crash-consistent 상태를 담습니다. 따라서 clone에서 기동한 PostgreSQL은 crash recovery를 거칩니다. 파일시스템은 트랜잭션 단위의 시점이나 특정 LSN에서의 분기를 인식하지 못합니다.

DBLab Engine은 이 계층을 제품화한 사례입니다. ZFS(기본) 또는 LVM 위에 데이터 디렉터리를 두고 주기적으로 스냅샷을 만듭니다. 요청이 오면 clone을 만들고 컨테이너에서 PostgreSQL을 기동합니다. 문서 사례에서는 10TiB 데이터베이스의 clone이 수 초 안에 끝납니다. 이 수치는 이 노트가 참고한 README 요약을 기준으로 합니다. 버전과 하드웨어에 따라 달라집니다. 4.0부터는 clone 위에 branch, commit, switch 같은 Git 어휘를 기능으로 추가했습니다. 실행 절차는 5.2 DBLab Engine에서 살펴봅니다.

데이터베이스 파일 계층의 선례

SQL Server의 database snapshot은 데이터베이스 파일 단위에서 copy-on-write를 구현한 오래된 기능입니다. 스냅샷은 sparse file로 생성됩니다. 원본 page를 수정하기 직전에 기존 page를 sparse file에 복사합니다. 읽기 전용이어서 branch 용도로는 쓰지 못합니다. 다만 "수정 전 page를 별도로 보관한다"는 형태는 이후 제품들과 같습니다.

Oracle Multitenant의 snapshot copy PDB는 스토리지의 스냅샷 기능을 이용해 PDB를 thin clone하는 방식입니다. 문서에 따르면 ACFS 같은 지원 스토리지가 필요합니다. 실제 동작은 하부 스토리지가 담당합니다. Delphix 같은 상용 데이터 가상화 제품군도 자체 스토리지 계층에서 copy-on-write를 구현했습니다. 이를 바탕으로 여러 데이터베이스 엔진에 thin clone을 제공해 왔습니다. 이 계열은 온프레미스 기업 환경에서 오래 쓰였습니다. 그러나 라이선스 비용과 별도 인프라가 필요해 개발자 도구로 널리 확산하지 못했습니다.

스토리지 볼륨 계층

Aurora는 데이터베이스 엔진 아래에 분산 스토리지 볼륨을 둡니다. 이 볼륨은 copy-on-write protocol로 clone을 만듭니다. clone은 볼륨이 처리하지만 엔진은 vanilla가 아닙니다. Aurora PostgreSQL은 redo 로그를 스토리지로 보내고 checkpoint를 수행하지 않습니다. 이런 전용 통합 구조를 갖춘 Aurora 엔진입니다. 뒤에서 설명할 Xata나 Tiger의 무수정 PostgreSQL과는 구분해야 합니다. 문서는 copy-on-write clone은 15개까지이고, 그 다음 clone은 full copy가 된다고 명시합니다. clone에서 다시 clone을 만드는 것도 허용됩니다. 원본을 삭제하면 공유 page의 소유권을 남은 clone들에 재분배합니다.

Xata는 같은 계층을 오픈소스 부품으로 구성했습니다. 블록 스토리지를 NVMe-oF로 네트워크에 노출합니다. 그 블록 계층에서 copy-on-write branching을 수행합니다. 그 위의 PostgreSQL은 수정하지 않은 vanilla 빌드입니다. Kubernetes의 CloudNativePG operator와 OpenEBS 스토리지 위에서 실행합니다. 문서에 따르면 branch 생성 시간은 초기 20초 이상에서 1~2초로 줄었습니다. Tiger Cloud 문서는 fork가 Fluid Storage라는 자체 스토리지 계층을 사용한다고 설명합니다. 이 계층에서 copy-on-write로 fork를 만듭니다. 무료 서비스에서는 30~90초가 걸립니다.

스토리지 볼륨 계층에서는 데이터베이스를 그대로 사용합니다. 또한 파일시스템보다 큰 규모와 네트워크 분리를 제공합니다. 단점은 해당 스토리지가 특정 클라우드나 스택에 종속된다는 점입니다. Aurora clone은 AWS 밖에서 재현되지 않습니다. Xata의 오픈소스 판도 Kubernetes와 OpenEBS 스택을 전제로 합니다.

데이터베이스 page 버전 계층

Neon은 copy-on-write를 PostgreSQL page의 버전 단위에 적용합니다. compute 노드는 데이터를 로컬 디스크에 저장하지 않습니다. page를 읽어야 할 때 pageserver에 "이 page를 이 LSN 시점으로" 요청합니다. pageserver는 WAL을 page별로 나눠 layer 파일에 저장합니다. 요청이 오면 base image에 WAL을 재생합니다. 그런 다음 해당 시점의 page를 만들어 반환합니다.

이 구조에서 branch는 별도의 복사 작업이 아닙니다. "특정 LSN을 조상으로 가리키는 새 timeline"입니다. 자식 timeline에 없는 page는 조상 timeline을 재귀적으로 찾아 읽습니다. 현재 LSN과 보존 범위 안의 과거 LSN에 같은 분기 방식이 적용됩니다. 따라서 PITR과 branching이 하나의 메커니즘으로 통합됩니다. 이를 구현하려면 PostgreSQL의 저장 관리자(smgr)를 바꿔야 합니다. 실제로 Neon은 PostgreSQL 소스에 패치를 유지합니다. 자세한 구조는 2.4 Timeline과 Branch 내부에서 살펴봅니다.

논리 구조 계층

Dolt는 가장 높은 계층에서 copy-on-write를 수행합니다. 테이블 데이터를 Prolly tree라는 내용 주소 기반 트리에 저장합니다. branch 사이에서 변경되지 않은 노드는 포인터로 공유합니다. 수정된 leaf를 새로 기록합니다. root까지 이어지는 경로의 내부 노드도 새로 기록합니다. 재청킹의 영향을 받은 노드도 새로 기록합니다. 이 계층에서는 branch뿐 아니라 diff와 merge도 row 단위로 처리합니다. Git처럼 두 branch의 변경을 세 방향 머지로 합칩니다.

대신 엔진을 새로 만들어야 합니다. Dolt는 MySQL 프로토콜을 구현한 독자 엔진입니다. PostgreSQL 호환판인 Doltgres는 베타 단계입니다. Doltgres 문서는 vanilla PostgreSQL보다 약 5배 느리고 SQL 호환 테스트 통과율이 약 91%라고 밝힙니다. 5.3 Dolt와 Doltgres에서 직접 실행해 봅니다.

계층별 비교

계층대표 사례CoW 단위데이터베이스 수정특정 시점(LSN) 분기row 단위 머지
파일시스템ZFS, LVM, btrfs, DBLab블록없음없음(스냅샷 시각)없음
데이터베이스 파일SQL Server snapshotpage엔진 내장없음(읽기 전용)없음
스토리지 볼륨Aurora, Xata, Tiger블록 / 청크Xata, Tiger 없음. Aurora는 전용 엔진서비스별 상이없음
page 버전Neonpage 버전패치 필요지원없음
논리 구조Dolt, Doltgresrow / 트리 노드독자 엔진commit 단위지원

표에서 보듯 낮은 계층은 범용성을 얻고, 높은 계층은 데이터베이스 의미를 얻습니다. 어느 쪽이 더 낫다고 판단하기보다 필요한 성질을 먼저 정해야 합니다. 운영 중인 PostgreSQL을 유지하면서 thin clone만 원한다면 파일시스템 계층이 가장 적은 변화로 목적을 이룹니다. 과거 임의 시점 분기와 서버리스 compute까지 원한다면 page 버전 계층이 필요합니다. 데이터 머지가 목적이라면 논리 구조 계층만이 답입니다.

일관성의 단위

파일시스템 스냅샷은 "그 순간의 디스크 상태"를 담습니다. PostgreSQL은 이 상태에서 기동할 때 WAL을 재생해 일관된 상태로 돌아옵니다. 그러나 이는 crash recovery입니다. 트랜잭션 경계에 맞는 시점을 선택한 것은 아닙니다. PGDATA, pg_wal, tablespace가 한 번에 포함되는 원자적 스냅샷이라면 먼저 CHECKPOINT를 실행합니다. 그러면 재생할 WAL 양이 줄어듭니다. clone을 기동할 때 crash recovery로 정합성을 맞추면 충분합니다. 여러 볼륨을 따로 스냅샷으로 만들어야 한다면 온라인 base backup 절차를 따라야 합니다. PostgreSQL 15 이후에는 pg_backup_start('label')을 호출한 세션을 유지해야 합니다. 같은 세션에서 pg_backup_stop()을 호출해야 합니다. 그 결과로 받은 backup_label 내용을 clone에 배치해야 유효한 백업이 됩니다. 인수 없이 pg_backup_start()만 실행하면 동작하지 않습니다. 반대로 Neon 같은 page 버전 계층은 LSN을 직접 지정합니다. 따라서 WAL 기록 순서 위에서 임의 시점을 선택합니다. 두 방식의 차이는 "어느 시점인가"를 묻는 사고 조사에서 드러납니다.

연습 문제

  1. ZFS clone과 Neon branch에서 "분기 시점"이 각각 무엇을 뜻하는지 한 문장씩 적습니다. 어느 쪽이 트랜잭션 경계와 맞는지 설명합니다.
  2. Aurora clone 15개 제한이 왜 생기는지 스토리지 계층 관점에서 추정하고, Neon의 layer 파일 구조에서는 같은 제한이 필요한지 생각해 봅니다.
  3. row 단위 머지가 필요한 실제 업무 사례를 하나 들고, 그 경우 Dolt 계열 외의 대안이 있는지 검토합니다.

참고