feat: 가상화 문서들 추가
This commit is contained in:
@@ -930,6 +930,227 @@ lint 가 잡아낸 것 중 사람이 놓치기 쉬운 것 둘을 적어 둔다.
|
||||
줄였다」)은 **systemd 를 설명하지 않고는 쓸 수 없는데**, 1층이 그 바닥을 채운다.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 0층. 가상화 — 「바닥」 아래에 있는 것
|
||||
|
||||
1층을 이 실험대의 바닥이라고 적었는데, 정확히는 **호스트의** 바닥이다. 위
|
||||
여덟 층 중 2층부터 위는 전부 게스트 두 대 안에서 돌고, 게스트는 호스트에서
|
||||
프로세스다. 번호를 다시 매기는 대신 아래에 한 층을 더한다.
|
||||
|
||||
이 층을 건너뛰면 호스트에서 읽은 숫자를 게스트의 숫자로 읽게 되고, 그
|
||||
착각은 8층까지 그대로 올라간다.
|
||||
|
||||
#### 게스트는 호스트에서 프로세스 하나다
|
||||
|
||||
**무엇인가.** libvirtd 가 게스트마다 `qemu-system-x86_64` 를 하나씩 띄운다.
|
||||
호스트에서 보면 VM 은 특별한 무엇이 아니라 프로세스 두 개이고, VM 이 쓰는
|
||||
메모리는 그 프로세스의 RSS 다. vCPU 도 마찬가지로 그 프로세스의 스레드라서
|
||||
`htop` 에서 스레드를 켜 두면 게스트마다 두 줄씩 더 나온다.
|
||||
|
||||
```bash
|
||||
export LIBVIRT_DEFAULT_URI=qemu:///system
|
||||
ps -eo rss,args --sort=-rss | grep '[q]emu-system' # 호스트에서 본 VM
|
||||
```
|
||||
|
||||
`htop` 에서는 `F5` 트리 뷰가 `libvirtd` 아래 `qemu-system` 이 달린 모양을
|
||||
보여 주고, `u` 로 `libvirt-qemu` 를 고르면 VM 만 남는다.
|
||||
|
||||

