TT Lab
요금제

한국어

시작하기
블로그

블로그

학생 VM 이 뜨기까지 4: PVC 관점의 오버레이 — 골든 디스크 하나, 학생마다 얇은 쓰기 층

상태: 초안 (2026-10-16), 우리 코드 + 공식 문서·소스 + 장난감 실측. 골든 VM 실측은 아직 없음. 골든 디스크는 코드와 운영자 도구까지 들어갔지만, 글을 쓰는 날 클러스터에는 골든 PVC 도 그것을 읽을 권한도 아직 적용되지 않았습니다(읽기 전용 조회로 확인). 그래서 이 글의 골든 VM 동작은 KubeVirt v1.9.0·CDI v1.66.0 의 문서와 소스로 확인한 것이고, 숫자는 노트북에서 돌린 장난감과 컨테이너 디스크(같은 원리)에서 잰 것입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 문서에서 찾지 못한 것은 「미확인」으로 표시합니다.

학생 VM 이 뜨기까지 4: PVC 관점의 오버레이

목차

3편은 두 길의 빈자리로 끝났습니다. 컨테이너 디스크는 20초에 뜨지만 루트가 2.4GiB 이고, DataVolume 은 루트가 크지만 VM 마다 이미지를 새로 가져와 첫 부팅이 195초까지 갑니다. 이 글은 그 사이를 메우는 세 번째 길, 골든 PVC + 쓰기 층을 봅니다.

1. 하고 싶은 것

네 가지를 한꺼번에 얻는 오래된 방법이 있습니다. 읽기 전용 바탕 하나 위에, 사람마다 바뀐 부분만 담는 얇은 층을 얹는 것입니다. 컨테이너 이미지의 레이어, 가상화의 링크드 클론, qcow2 의 백킹 파일이 모두 같은 생각입니다.

2. 원리: 바탕과 쓰기 층

골든 PVC 하나 위에 학생마다 얇은 쓰기 층 ① 디스크 한 장은 두 겹이다 (칸 = 클러스터) 게스트가 보는 /dev/vda 쓰기 층 disk.qcow2 바탕(골든) disk.img 쓴 칸은 쓰기 층에서, 안 쓴 칸은 바탕에서 읽는다. 처음 쓰는 칸은 바탕에서 통째로 끌어올린 뒤 고친다. ② 여러 학생이 하나의 골든을 나눠 읽는다 노드 1 · virt-launcher 학생 A의 VM QEMU 쓰기 층 emptyDir 노드 1 · virt-launcher 학생 B의 VM QEMU 쓰기 층 emptyDir 노드 2 · virt-launcher 학생 C의 VM QEMU 쓰기 층 emptyDir 안 쓴 칸 읽기 골든 PVC — 공유 스토리지(CSI 드라이버) · RWX disk.img(raw) · 모든 VM 이 readOnly: true 로 붙는다 VM 이 소유하지 않는다 → VM 을 지워도 남는다 세션이 끝나면 파드가 사라지면서 쓰기 층(emptyDir)도 함께 간다.
그림: 골든 PVC 하나 위에 학생마다 얇은 쓰기 층

qcow2 디스크 이미지는 백킹 파일을 가리킬 수 있습니다. qemu-img create -f qcow2 -b <바탕> -F raw <쓰기 층> 으로 만들면 새 이미지는 바탕과 달라진 부분만 기록합니다(qemu-img 문서). 이때 디스크는 일정한 크기의 칸(클러스터)으로 나뉘고, 규칙은 두 줄입니다.

그래서 쓰기 층은 비어서 시작하고, 게스트가 쓴 칸만큼만 자랍니다. 바탕을 바꾸지 않으므로 여러 디스크가 같은 바탕을 함께 가리켜도 서로 섞이지 않습니다.

장난감으로 재 보기

노트북에 qemu-img 가 없어서(새로 받지 않았습니다) 위의 두 규칙만 파이썬으로 흉내 내어 같은 조건에서 쟀습니다. 바탕 512MiB(무작위 바이트), 게스트가 4KiB 쓰기를 무작위 자리에 1,000번 하는 일입니다. 「통째로 복사한 뒤 쓰기」 와 「빈 쓰기 층에 쓰기」 를 다섯 번씩 돌린 중앙값입니다(0번 바퀴는 예열로 뺐습니다). 두 방식의 디스크 전체 해시가 다섯 바퀴 모두 같아, 게스트가 보는 디스크는 똑같다는 것도 확인했습니다.

칸 64KiB 칸 2MiB
통째로 복사(512MiB) 0.217초 0.151초
빈 쓰기 층 만들기 0.0002초 0.0002초
쓰기 1,000번 뒤 쓰기 층 크기 59MiB 504MiB(바탕의 98%)

