feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다

기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-17 11:01:55 +09:00
co-authored by Claude Opus 5
parent d473609e0a
commit 2109f726fe
574 changed files with 159654 additions and 1551 deletions
@@ -0,0 +1,94 @@
---
kind: QUESTION
slug: guest-input-hole-under-the-iptables-backend
title: libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한가
topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-1
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
---
# libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한가
libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한지는 재지 않았다. nftables 백엔드에서는 그 체인의 reject 가 밖에서 온 요청을 connection refused 로 끊었고, forward 체인의 accept 도 막지 못했다. 백엔드만 바꿔 재면 닫힌다.
## 관계
- **호스트 안에서는 404, 밖에서는 connection refused — 앞 체인의 accept 가 libvirt 의 reject 를 막지 못했다**
그 기록이 nftables 백엔드에서 증상과 원인과 해결을 닫았고, 이 물음은 거기서 미확인으로 표시된 한 줄을 받는다.
- **엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리**
그 결정이 감수한 일곱 가지 중 3번이 libvirt 방화벽에 구멍을 뚫는 일이라, 이 물음의 답이 그 비용의 적용 범위를 정한다.
- **Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge**
밖에서 들어온 패킷이 게스트까지 가는 경로를 그 기록이 설명한다.
- **이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가**
밖에서 게스트로 들어오는 경로가 FORWARD 를 타는지가 그 모드에서 갈린다.
## 사실
- 제3부가 그린 게스트 패킷 경로 위에서 이 구축이 가장 오래 막힌 지점이 여기라고 §180 이 적었다.
- 호스트에서 curl http://192.168.122.10 을 치면 엣지 nginx 가 404 로 응답했다.
- 밖에서 curl http://100.83.212.4 을 치면 connection refused 가 왔다. 드롭이면 기다리다 죽으니, 타임아웃이 아니라 즉시 거절이라는 점이 단서였다.
- libvirt 는 자기 테이블 ip libvirt_network 의 guest_input 체인을 reject 로 끝낸다. 그 앞에는 established,related 를 accept 하는 규칙이 있다.
- 그 reject 규칙의 카운터가 4 패킷 240 바이트였고 밖에서 친 curl 횟수와 정확히 일치했다. 범인은 그 숫자로 확정했다.
- DNAT 파일에는 priority filter - 10 으로 먼저 도는 forward 체인이 있고 거기에 ct state new accept 를 넣어 두었다. 그런데도 패킷은 뒤 체인에서 거절됐다.
- nftables 는 같은 훅에 붙은 base 체인을 우선순위 순으로 전부 평가한다. 앞 체인의 accept 는 이 체인은 통과라는 뜻이고, 평가를 즉시 끝내는 것은 drop 이다. iptables 감각으로 쓰면 정확히 여기서 틀린다.
- 구멍은 libvirt 체인 맨 앞에 뚫었다. insert 가 맨 앞이고 add 가 맨 뒤다.
- 그 규칙은 휘발성이다. libvirt 가 네트워크를 다시 세우면 guest_input 을 새로 쓰면서 날아가므로 DNAT 유닛의 ExecStartPost 에 넣었다.
- 이 호스트는 nftables 백엔드다. firewall_backend 가 iptables 일 때도 같은지는 재지 않았다고 §180 이 미확인으로 표시했다.
## 가정
- 백엔드를 iptables 로 바꿔도 libvirt 가 밖에서 게스트로 들어오는 경로에 자기 규칙을 만든다고 본다. 확인한 것은 nftables 백엔드 하나뿐이라, 규칙을 쓰는 도구만 달라지고 libvirt 가 그 경로를 거른다는 것 자체는 같다고 전제한다.
- 엣지 게스트와 DNAT 구성은 그대로 두고 백엔드만 바꾼다고 본다. 둘을 같이 바꾸면 결과가 어느 쪽 때문인지 가려지지 않는다.
- §178 이 적은 test-server 한 대에서 잰다고 전제한다. 다른 배포판이나 다른 libvirt 버전에서 같은 결과가 나오는지는 이 물음이 묻지 않는다.
- 밖에서 치는 경로는 전과 같다고 본다. §180 의 측정이 밖에서 100.83.212.4 로 친 요청이었으므로 같은 주소를 같은 방법으로 친다.
## 미지수
- firewall_backend 를 iptables 로 둔 호스트에서 밖에서 게스트로 가는 FORWARD 경로를 무엇이 끝내는지.
- 그때 forward 체인의 ct state new accept 가 실제로 먹는지, 아니면 거기서도 libvirt 쪽 규칙에 구멍을 따로 뚫어야 하는지.
- 구멍이 필요하다면 그 방법이 nftables 에서 쓴 insert 맨 앞 규칙과 어떻게 다른지, 그리고 그 규칙도 네트워크를 다시 세우면 날아가는지.
## 제약
- 잴 수 있는 호스트가 한 대다. §178 이 적은 test-server 는 Arch Linux, i5-1135G7 논리 코어 8, RAM 11,648MiB, QEMU 11.1.1 · libvirt 12.7.0 이다.
- 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT(virbr0) + 호스트 진입 구조를 택했다. 밖에서 들어온 패킷이 FORWARD 를 타는 것이 그 구성 때문이라, 네트워크 모드가 달라지면 이 물음이 묻는 상황도 달라진다.
- 백엔드를 바꾸려면 libvirt 네트워크를 다시 세워야 하고, 그때 guest_input 에 뚫어 둔 구멍이 날아간다. 엣지 게스트가 밖에서 들어오는 요청을 받는 진입점이므로 되돌릴 절차를 먼저 준비한다.
- 이 물음에 쓸 측정 원문이 아직 없다. §180 의 curl 출력도 규칙 덤프도 final/evidence/ 에 없어서, 카운터 4 패킷 240 바이트의 근거는 SSOT 본문에 옮겨 적힌 덤프뿐이다.
## 선택지
### 1. 구멍을 뺀 채 백엔드만 iptables 로 바꾸고 밖에서 한 번 친다
§183 이 적은 그대로다. 백엔드를 바꾸면서 libvirt 네트워크를 다시 세우면 guest_input 의 구멍은 어차피 날아가기 때문에, 구멍 없는 상태가 저절로 만들어진다. 그 상태에서 밖에서 엣지로 curl 을 치고 응답인지 connection refused 인지, 그리고 거기까지 걸린 시간을 적는다. 같은 시각에 libvirt 가 만든 규칙을 그대로 덤프해 어느 규칙이 패킷을 끝내는지 본다. 규칙마다 카운터가 붙어 있으면 그 값이 친 횟수와 맞는지도 함께 읽는다. nftables 에서 범인을 지목한 것이 그 카운터였다.
§180 이 적은 그 forward 체인은 지금 DNAT 파일에 없다. §189 가 싣는 파일에 남은 체인은 prerouting 하나이고, accept 는 유닛의 ExecStartPost 가 libvirt 체인 안에 넣는다. 그래서 아래 셋 가운데 마지막을 재려면 그 체인을 먼저 되돌려 놓아야 한다.
읽어 낼 것은 셋이다.
밖에서 친 요청의 결과 : 응답인가 connection refused 인가
패킷을 끝낸 규칙 : 어느 테이블의 어느 체인인가
forward 체인의 accept : 먹었는가 먹지 않았는가
### 2. 구멍을 남겨 둔 채 백엔드만 바꾼다 — 제외
구멍이 이미 열려 있으면 요청은 통과하고, 그 통과가 구멍 덕분인지 백엔드가 원래 막지 않아서인지 가려지지 않는다.
### 3. libvirt 가 iptables 백엔드에서 만드는 규칙을 문서로 읽어 견준다 — 제외
호스트를 건드리지 않아도 되지만 답이 나오지 않는다. §180 에서도 규칙 목록이 아니라 카운터가 범인을 지목했으니, 우선순위가 다른 base 체인 둘이 한 훅에 붙었을 때 어느 쪽이 패킷을 끝내는지는 이 구성에서 돌려 봐야 나온다.
## 다음 검증
1. libvirt 의 firewall_backend 를 iptables 로 두고 네트워크를 다시 세운다. 이때 guest_input 에 뚫어 둔 구멍이 날아가므로 구멍 없는 상태에서 시작한다.
2. 밖에서 엣지로 curl 을 치고 결과가 응답인지 connection refused 인지, 그리고 거기까지 걸린 시간을 적는다.
3. 같은 시각에 libvirt 가 만든 규칙을 그대로 덤프해 어느 규칙이 패킷을 끝내는지와 그 카운터가 친 횟수와 맞는지를 본다.
4. 출력 원문은 final/evidence/raw/ 에 남기고, 그 실행의 명령과 cwd 와 실행 시각과 종료 코드는 meta/ 에 적는다.
5. 백엔드를 nftables 로 되돌리고 DNAT 유닛의 ExecStartPost 가 구멍을 다시 뚫는지 확인한다.
닫는 조건 : 백엔드를 iptables 로 둔 상태에서 밖에서 친 요청이 응답을 받으면 그 백엔드에서는 구멍이 필요 없다고 적고 닫는다. 여전히 거절되면 어느 규칙이 끝냈는지와 그때의 구멍 방법을 「호스트 안에서는 404, 밖에서는 connection refused」 기록의 해결 절에 행으로 더한 뒤 닫는다. 어느 쪽이든 그 결과가 엣지를 게스트로 옮기며 감수한 비용 3번의 적용 범위를 정한다. 지금 그 비용은 nftables 백엔드에서만 확인했다.
@@ -0,0 +1,115 @@
---
kind: QUESTION
slug: is-this-lab-issuing-certificates-with-http-01-or-dns-01
title: 이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가
topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
source:
- final/document.md#204-재구축할-때-무엇이-남아-있나
- final/document.md#266-dns-01-은-언제-쓰는가-네-가지-경우
- final/document.md#265-도메인-검증-http-01-vs-dns-01
---
# 이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가
어느 방식으로 받고 있는지 아직 읽지 못했다. §266 의 결론과 §190 은 DNS-01 을 가리키는데 원본 가이드 04 는 HTTP-01 로 적혀 있다. `authenticator` 한 값이면 갈리지만 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 못 읽었다.
## 관계
- **인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다**
그 결정이 적은 것이 의도인지 이 호스트의 실제 설정인지를 이 물음이 가른다.
- **DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다**
그 절차가 `--dns-cloudflare` 로 받는 길을 적는다. 이 호스트가 그 길로 받았는지는 확인하지 않았다.
- **실험대를 철거하고 무엇이 남는지 확인한다**
그 절차의 「인증서를 지우지 않는다」가 이 물음의 답에 걸려 있다.
- **갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초**
갱신은 됐는데 서빙까지 안 간 사건이다. 이 물음이 캐는 것은 갱신 자체가 도는지다.
- **공개 터널을 쓰지 않고 tailnet 직결로 둔다 — 홉이 하나 늘면 재려던 계약이 오염된다**
공개 인터넷에서 이 호스트에 닿을 길을 두지 않기로 한 결정이고, HTTP-01 이 성립하지 않는 조건을 그 결정이 만든다.
- **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가**
그 물음이 재는 7단계 가운데 04 의 통과 조건이 이 답으로 확정된다.
## 사실
§266 이 문서 둘이 어긋나 있다고 직접 적었다. 그 절의 결론과 §190 은 DNS-01 을 가리킨다.
원본 가이드 `docs/guides/04-tls/README.md``certbot certonly --webroot` 로 적혀 있다. 전제도 공개 DNS 에 이름 셋이 이 호스트를 가리켜야 한다는 것이다.
`dig +short auth.hyeonworks.com``100.83.212.4` 를 낸다.
`100.64.0.0/10` 은 CGNAT(Carrier-Grade NAT, 통신사 공용 주소 변환)용 예약 대역이라 공개 인터넷에서 라우팅 자체가 안 된다. 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지 않는다. 이것은 규격이고 이 실험대가 잰 값이 아니다.
`certbot plugins` 를 돌린 실측이 `dns-cloudflare` · `standalone` · `webroot` 세 줄을 냈으니 플러그인은 깔려 있다.
§190 이 적은 발급 대상은 `-d hyeonworks.com -d '*.hyeonworks.com'` 이고 lineage 디렉터리는 `/etc/letsencrypt/live/hyeonworks.com/` 이다. 자격증명 파일은 `/etc/letsencrypt/cloudflare.ini` 이고 권한이 `600` 이다.
ACME 명세가 와일드카드를 DNS-01 로만 허용한다. 호스트 한 대에 파일을 놓는 것은 그 이름 하나를 통제한다는 증명이고, DNS 존의 TXT 레코드를 고칠 수 있다는 것은 도메인 전체를 통제한다는 증명이라 증명의 급이 다르다.
§204 가 이 항목을 「미측정」으로 적었다. 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다.
## 가정
지금 서빙되는 인증서가 와일드카드라면 발급은 DNS-01 로 이뤄졌을 수밖에 없다. 다만 그 인증서가 실제로 와일드카드인지는 이 저장소에 출력으로 남아 있지 않다.
플러그인이 보인다는 것과 그것으로 받았다는 것을 같게 읽지 않는다. `certbot plugins` 는 설치된 것을 세지 무엇으로 발급했는지를 세지 않는다.
갱신이 돌고 있다는 것도 아직 확인이 아니다. §218 의 구축 완료 판정 기준이 `systemctl is-active nginx certbot-renew.timer``active active` 를 요구하지만, §190 은 `systemctl list-timers certbot-renew.timer` 의 실제 출력이 남아 있지 않다고 적는다.
갱신 설정이 발급 시점의 방식을 그대로 물려받았다고 전제한다. 발급 뒤에 누가 `renewal/*.conf` 를 손으로 고쳤다면 그 전제가 깨진다.
## 미지수
`/etc/letsencrypt/renewal/*.conf``authenticator` 가 무엇인가.
그 값이 `webroot``standalone` 이면 지금 갱신이 실제로 돌고 있는가. 검증이 성립하지 않는 주소에 HTTP-01 로 설정돼 있다면 갱신은 조용히 실패한다.
그 값이 `dns-cloudflare` 라면 `/etc/letsencrypt/cloudflare.ini` 에 든 토큰의 권한이 `존 하나 + DNS:Edit` 으로 좁혀져 있는가. §266 이 그 범위를 값으로 치르는 것이라고 적었는데, 이 호스트의 토큰이 실제로 그 범위인지는 SSOT 에 없다.
지금 서빙되는 인증서가 와일드카드인가, 그리고 lineage 이름이 무엇인가.
## 제약
호스트의 `sudo` 가 비밀번호를 요구해서 비대화식으로는 읽을 수 없고, 콘솔에서 쳐야 한다. 이것이 §204 가 미측정으로 남긴 까닭이다.
비밀 값을 옮기지 않는다. `cloudflare.ini` 의 토큰은 길이와 존재 여부까지만 적고 값을 찍는 명령을 남기지 않는다.
Let's Encrypt 를 tailnet 에 초대할 방법이 없다. 검증 방식을 바꿔 가며 돌려 보는 실험으로 답을 대신할 수 없다.
발급 한도가 같은 이름 조합에 주당 중복 5장이다. 시험은 `--dry-run` 으로 먼저 한다.
## 선택지
### 1. 갱신 설정 파일을 콘솔에서 직접 읽는다
`authenticator` 한 값이 이 물음을 통째로 닫는다. 같은 콘솔 세션에서 쓸 수 있는 방식과 갱신 예행연습과 이름이 가리키는 주소까지 한 번에 찍어 같은 시각의 출력으로 묶는다. 값을 읽을 수 없는 경우는 파일이 없을 때뿐이고, 그때는 이 호스트가 인증서를 받은 적이 없다는 다른 답이 된다.
### 2. 지금 서빙되는 인증서가 와일드카드인지부터 본다
`sudo` 없이도 밖에서 TLS 핸드셰이크만으로 도메인 목록을 볼 수 있다. 와일드카드로 나오면 발급이 DNS-01 이었다는 쪽으로 정황이 좁혀진다. 다만 좁혀질 뿐 `authenticator` 를 대신하지는 못한다. 발급은 와일드카드로 받고 갱신 설정만 다른 경우를 이 방법으로는 가르지 못한다.
### 3. 인증서를 지우고 다시 받아 본다 — 제외
다시 받아 보면 어느 방식이 성립하는지 바로 드러나지만, §204 가 「어느 쪽인지 모르는 채로는 지우지 않는다」로 그 순서를 막아 두었다. HTTP-01 로 설정돼 있으면 재발급이 안 되고, 그러면 재구축을 시작하자마자 검증 방식부터 손봐야 한다. 답을 얻으려고 답이 필요한 상태를 만드는 순서다.
## 다음 검증
§204 와 §266 이 적은 네 줄을 호스트 콘솔에서 그대로 친다.
1. `sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf` 으로 검증 방식을 읽는다.
2. `certbot plugins` 를 돌려 앞에 `*` 가 붙은 줄을 적는다.
3. `sudo certbot renew --dry-run` 으로 갱신이 실제로 되는지 본다.
4. `dig +short auth.hyeonworks.com` 으로 Let's Encrypt 가 올 수 있는 주소인지를 같은 시각에 함께 남긴다.
5. 지금 서빙되는 인증서가 와일드카드인지와 lineage 이름이 무엇인지를 `certbot certificates` 의 도메인 목록으로 적는다.
6. 출력 원문을 `final/evidence/raw/` 에 남기고 `meta/` 에 명령과 cwd 와 실행 시각과 종료 코드를 적는다. `cloudflare.ini` 의 토큰 값은 찍지 않는다.
닫는 조건 : `authenticator` 한 값과 `--dry-run` 결과가 나오면 닫는다.
`dns-cloudflare` 면 「인증서는 DNS-01 로 받는다」가 이 호스트의 현재 상태를 적은 것으로 확인되고, 철거 절차의 「인증서를 지우지 않는다」가 정책에서 선택으로 바뀐다. §204 가 적은 대로 그때는 백업이 헛수고이므로 지워도 되고 재구축 절차가 한 단계 짧아진다.
`webroot``standalone` 이면 그 결정이 적은 것은 의도이고 실제 설정은 다른 것이므로, §190 과 가이드 04 가운데 어느 쪽이 실재인지를 먼저 고친 뒤 그 기록을 다시 판정한다.
`--dry-run` 이 실패하면 갱신이 이미 멈춰 있다는 뜻이라 그 자체가 새 Case 다. 「갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다」가 갱신은 되는데 서빙까지 안 간 것을 다뤘다면, 이번 것은 갱신 자체가 안 되는 쪽이고 증상이 또 조용하다.
@@ -0,0 +1,95 @@
---
kind: QUESTION
slug: qcow2-transfer-time-over-wifi
title: WiFi 전용 호스트에서 대용량 qcow2 를 옮기는 데 실제로 몇 시간이 걸리는가
topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-3
- final/document.md#181-qcow2-가-담는-것과-담지-않는-것
- final/document.md#178-이-부의-출처와-범위
---
# WiFi 전용 호스트에서 대용량 qcow2 를 옮기는 데 실제로 몇 시간이 걸리는가
이 호스트의 qcow2 한 장을 WiFi 로 다른 기계에 옮기는 데 몇 시간이 걸리는지 아직 재지 않았다. 이더넷이 없어 쓸 수 있는 링크가 그 WiFi 하나뿐이고, 파일이 몇 바이트인지도 SSOT 에 없다. 크기를 먼저 확정하고 한 번 옮겨 시간을 잰다.
## 관계
- **qcow2 파일 한 장이 담는 것 — 매핑표와 데이터 클러스터가 같은 파일 안에 있고, 파일 밖을 가리키는 것은 백킹 파일 경로 하나다**
옮길 대상이 무엇이고 파일 크기가 왜 가상 크기와 다른지를 그 기록이 설명한다.
- **virsh save 의 RAM 덤프 크기와 소요 시간은 할당 메모리에 어떻게 비례하는가**
같은 이동의 다른 절반이라, 실행 상태까지 옮기려면 그 물음이 재는 파일도 함께 건너간다.
- **이 disk image 는 qcow2 인가 RAW 인가, 그리고 Guest 가 보는 크기와 Host 가 실제로 쓰는 크기는 얼마나 다른가**
옮길 바이트 수를 그 물음이 먼저 확정한다. qemu-img info 와 du 의 같은 출력을 둘이 함께 쓴다.
- **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다**
옮기는 것이 현실적이지 않으면 이 실험대는 문서로만 복원되고, 그 기준을 지키는 일이 선택에서 전제로 바뀐다.
- **/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device**
옮길 파일이 어떤 백엔드 형식인지를 그 기록이 가른다.
## 사실
- §178 은 이 호스트에 이더넷 없이 WiFi 만 있다고 적었다. 브리지를 못 쓰고 libvirt NAT(virbr0) + 호스트 진입 구조를 택한 것도 같은 제약 때문이다.
- §181 은 qcow2 가 희소 할당이지 압축이 아니라고 적었다. 20GB 이미지가 2GB 인 것은 쓴 블록만 파일에 존재하기 때문이고, 1TB 를 채우면 1TB 파일이 된다.
- 메타데이터 오버헤드는 클러스터 64KiB · L2 항목 8B 기준 0.02% 미만이다. 1TiB 당 약 160MiB 다.
- 게스트에서 지워도 파일은 줄지 않는다. 클러스터가 이미 할당된 상태라, 줄이려면 fstrim(디스크에 discard='unmap' 이 있어야 한다)이나 qemu-img convert 를 돌려야 한다.
- 복사하면 따라오는 것과 따라오지 않는 것을 §181 이 갈라 적었다. 실행 중인 프로세스와 페이지 캐시가 따라오지 않는 쪽이고, 파일 밖을 가리키는 것은 헤더에 절대경로로 적히는 백킹 파일 경로 하나다.
- §178 이 적은 이 실험대의 게스트는 Debian 12 genericcloud 3대다. 엣지 1대와 k3s 2노드다.
- 이 호스트의 이미지가 몇 바이트인지는 SSOT 어디에도 없다. 제4부에서 형식과 세 크기를 묻는 물음이 아직 열려 있다.
## 가정
- 게스트를 멈춘 상태에서 옮긴다고 본다. 돌고 있는 채로 복사하면 파일이 바뀌는 중에 읽게 되고, 그렇게 옮긴 이미지를 쓸 수 있는지는 이 물음이 다루지 않는다.
- 받는 쪽 기계와 디스크가 병목이 아니라고 전제한다. 그래야 잰 시간이 WiFi 링크의 값이 된다. 그렇지 않으면 어디가 느린지부터 갈라야 한다.
- 링크 상태가 재는 동안 크게 바뀌지 않는다고 본다. WiFi 는 시간대에 따라 값이 흔들리므로 시작 시각과 종료 시각과 평균 전송률을 함께 적는다.
- 손대지 않으면 파일은 커지기만 한다고 전제한다. §181 이 적었듯 게스트에서 지워도 클러스터는 할당된 채라 파일이 줄지 않아서, 지금 재는 시간을 앞으로의 하한으로 읽는다.
## 미지수
- 이 호스트의 qcow2 파일이 실제로 몇 바이트인지.
- 그 파일을 이 WiFi 링크로 다른 기계에 옮기는 데 몇 분 또는 몇 시간이 걸리는지.
- qemu-img convert 로 먼저 줄인 뒤 옮기는 편이 변환 시간까지 합쳐도 전체 시간을 줄이는지.
## 제약
- 링크가 WiFi 하나다. 이더넷이 없으므로 더 빠른 경로를 골라 견줄 수 없고, 잰 값이 이 호스트에서 낼 수 있는 값이다.
- 파일 크기를 확정하기 전에는 시간을 해석할 수 없다. 가상 크기와 실제 점유를 따로 적는 것이 먼저다.
- 게스트를 멈추는 동안 그 게스트가 하던 일도 멈춘다. 엣지를 고르면 밖에서 들어오는 요청이 끊기므로 대상과 시간대를 그에 맞춰 정한다.
- 한 번의 실측으로 닫는다. 반복해서 분포를 보는 것은 이 물음이 묻는 범위 밖이다.
## 선택지
### 1. 크기를 먼저 확정하고 그 파일 한 장을 한 번 옮긴다
qemu-img info 와 du 로 가상 크기와 실제 점유를 따로 적는다. 제4부에서 형식과 세 크기를 묻는 물음이 같은 출력을 쓰기 때문에 한 번만 돌려 두 물음이 나눠 쓴다. 그다음 게스트를 멈춘 상태에서 그 파일을 다른 기계로 복사하고 시작 시각과 종료 시각과 평균 전송률을 적는다.
옮기는 것이 파일 한 장이 아니다. §187 이 적은 이 실험대의 게스트 디스크 셋은 전부 base.qcow2 위의 오버레이라, 오버레이만 건너가면 헤더에 절대경로로 적힌 그 base 를 받는 쪽에도 같은 경로로 두어야 한다.
적어 두는 값은 네 가지다.
가상 크기 : qemu-img info 가 보여 주는 virtual size
실제 점유 : du 가 보여 주는 값
전송 시간 : 시작 시각과 종료 시각
평균 전송률 : 옮긴 바이트를 걸린 시간으로 나눈 값
### 2. qemu-img convert 로 줄인 사본을 같은 방법으로 한 번 더 옮긴다
1번에 이어서 변환에 걸린 시간과 줄어든 크기를 적고, 변환 시간까지 합친 총 시간을 1번과 견준다. 줄인 쪽이 더 빠르면 옮기기 전에 줄이는 것이 절차가 되고, 아니면 그대로 옮긴다.
### 3. 전송률만 재고 파일 크기로 나눠 시간을 계산한다 — 제외
큰 파일을 실제로 밀어 넣지 않아도 숫자가 나오지만, 계산한 시간은 잰 시간이 아니다. WiFi 는 오래 이어지는 전송에서 값이 흔들리고 옮기는 동안 디스크 읽기도 함께 걸린다. §183 이 물은 것은 현실적으로 몇 시간인가이므로 한 번은 끝까지 옮긴다.
## 다음 검증
1. 옮길 이미지 경로마다 qemu-img info 와 du 를 돌려 가상 크기와 실제 점유를 따로 적는다. 제4부에서 형식과 세 크기를 묻는 물음이 같은 출력을 쓴다.
2. 그 게스트를 멈추고 파일 한 장을 다른 기계로 복사한다. 시작 시각과 종료 시각과 평균 전송률을 적는다.
3. qemu-img convert 로 사본을 줄이고 걸린 시간과 줄어든 크기를 적는다.
4. 줄인 사본을 같은 방법으로 한 번 더 옮기고, 변환 시간까지 합친 총 시간을 2번과 견준다.
5. 출력 원문을 final/evidence/raw/ 에 남기고 meta/ 에 명령과 cwd 와 실행 시각과 종료 코드를 적는다.
닫는 조건 : 파일 크기와 전송 시간이 한 번의 실측으로 나오면 닫는다. 그 시간이 이 실험대를 다른 기계로 옮기거나 백업하는 것이 현실적인 절차인지, 아니면 기반 7단계 가이드를 다시 도는 재구축이 더 빠른지를 가른다. 재구축이 더 빠르다고 나오면 「단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다」가 요구하는 검증이 선택이 아니라 전제가 된다. 옮길 수 없는 실험대는 문서로만 복원된다.
@@ -0,0 +1,97 @@
---
kind: QUESTION
slug: virsh-save-ram-dump-size-and-time
title: virsh save 의 RAM 덤프 크기와 소요 시간은 할당 메모리에 어떻게 비례하는가
topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-2
- final/document.md#181-qcow2-가-담는-것과-담지-않는-것
- final/document.md#178-이-부의-출처와-범위
---
# virsh save 의 RAM 덤프 크기와 소요 시간은 할당 메모리에 어떻게 비례하는가
virsh save 가 만드는 RAM 덤프가 몇 바이트이고 save 와 restore 가 각각 몇 초 걸리는지 이 호스트에서 재지 않았다. §181 은 RAM 크기만큼 파일이 더 생긴다고만 적었다. 이 물음은 그 크기가 할당한 RAM 과 게스트가 실제로 쓰던 양 중 무엇에 가까운지를 가른다.
## 관계
- **qcow2 파일 한 장이 담는 것 — 매핑표와 데이터 클러스터가 같은 파일 안에 있고, 파일 밖을 가리키는 것은 백킹 파일 경로 하나다**
실행 상태가 왜 qcow2 에 없는지와 virsh save 가 그것을 어떻게 대신하는지를 그 기록이 설명한다. 이 물음이 재는 수치가 그 설명에서 비어 있는 한 줄을 채운다.
- **WiFi 전용 호스트에서 대용량 qcow2 를 옮기는 데 실제로 몇 시간이 걸리는가**
같은 이동을 두고 재는 값이 다르다. 저쪽은 다른 기계로 가는 파일 전송 시간이고, 이쪽은 같은 호스트에 새로 생기는 메모리 덤프의 크기와 시간이다.
- **balloon target 을 바꾸면 게스트가 쓸 수 있는 메모리는 어떻게 따라 움직이는가**
§183 이 대조군으로 쓰라고 적은 실사용값을 그 물음이 잰다.
- **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가**
덤프 크기를 견줄 설정값과 resident 값을 그 물음이 가상 머신마다 적는다.
- **virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식**
할당한 양과 게스트가 실제로 쓰는 양이 갈리는 구조를 그 기록이 설명한다.
## 사실
- §181 은 qcow2 를 통째로 옮겨도 실행 중인 프로세스(PID · FD · 소켓 · JVM 힙)와 페이지 캐시와 아직 내려가지 않은 dirty page 는 따라오지 않는다고 적었다. VM 정의 XML(vCPU · RAM · NIC · machine type · CPU 모델)도 파일 밖에 있다.
- §181 이 적은 실행 상태를 옮기는 길은 둘이다.
virsh save 로 내보내고 복사한 뒤 restore : VM 이 멈추고 RAM 크기만큼 파일이 더 생긴다
virsh migrate --live --copy-storage-all : 두 호스트 libvirt 가 붙고 CPU 모델이 호환돼야 한다
- §181 은 파일이 더 생긴다고만 적고 몇 바이트인지도 몇 초 걸리는지도 대지 않았다.
- §178 이 적은 이 실험대는 test-server 한 대다. Arch Linux, i5-1135G7 논리 코어 8, RAM 11,648MiB, QEMU 11.1.1 · libvirt 12.7.0 이고 게스트는 Debian 12 genericcloud 3대다. 그중 1대가 엣지이고 나머지 둘이 k3s 노드다.
- 게스트에 준 RAM 과 게스트가 실제로 쓰는 양은 갈린다. 그 구조를 제2부가 ballooning 으로 설명했고, 이 호스트에서 그 값을 잰 기록은 없다.
- §183 은 이 물음을 미측정으로 적으면서 「제2부의 balloon 실사용값과 대조하면 재미있는 대조군이 된다」고 덧붙였다. 두 물음을 하나로 합치지 않고 이어 둔 근거가 그 한 줄이다.
## 가정
- virsh save 가 만드는 파일이 한 장이라고 본다. §181 은 파일이 더 생긴다고만 적고 개수를 말하지 않았다.
- 저장하는 동안 호스트의 다른 부하가 크게 바뀌지 않는다고 전제한다. 소요 시간을 재는 것이라 같은 조건에서 두 번 이상 돌린다.
- 할당 RAM 을 바꾼 뒤에도 게스트가 같은 일을 하고 있다고 본다. 하는 일이 함께 바뀌면 덤프 크기가 할당량 때문에 달라진 것인지 부하 때문인지 가려지지 않는다.
- restore 가 성공한다고 전제한다. 실패하면 재는 것이 소요 시간이 아니라 실패 원인으로 바뀐다.
## 미지수
- 게스트 한 대를 virsh save 했을 때 생기는 파일이 실제로 몇 바이트인지.
- 그 크기가 할당한 RAM 과 같은지, 아니면 게스트가 실제로 쓰던 양에 가까운지.
- save 와 restore 가 각각 몇 초 걸리는지.
- 할당 RAM 을 바꾸면 그 셋이 어떻게 움직이는지.
## 제약
- 호스트 RAM 이 11,648MiB 다. 할당 RAM 을 크게 올려 가며 재는 데 한계가 있고, 덤프 파일이 놓일 디스크 공간도 같은 호스트에서 나온다.
- virsh save 는 VM 을 멈춘다. 멈추지 않는 이동은 이 물음의 대상이 아니고, §181 이 적은 다른 길인 virsh migrate 는 호스트 둘을 요구한다. 이 실험대는 한 대다.
- 엣지 게스트에는 nginx 와 certbot 이 올라가 있어 멈추면 밖에서 들어오는 요청이 끊긴다. 재는 대상은 k3s 노드 쪽에서 고른다.
- 이 물음은 크기와 시간까지만 본다. 그 시간이 실험대를 옮기는 절차로 쓸 만한지는 파일 전송 시간을 재는 물음과 함께 놓아야 갈린다.
## 선택지
### 1. 할당 RAM 을 둘 이상 두고 같은 게스트에 save 와 restore 를 돌린다
§183 이 물은 것을 그대로 재는 방법이다. 게스트 하나를 골라 지금 할당으로 한 번, 할당을 바꿔 한 번 더 돌린다. 매번 생긴 파일의 크기와 각 단계의 소요 시간을 적고, 같은 시각에 balloon 쪽 실사용값을 찍어 둔다. 두 점이 있어야 덤프 크기가 할당량 쪽인지 실사용량 쪽인지 갈린다.
그 대조군이 지금 나오는지는 따로 열려 있다. 제2부는 §83 의 OQ-8 로 virtio-balloon 이 이 가상 머신들에 구성되어 있는가부터 묻고 있어서, balloon 쪽 값을 찍으려면 그 확인이 먼저다.
한 번 돌릴 때마다 아래 넷을 적는다.
덤프 파일 크기 : 몇 바이트인가
save 소요 시간 : 몇 초인가
restore 소요 시간 : 몇 초인가
같은 시각의 balloon 실사용값 : 얼마인가
### 2. 지금 할당으로 한 번만 돌린다 — 제외
한 점으로는 크기가 할당량에 비례하는지 실사용량에 비례하는지 가려지지 않는다. 두 값이 우연히 비슷하게 나오면 어느 쪽인지 말할 도리가 없어서다.
### 3. 게스트 안에서 메모리를 채워 놓고 잰다 — 제외
실사용량을 올려 두면 두 값이 확실히 벌어지기 때문에 가르기 쉬워진다. 다만 그렇게 재면 게스트가 하는 일이 평소와 달라지기 때문에, 이 실험대를 실제로 멈췄다 세우는 데 드는 시간과는 다른 값이 나온다. 부하를 준 상태의 측정은 이 물음이 닫힌 뒤에 따로 둔다.
## 다음 검증
1. 재는 대상 게스트를 고르고 그 시점의 할당 RAM 과 balloon 쪽 실사용값을 먼저 적는다.
2. virsh save 를 돌리고 걸린 시간과 생긴 파일의 크기를 적는다.
3. restore 를 돌리고 걸린 시간을 적는다. 게스트가 원래 하던 일을 그대로 이어 가는지도 본다.
4. 같은 게스트의 할당 RAM 을 바꿔 1번부터 다시 돌린다. 두 번 이상 반복한다.
5. 찍은 출력은 final/evidence/raw/ 에 원문 그대로 남기고, meta/ 에 명령과 cwd 와 실행 시각과 종료 코드를 적는다.
닫는 조건 : 할당 RAM 두 값 이상에서 덤프 크기와 소요 시간이 나오고, 그것이 할당량과 실사용량 중 어느 쪽으로 움직이는지 말할 수 있으면 닫는다. 값이 나오면 이 실험대를 멈췄다 다시 세우는 데 드는 시간이 정해지고, 「RAM 크기만큼 파일이 더 생긴다」고만 적힌 qcow2 개념 기록에 이 호스트의 실제 수치가 들어간다. 덤프가 실사용량 쪽으로 움직인다고 나오면 제2부의 balloon 기록이 반대 방향의 근거를 하나 얻는다.