|
||||
|
||||
호스트 경계 안에 있는 것은 libvirtd 와 프로세스 두 개뿐이고, 게스트가 보는
|
||||
디스크와 인터페이스는 자기를 담은 프로세스가 내준다. 프로세스에 적힌 RSS 와
|
||||
게스트에 적힌 available 은 같은 메모리를 다른 껍질에서 읽은 값이다. 그림에
|
||||
`machine.slice` 와 `virbr0` 는 넣지 않았다 — 앞의 것은 1층에, 뒤의 것은 다음
|
||||
항목에 있다.
|
||||
|
||||
**왜 여기 나오나.** A-4 의 노드 상실이 이 층에서 일어난다. `virsh destroy`
|
||||
는 게스트에 ACPI 신호를 보내지 않고 프로세스를 끊으므로, 게스트 입장에서는
|
||||
예고가 없다.
|
||||
|
||||
```
|
||||
=== 워커 노드(kc-lab-2) 전원 차단 — virsh destroy 는 종료 신호가 없다 ===
|
||||
차단 시각: 12:07:43
|
||||
Domain 'kc-lab-2' destroyed
|
||||
+45초 node=NotReady | keycloak-0=Running | 외부 HTTP 503
|
||||
```
|
||||
|
||||
**없거나 틀리면.** 호스트에서 프로세스가 사라진 것과 게스트 안에서
|
||||
서비스가 죽은 것을 같은 사건으로 읽게 된다. 4층의 축출 타이머가 45초 뒤에
|
||||
움직이는 이유는 게스트가 죽었다고 말한 적이 없기 때문이다.
|
||||
|
||||
#### 디스크와 네트워크는 virtio 로 붙는다
|
||||
|
||||
**무엇인가.** 게스트는 실재하는 하드웨어 대신 반가상화 장치를 본다.
|
||||
|
||||
```bash
|
||||
virt-install --name kc-lab-1 --memory 3584 --vcpus 2 \
|
||||
--disk size=20,backing_store=/var/lib/libvirt/images/base.qcow2 \
|
||||
--disk vol=default/seed-kc-lab-1.iso,device=disk,bus=virtio,readonly=on \
|
||||
--network network=default,mac=52:54:00:aa:bb:11 \
|
||||
--import --os-variant debian12 --noautoconsole
|
||||
```
|
||||
|
||||
`--network network=default` 는 게스트를 `virbr0` 에 붙인다. libvirt 의 NAT
|
||||
네트워크이고 대역은 `192.168.122.0/24` 이며, kc-lab-1 이 `.11`, kc-lab-2 가
|
||||
`.12` 다. 「주입이 먹지 않는다」의 여섯 번째, `tc` 를 `eth0` 에 걸었는데
|
||||
아무 일도 없었던 것도 장치 이름이 이 층에서 정해지기 때문이다 — Debian
|
||||
게스트의 인터페이스는 `enp1s0` 다.
|
||||
|
||||
**없거나 틀리면 — 조용히 실패한다.** 시드 ISO 를 virtio 디스크가 아니라
|
||||
SATA CD-ROM 으로 붙이면(`virt-install --cloud-init` 의 기본값이다) Debian
|
||||
`genericcloud` 이미지는 그 장치를 못 본다. 크기를 줄이려고 물리 하드웨어
|
||||
드라이버를 뺀 이미지라 AHCI 가 없다. cloud-init 은 데이터소스를 찾지 못한
|
||||
채 오류를 남기지 않고 끝나고, 밖에서 보이는 증상은 hostname 이 `localhost`
|
||||
로 남고 SSH 가 `Permission denied (publickey)` 로 거부되는 것뿐이다.
|
||||
|
||||
**확인.** 게스트에 들어갈 수 없을 때는 화면을 뜬다.
|
||||
|
||||
```bash
|
||||
virsh domblklist kc-lab-1 # 붙은 디스크
|
||||
virsh net-dhcp-leases default # 게스트 IP
|
||||
virsh screenshot kc-lab-1 /tmp/kc1.ppm # 확장자와 무관하게 PNG 로 저장된다
|
||||
```
|
||||
|
||||
`localhost login:` 이면 cloud-init 이 안 돌았고 `kc-lab-1 login:` 이면 돌았다.
|
||||
|
||||
#### 같은 메모리가 세 곳에서 다르게 보인다
|
||||
|
||||
**무엇인가.** 이 실험대에서 메모리를 읽는 곳은 셋이고, 셋이 다른 값을
|
||||
내는 것이 정상이다.
|
||||
|
||||
| 어디서 | 무엇을 보나 |
|
||||
|---|---|
|
||||
| 호스트 `htop` | QEMU 프로세스의 RSS = 게스트 전체 |
|
||||
| 게스트 `free -m` | 게스트 커널이 나눠 쓰는 값 |
|
||||
| `kubectl top` | 파드·노드 단위 working set |
|
||||
|
||||
2026-09-03, Keycloak 을 올리기 전 호스트만 보면 남은 것이 없어 보였다.
|
||||
|
||||
```
|
||||
lab host 총 7628MB · 사용 7189MB · 여유 439MB
|
||||
├ qemu #1 RSS 3765MB kc-lab-1 (할당 3584MB) → 상한 도달
|
||||
└ qemu #2 RSS 2633MB kc-lab-2 (할당 2560MB) → 상한 도달
|
||||
```
|
||||
|
||||
같은 시각 게스트 안에는 여유가 있었다.
|
||||
|
||||
```
|
||||
kc-lab-1 총 3423MB · used 1464 · buff/cache 2020 · available 1959MB
|
||||
kc-lab-2 총 2480MB · used 580 · buff/cache 1714 · available 1899MB
|
||||
```
|
||||
|
||||
**왜 이런가.** QEMU 의 RSS 는 게스트가 **터치한** 페이지만큼이다. 게스트가
|
||||
페이지 캐시로 메모리를 채우면 RSS 도 할당 상한까지 올라가고, 상한에 닿으면
|
||||
거기서 멈춘다. 위 두 프로세스가 그 상태였다. 그래서 게스트 안에 워크로드를
|
||||
더 올려도 호스트 압박은 늘지 않는다 — 게스트의 페이지 캐시가 밀려날 뿐이다.
|
||||
|
||||
**없거나 틀리면.** 「호스트 여유 439MB」를 자원이 없다는 뜻으로 읽는다.
|
||||
게스트 여유를 합치면 약 3.8GB 였고, 배포 예산은 약 2600Mi 였다.
|
||||
|
||||
**확인.**
|
||||
```bash
|
||||
ps -eo rss,args --sort=-rss | grep '[q]emu-system' # 호스트에서 본 VM
|
||||
ssh kc-lab-1 free -m # 게스트 안 실제
|
||||
kubectl top nodes # working set
|
||||
```
|
||||
|
||||
#### 상한을 바꾸려면 껐다 켜야 한다
|
||||
|
||||
**무엇인가.** 호스트 메모리를 8GB 에서 12GB 로 물리 증설한 뒤, 게스트를 다시
|
||||
만들지 않고 할당만 옮겼다.
|
||||
|
||||
```bash
|
||||
virsh setmaxmem kc-lab-1 5120M --config
|
||||
virsh setmem kc-lab-1 5120M --config
|
||||
```
|
||||
|
||||
`setmaxmem` 이 상한이고 `setmem` 이 현재 할당이다. 현재값을 상한보다 크게
|
||||
줄 수 없으므로 `setmaxmem` 이 먼저다. `--config` 는 다음 부팅부터,
|
||||
`--live` 는 실행 중인 도메인에 즉시 적용된다. 다만 `setmaxmem --live` 는
|
||||
대개 거부된다 — 게스트가 부팅할 때 메모리 맵을 정하기 때문이다.
|
||||
|
||||
**왜 여기 나오나.** 이 재배분이 1층의 `machine.slice` 아래에서 일어난다.
|
||||
그리고 증설 전에는 관측 스택을 올릴 만큼 남지 않았다. 증설 뒤 kc-lab-1 이
|
||||
2045Mi(41%), kc-lab-2 가 1131Mi(28%), 호스트 여유가 3957MB 였다.
|
||||
|
||||
**확인.**
|
||||
```bash
|
||||
virsh dominfo kc-lab-1 | grep -i memory
|
||||
ssh kc-lab-1 free -m # 게스트가 실제로 인식한 값
|
||||
```
|
||||
|
||||
#### swap 은 게스트에 두지 않는다
|
||||
|
||||
호스트에는 8GB 의 swap 이 있고 게스트에는 0MB 다. 이유는 셋이다.
|
||||
k3s 와 kubelet 은 기본적으로 swap 을 거부하고, 호스트 swap 으로 QEMU 의
|
||||
페이지가 밀리면 게스트 성능이 급락하며, 무엇보다 이 실험대가 재는 것이
|
||||
**타이밍**이다. refresh 경쟁과 복제 지연을 재는 동안 swap 이 끼면 8층의
|
||||
측정이 통째로 뜻을 잃는다.
|
||||
|
||||
#### 이 층 아래의 구조 — 조사한 것
|
||||
|
||||
여기까지는 이 실험대에서 읽은 값이다. 아래는 그 아래에 무엇이 있는지를
|
||||
공식 문서에서 확인한 것이고 **이 실험대에서 잰 것이 아니다.** 세 갈래 중
|
||||
앞의 둘은 이 실험대가 쓰고 셋째는 쓰지 않는다.
|
||||
|
||||