환경: Apple M1 Max 노트북, 내장 SSD, Python 3.12, 표준 라이브러리만. 코드와 원자료는 docs/experiments/2026-10-16-cow-overlay-toy/ 에 있습니다.

숫자에서 읽을 것은 세 가지입니다.

  1. 만들기는 사실상 공짜입니다. 빈 쓰기 층은 디스크 크기와 상관없이 0.2밀리초입니다. 복사는 디스크 크기에 비례합니다. 여기서 0.2초인 것은 같은 SSD 안의 복사이고 바탕이 파일 캐시에 있었기 때문이라, 네트워크 너머의 저장소에서 10Gi 를 복사하는 시간은 이 숫자로 말할 수 없습니다(미실측). 계산만 해 보면 1Gbps 회선에서 10GiB 는 이론상 약 86초입니다.
  2. 쓰기 층은 「쓴 양」 이 아니라 「건드린 칸」 만큼 자랍니다. 실제로 쓴 것은 3.9MiB 인데 64KiB 칸 945개를 건드려 59MiB 가 됐습니다(약 15배). 흩어진 작은 쓰기는 칸 크기만큼 부풉니다.
  3. 칸이 크고 쓰기가 흩어지면 층이 바탕만큼 커집니다. 2MiB 칸에서는 512MiB 디스크가 256칸뿐이라 쓰기 1,000번이 거의 모든 칸을 건드렸습니다. 쓰기 층이 얇게 유지되는 것은 게스트가 디스크의 작은 부분만 고친다는 가정 위에서입니다. OS 를 통째로 업그레이드하는 실습이라면 이 가정이 깨집니다.

실제 VM 에서는 어떨까요. 같은 구조인 컨테이너 디스크 VM 하나(운영자용, 9일째 실행 중)를 읽어 보니, 바탕(약 596MiB) 위의 쓰기 층이 약 1.10GiB 였습니다(실측, 2026-10-14). 9일 동안 패키지를 깔고 로그를 쌓은 VM 이라 한 시간짜리 실습 세션보다 훨씬 많이 쓴 경우입니다. 골든 VM 의 부팅 직후·실습 뒤 쓰기 층 크기는 미실측입니다.

3. KubeVirt 의 ephemeral 볼륨: 이것을 PVC 위에서

KubeVirt 에는 이 구조를 PVC 로 해 주는 볼륨 원본이 이미 있습니다. 사용자 안내서의 ephemeral 볼륨입니다. 요약하면 이렇습니다.

TT Lab 백엔드가 만드는 골든 VM 의 루트 볼륨은 이것 한 줄입니다(backend/app/vms.py 의 vm_manifest).

volumes:
  - name: root
    ephemeral:
      persistentVolumeClaim:
        claimName: golden-ubuntu-24.04-10gi   # OS 와 크기로 정해진 이름
        readOnly: true                        # 빼지 않는다

KubeVirt v1.9.0 소스를 따라가면 이 한 줄이 다음 일을 합니다.

  1. virt-controller 가 virt-launcher 파드를 만들 때, 이 PVC 를 파드 볼륨으로 그대로(readOnly 포함) 옮겨 적고, 쓰기 층을 둘 빈 디렉터리(emptyDir)를 하나 더 붙입니다(pkg/virt-controller/services/rendervolumes.go).
  2. 파드 안에서 PVC 는 /var/run/kubevirt-private/vmi-disks/root/ 에 붙고, 바탕은 그 안의 disk.img(raw)입니다.
  3. 시작할 때 qemu-img create -f qcow2 -b <바탕 disk.img> -F raw <쓰기 층 disk.qcow2> 로 쓰기 층을 만듭니다(pkg/ephemeral-disk/ephemeral-disk.go). 쓰기 층의 가상 크기는 바탕과 같습니다. 그래서 바탕의 크기가 곧 게스트 루트의 크기입니다.

컨테이너 디스크도 같은 원리라는 점이 재미있습니다. 바탕이 레지스트리에서 받은 노드 캐시냐, 스토리지 클래스가 내준 PVC 냐만 다릅니다(3편 그림).

4. PVC 의 눈으로 보면

「누가 어떤 PVC 를 갖고, 무엇이 언제 생기고 사라지나」 로 정리하면 이렇습니다.

골든 PVC 쓰기 층
무엇 OS·크기마다 하나(이름 golden-<OS>-<크기>) VM 마다 하나의 disk.qcow2 파일
어디에 공유 스토리지(CSI 드라이버), RWX, 파일시스템 모드 virt-launcher 파드의 emptyDir — 노드 로컬, PVC 가 아니다
누가 만드나 운영자가 CDI DataVolume 으로 한 번(레지스트리 원본) KubeVirt 가 VM 을 켤 때마다
누가 소유하나 아무도(VM 이 소유하지 않는다) virt-launcher 파드
붙는 모양 모든 VM 이 readOnly: true 로 그 VM 의 QEMU 만
언제 사라지나 운영자가 지울 때만 VM 이 끝나 파드가 지워질 때

몇 가지 결정에는 이유가 있습니다.

판정과 대체

백엔드는 VM 을 만들기 전에 골든을 읽어 지금 써도 되는지 판정합니다. 없는 PVC 를 가리키는 VM 은 KubeVirt 가 파드를 만들지 못해 영영 뜨지 않기 때문입니다(v1.9.0 rendervolumes.go 의 PVC 없음 오류).

상황 결과
골든 PVC 가 없다 · 읽을 권한이 없다 · API 오류 컨테이너 디스크로 연다
지우는 중이다 · 아직 바인딩되지 않았다 컨테이너 디스크로 연다
한 노드에만 붙는 볼륨이다 컨테이너 디스크로 연다
CDI 가 아직 가져오는 중이다 컨테이너 디스크로 연다
다른 이미지에서 가져온 골든이다 컨테이너 디스크로 연다
레슨의 값이 오타다 오류(커리큘럼 시험이 먼저 잡는다)

대체될 때 학생 터미널에는 「루트가 약 2.4GiB 로 평소보다 작다, df -h / 로 확인하라」 는 안내가 뜹니다. 안내는 대화형 셸에서만 찍습니다. 채점 에이전트도 로그인 셸로 명령을 돌리기 때문에, 아니면 안내가 채점 출력에 섞입니다. 왜 DataVolume 이 아니라 컨테이너 디스크로 돌아가느냐 하면, 백엔드에 PVC 를 만드는 권한을 다시 주지 않기 위해서이고, 195초짜리 첫 부팅이 골든 실습의 준비 시간 상한을 넘겨 오히려 실패로 끝나기 때문입니다. 그래서 골든은 최적화이지 전제가 아닙니다. 골든을 켠 실습도 컨테이너 디스크에서 통과해야 합니다.

5. 왜 CSI 복제나 스냅샷이 아닌가

쿠버네티스에는 볼륨을 불리는 공식 방법이 이미 둘 있습니다.

CDI 는 DataVolume 으로 PVC 를 복제할 때 이 둘을 알아서 고릅니다. 스냅샷을 쓸 수 있으면 「스냅샷 → 복원 → 스냅샷 삭제」 로 하고(smart clone), CSI 복제를 쓸 수 있으면 그것을, 둘 다 안 되면 원본에서 대상으로 데이터를 흘려 보내는 호스트 복사로 떨어집니다(CDI 복제·StorageProfile 문서). 이 선택은 스토리지 클래스의 StorageProfile 에 cloneStrategy 로 남습니다.

학생 셋에게 10Gi 루트를 주는 세 방법 가. PVC 복제(CSI 클론 · 호스트 복사) 골든 A 10Gi B 10Gi C 10Gi 만들 때: 저장소가 클론을 하면 빠르고, 못 하면 10Gi × 3 을 복사 공간: 저장소 구현에 따라 0 ~ 30Gi · VM 마다 PVC 하나 나. 스냅샷에서 복원 골든 스냅샷 A 10Gi B 10Gi C 10Gi 필요: CSI 드라이버의 스냅샷 기능 + VolumeSnapshotClass 공간·시간은 저장소가 복원을 어떻게 하느냐에 달렸다 다. 골든 + 쓰기 층(이 시리즈의 방법) 골든 A 쓴 만큼 B 쓴 만큼 C 쓴 만큼 만들 때: 빈 파일 하나 · 공간: 골든 10Gi + 학생마다 쓴 만큼 대가: 쓰기 층이 노드 로컬이라 VM 을 끄면 사라진다 글 쓰는 날 TT Lab 의 공유 스토리지: 스냅샷 클래스 없음, CDI 복제 전략 copy. 나는 쓸 수 없고, 가는 네트워크로 통째로 복사한다(2026-10-16 확인).
그림: 학생 셋에게 10Gi 루트를 주는 세 방법

글을 쓰는 날 TT Lab 의 공유 스토리지 클래스를 읽어 보니, 스냅샷 CRD 는 깔려 있지만 VolumeSnapshotClass 가 하나도 없고, StorageProfile 의 복제 전략은 copy(호스트 복사) 였습니다(2026-10-16 조회). 그러니 이 저장소에서 PVC 복제로 학생마다 10Gi 를 만들면, VM 하나마다 10Gi 를 네트워크로 통째로 복사하게 됩니다. 스냅샷 길은 아예 없습니다.