|
||||
|
||||
왼쪽부터 CPU · virtio I/O · 패스스루다. 셋의 차이는 호스트 유저공간을
|
||||
지나는가와 몇 번 지나는가에 있다.
|
||||
|
||||
**CPU — 유저공간이 커널에 들어갔다 나온다.** `open("/dev/kvm")` 으로 KVM
|
||||
핸들을 얻고, 시스템 ioctl 로 VM 을, VM ioctl 로 vCPU 를 만든다
|
||||
(`KVM_CREATE_VM` · `KVM_CREATE_VCPU`). 게스트를 돌리는 것은 vCPU ioctl
|
||||
`KVM_RUN` 이고, 커널은 vcpu fd 를 offset 0 으로 mmap 한 공유 메모리
|
||||
(`struct kvm_run`)로 왜 나왔는지를 알린다. 크기는 `KVM_GET_VCPU_MMAP_SIZE`
|
||||
로 묻는다. 문서에 이런 문장이 있다 — 「vcpu ioctl 은 그 vcpu 를 만든
|
||||
스레드에서 내야 한다」. **앞에서 본 「vCPU 는 QEMU 프로세스의 스레드」가
|
||||
여기서 나온다.** ([커널 KVM API 문서](https://docs.kernel.org/virt/kvm/api.html))
|
||||
|
||||
하드웨어 쪽 이름은 VMX 다. 프로세서는 VMX root 와 VMX non-root 로 나뉘어
|
||||
돌고, VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로
|
||||
저장한다. ([Intel SDM Vol. 3C](https://cdrdv2-public.intel.com/789585/326019-sdm-vol-3c.pdf))
|
||||
|
||||
**I/O — 게스트가 보는 장치는 규격이다.** virtio 는 「서로 다른 종류의
|
||||
드라이버와 장치가 통신하는 규약을 정한 공개 표준」이고, 주고받는 통로는
|
||||
virtqueue 라는 링 버퍼다. 게스트에 장치를 내보이는 전송 계층은 PCI · MMIO ·
|
||||
CCW 이고 리눅스에서는 virtio-pci 와 virtio-mmio 가 그 드라이버다.
|
||||
([커널 virtio 문서](https://docs.kernel.org/driver-api/virtio/virtio.html))
|
||||
|
||||
**앞의 「시드를 virtio 디스크로 붙인다」가 이 규격이다.** Debian
|
||||
`genericcloud` 이미지가 AHCI 를 못 보는 것은 그 이미지에 물리 하드웨어
|
||||
드라이버가 없기 때문이지 virtio 가 특별해서가 아니다.
|
||||
|
||||
virtqueue 를 QEMU 밖과 나누는 길이 따로 있다. vhost-user 문서는 그 규약이
|
||||
「리눅스 커널의 vhost 구현을 제어하는 ioctl 인터페이스를 보완」하며 「같은
|
||||
호스트의 유저공간 프로세스와 virtqueue 를 공유하는 제어 평면」이라고 적는다.
|
||||
앞쪽이 QEMU 이고 뒤쪽이 virtqueue 를 소비하는 쪽이다.
|
||||
([QEMU vhost-user 규약](https://www.qemu.org/docs/master/interop/vhost-user.html))
|
||||
|
||||
**패스스루 — 이 실험대는 쓰지 않는다.** VFIO 는 「IOMMU 로 보호되는
|
||||
환경에서 장치 접근을 유저공간에 안전하게 여는, IOMMU 와 장치에 중립인
|
||||
프레임워크」다. 소유의 단위는 장치가 아니라 IOMMU 그룹인데, 「시스템의 다른
|
||||
모든 장치로부터 격리할 수 있는 장치 묶음」이 그룹이고 격리가 늘 장치 하나
|
||||
단위로 되지는 않기 때문이다.
|
||||
|
||||
```
|
||||
/dev/vfio/vfio 컨테이너를 연다
|
||||
/dev/vfio/$GROUP 그룹을 열어 VFIO_GROUP_SET_CONTAINER 로 붙인다
|
||||
VFIO_GROUP_GET_DEVICE_FD 장치 fd 를 받는다
|
||||
VFIO_IOMMU_MAP_DMA 장치가 닿을 주소 범위를 매핑한다
|
||||
```
|
||||
|
||||
호스트 드라이버에서 떼어 `vfio-pci` 에 묶는 것이 장치를 넘기는 방법이고,
|
||||
IOMMU 가 DMA 와 인터럽트 리매핑으로 장치가 아무 메모리나 건드리지 못하게
|
||||
막는다. ([커널 VFIO 문서](https://docs.kernel.org/driver-api/vfio.html))
|
||||
|
||||
**세 갈래의 차이는 깊이다.** CPU 는 `KVM_RUN` 으로 들어갔다 `struct kvm_run`
|
||||
으로 나오는 왕복이 있고, virtio 는 virtqueue 를 누가 소비하느냐에 따라
|
||||
왕복하는 곳이 달라지며, 패스스루는 유저공간 드라이버가 장치에 직접 닿는다.
|
||||
**이 실험대가 잰 값은 앞의 두 갈래에서만 나온 것이다.** 패스스루는 이
|
||||
실험대에 없으므로 여기 적은 것은 문서를 읽은 결과이고 측정이 아니다.
|
||||
|
||||
---
|
||||
|
||||
### 1층. 리눅스와 systemd — 이 실험대의 바닥
|
||||
@@ -2131,6 +2352,15 @@ curl -s localhost:19090/api/v1/targets | jq -r '.data.activeTargets[].labels.job
|
||||
전체를 무효화한다는 규격이다. 뒤엣것이 B-3 의 「이긴 요청의 토큰도 못 쓴다」를
|
||||
설명한다.
|
||||
|
||||
**0층은 성격이 다르다.** 「이 층 아래의 구조」에 적은 것은 이 실험대에서 잰
|
||||
것이 아니라 커널·Intel·QEMU 문서를 읽은 결과다. KVM 의 ioctl 층과
|
||||
`struct kvm_run`, VMX root 와 non-root, virtio 의 virtqueue 와 전송 계층,
|
||||
vhost-user 가 무엇을 보완하는지, VFIO 의 IOMMU 그룹과 DMA 리매핑이 그렇다.
|
||||
**그중 패스스루는 이 실험대에 아예 없다** — 쓴 적이 없으므로 잰 값도 없고,
|
||||
거기 적은 것은 전부 문서에서 옮긴 것이다. 반면 같은 0층의 앞쪽,
|
||||
`qemu-system-x86_64` 의 RSS 와 게스트의 `available` 과 `virsh` 로 옮긴 메모리
|
||||
할당은 이 실험대에서 읽은 값이다.
|
||||
|
||||
**한 항목만은 재지 않았다.** 1층의 `Restart=on-failure` 는 유닛 파일을 읽어
|
||||
적었지 nginx 를 죽여 확인하지 않았다. 이 기록이 스물여섯 번 배운 것이
|
||||
「설정이 그렇다고 그렇게 동작하지는 않는다」이므로 그대로 적어 둔다. 재려면
|
||||
|
||||
Reference in New Issue
Block a user