저장소가 바뀌어도 쓰기 층이 유리한 점은 남습니다.

PVC 복제·스냅샷 복원 골든 + 쓰기 층
VM 마다 생기는 PVC 하나 없음
PVC 쿼터(VM 네임스페이스 12개)에 대해 동시 VM 수가 PVC 수에 묶인다 골든 수만 센다
백엔드에 필요한 권한 PVC 만들기·지우기 PVC 읽기 하나
만들 때 드는 시간 저장소의 복제 구현에 달렸다(여기선 전체 복사) 빈 파일 하나
세션이 끝나면 PVC 를 지워야 한다(지우기 실패 = 공간 누수) 파드와 함께 사라진다
학생 VM 을 끄고 다시 켜면 디스크가 남는다 쓰기 층이 사라진다

마지막 줄이 이 방식이 치르는 값입니다. 실습 세션은 한 번 열고 끝나므로 받아들였습니다. 학생이 며칠에 걸쳐 같은 디스크를 이어 써야 하는 과제라면 이 방식은 맞지 않습니다.

6. 세션이 끝나면: 쓰기 층만 사라진다

골든 VM 매니페스트에는 dataVolumeTemplates 가 없습니다. VM 이 골든을 소유하지 않으므로 VM 을 지워도 골든은 남습니다. 회수기가 지우는 것은 VM 과 cloud-init Secret 뿐이고(5편), VM 이 지워지면 virt-launcher 파드가 끝나며 그 emptyDir, 곧 쓰기 층이 함께 사라집니다. 다음 학생에게 넘어가는 것은 아무것도 없습니다.

골든 자체를 바꿔야 할 때(이미지 갱신)는 지우고 같은 이름으로 다시 만듭니다. 쓰고 있는 VM 이 있으면 쿠버네티스의 「사용 중인 스토리지 객체 보호」 때문에 PVC 는 그 VM 들이 끝날 때까지 남고, 그동안 백엔드는 「지우는 중」 을 보고 새 VM 을 컨테이너 디스크로 엽니다. 이름에 버전을 넣어 새 골든을 옆에 만드는 방법(CDI 의 DataImportCron 이 하는 방식)도 있지만, 그러면 백엔드가 이름을 풀려고 권한이 하나 더 늘고 학생 환경이 시험해 보지 않은 이미지로 저절로 바뀝니다. OS 여섯 개 규모에서는 손으로 갈아 끼우는 편이 싸다고 판단했습니다.

7. 대가와 한계

잴 것과 재는 방법

측정 스크립트(scripts/measure_vm_disks.py)는 컨테이너 디스크·DataVolume·골든을 같은 노드에서 한 대씩 만들고 재고 지웁니다. 시작 전과 바퀴마다 노드의 상태와 메모리 여유를 보고, 0번 바퀴는 예열로 따로 적고, 1번 바퀴부터 중앙값·최소·최대를 냅니다. 재는 것은 준비 시간(요청부터 에이전트 준비까지), VMI Running 까지의 시간, 게스트 루트의 크기와 여유, 쓰기 층의 실제 크기(부팅 직후와 128MiB 를 쓴 뒤), 지우기 시간입니다. VM 을 만들고 지우는 일이라 클러스터 쓰기 권한이 있는 사람이 돌립니다.

python3 deploy/vms/golden.py manifest --os ubuntu-24.04 --storage-class <공유 스토리지 클래스>   # 골든 하나(적용은 사람)
python3 scripts/measure_vm_disks.py --preflight --node <노드>                                 # 아무것도 만들지 않고 본다
python3 scripts/measure_vm_disks.py --node <노드> --rounds 4                                  # 예열 1 + 3바퀴

골든을 실습에 켤지는 이 결과로 정합니다. 준비 시간 중앙값이 컨테이너 디스크의 1.5배 안이고, 루트가 9GiB 이상이며, 부팅 직후 쓰기 층이 수백 MB 안이면 kubeadm 류 실습부터 켭니다.

티티 한마디. 서른 명에게 10Gi 씩 나눠 주는데 실제로 만드는 건 10Gi 하나와 빈 파일 서른 개예요. 대신 「바탕은 절대 건드리지 않기」 가 생명이라 readOnly: true 는 꼭 붙여 두고, 쓰기는 칸 단위로 부푼다는 것도 기억해 두세요!

참고

문서와 소스는 모두 2026-10-16 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다