Files
DongHyeonkaandClaude Opus 5 2109f726fe 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>
2026-09-17 11:01:55 +09:00

342 lines
26 KiB
Markdown

---
id: 5b1de31c-0495-4226-8607-73ed7284b314
kind: CONCEPT
slug: guest-packet-path-to-physical-nic
title: Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge
topic: network-virtualization
topicName: 네트워크 가상화
project: virtualization
status: 초안
basisVersion: Linux KVM/QEMU/libvirt · virtio-net frontend · vhost-net kernel backend · TAP · Linux Bridge
studio: "https://hyeonworks.com/studio/documents/5b1de31c-0495-4226-8607-73ed7284b314/edit"
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
assets:
- key: packet-control-and-data-paths
file: ../../../final/assets/diagrams/packet-control-and-data-paths/packet-control-and-data-paths.svg
source:
- final/document.md#94-전체-네트워크-계층
- final/document.md#121-이-ssot에서-파생될-concept
- final/document.md#125-최종-기준-구조
- final/document.md#89-문서-목적
- final/document.md#90-virsh-libvirt-virtio-구분
- final/document.md#91-virtio-net은-정확히-어디에-있는가
- final/document.md#92-frontend와-backend
- final/document.md#93-guest-os는-왜-qemu가-아니라-virtio-net을-사용하는가
- final/document.md#95-physical-nic의-역할
- final/document.md#96-linux-bridge의-역할
- final/document.md#97-routing의-역할
- final/document.md#98-nat의-역할
- final/document.md#99-tap의-역할
- final/document.md#100-virtqueue의-역할
- final/document.md#101-guest-tcp-ip-stack의-역할
- final/document.md#102-packet이-keycloak까지-올라오는-과정
- final/document.md#103-qemu-virtio-device-model의-역할
- final/document.md#104-왜-tap-→-vhost-net-→-qemu-→-virtqueue-라고-일반화하면-안-되는가
- final/document.md#105-control-path와-data-path
- final/document.md#106-qemu가-userspace인데-packet이-qemu를-안-거칠-수-있는-이유
- final/document.md#107-vhost-net-최적화
- final/document.md#108-vhost-net은-qemu를-제거하지-않는다
- final/document.md#109-fast-path와-slow-control-path
- final/document.md#110-data-copy-최적화
- final/document.md#111-interrupt-notification-최적화
- final/document.md#112-multi-queue-최적화
- final/document.md#113-offload-최적화
- final/document.md#114-linux-bridge가-항상-host-tcp-ip-stack을-거치는-것은-아니다
- final/document.md#115-host-physical-nic로-나갈-때-virtio를-다시-거치지-않는다
- final/document.md#117-이-구조에서-발생할-수-있는-문제-117-4
- final/document.md#117-이-구조에서-발생할-수-있는-문제-117-5
- final/document.md#124-핵심-claim
---
# Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge
가상 머신 안의 Keycloak 이 HTTP 요청 하나를 받으려면 그 패킷이 호스트의 물리 NIC 에서 Linux Bridge 와 TAP, vhost-net, virtqueue 를 지나 게스트 커널의 TCP/IP 스택까지 올라와야 한다. virtio-net 은 그 경로에 놓인 프로그램 하나가 아니라 게스트 쪽 프런트엔드 드라이버와 호스트 쪽 백엔드를 잇는 I/O 계약이고, 그 백엔드 자리는 QEMU 사용자 공간이 맡을 수도 호스트 커널의 vhost-net 이 맡을 수도 있다. 가상 머신 두 대로 Keycloak 멀티 노드 실험을 돌리다 요청이 느려지거나 아예 닿지 않으면, 이 경로를 알아야 그 증상을 애플리케이션·저장소 쪽 문제와 네트워크 가상화 계층 문제로 갈라 볼 수 있다.
## 관계
- **packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다**
이 글이 세운 계층을 그대로 관측 지점으로 쓰는 절차다. 어느 계층까지 패킷이 보였는지로 의심 구간을 좁힌다.
- **Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다**
이 경로가 실험의 여러 노드에 공유되기 때문에 생기는 오귀속을 막는 기준이다.
- **이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가**
이 글은 Bridge 와 라우팅, NAT 가 각각 무엇을 하는지까지만 적었다. 이 호스트가 NAT 로 돈다는 것은 실험대를 세운 기록이 나중에 적었고, 그 구성에서 프레임이 어느 계층을 지나는지는 그 물음이 받는다.
- **VM1 과 VM2 의 TAP/vnet interface 는 무엇이고 어디에 붙어 있는가**
TAP 이 게스트의 이더넷 프레임과 호스트 네트워크를 잇는 접점이라는 설명이 이 물음의 전제다.
- **이 호스트에서 vhost-net 이 실제로 packet datapath 를 맡고 있는가**
백엔드가 QEMU 사용자 공간일 수도 vhost-net 일 수도 있다고 적었다. 이 호스트가 어느 쪽인지는 아직 모른다.
- **QEMU backend 와 vhost-net 의 차이가 이 호스트에서 실제로 보이는가**
두 백엔드의 경로 차이는 여기 적었지만, 그 차이가 지연이나 CPU 사용량으로 드러나는지는 재지 않았다.
- **이 가상 머신들의 virtio-net 에 multi-queue 가 켜져 있는가**
큐를 하나만 쓰면 부하가 몰릴 수 있다고만 적었다. 지금 큐가 몇 개인지는 확인하지 않았다.
- **Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가**
여기 그린 경로는 가장 기본적인 조합을 기준으로 한 것이고, 이 테스트 환경의 실제 경로는 추적하지 않았다.
- **부하 중 network 가상화가 지연을 바꿀 만큼 호스트 CPU 를 쓰는가**
vhost-net 과 QEMU 스레드, softirq 도 호스트 CPU 를 쓴다는 것까지가 이 글의 범위다.
- **KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지**
QEMU 가 장치를 만들지만 반복 실행은 커널이 맡는다는 구조가 CPU 쪽과 같다. 그쪽 글이 같은 구분을 명령 실행으로 설명한다.
## 본문
<!-- body:start -->
## virtio-net 은 어디에 있는가
virtio 는 명령어가 아니고 프로그램 하나도 커널 모듈 하나도 아니다. 게스트와 호스트/하이퍼바이저가 가상 I/O 장치를 효율적으로 쓰려고 정해 둔 표준화된 인터페이스이자 프로토콜이다. 그 계열에는 네트워크의 virtio-net 과 블록 I/O 의 virtio-blk 가 있다. SCSI(Small Computer System Interface) 의 virtio-scsi 와 메모리 벌룬의 virtio-balloon 도 같은 계열이고, 이 글은 그중 virtio-net 을 다룬다.
가상 NIC 를 성립시키는 구현은 게스트와 호스트로 나뉘어 있다. 게스트 커널 쪽에는 TCP/IP 스택과 virtio-net 프런트엔드 드라이버, virtqueue 가 들어 있다.
```text label="Guest 측"
Guest Kernel
├─ TCP/IP Stack
├─ virtio-net Frontend Driver
└─ virtqueue
```
호스트 쪽은 사용자 공간과 커널로 다시 갈린다. QEMU 프로세스 안에 virtio-net 장치 모델이 있고, 커널에는 vhost-net 과 TAP, Linux Bridge 와 라우팅·NAT, 물리 NIC 드라이버가 있다.
```text label="Host 측"
Host Userspace
└─ QEMU virtio-net Device Model
Host Kernel
├─ vhost-net (사용하는 경우)
├─ TAP
├─ Linux Bridge / Routing / NAT
└─ Physical NIC Driver
```
그래서 virtio 는 특정 커널 계층 자체가 아니라 게스트 쪽 프런트엔드(frontend)와 호스트 쪽 백엔드(backend) 사이의 I/O 계약이다. 프런트엔드는 게스트 커널의 virtio-net 드라이버이고, 백엔드는 게스트가 넘긴 패킷 버퍼를 호스트 쪽에서 처리하는 구현이다. 이 백엔드 자리에는 QEMU 사용자 공간이 올 수도 있고 호스트 커널의 vhost-net 이 올 수도 있다.
둘 사이에 놓인 virtqueue 는 NIC 도 아니고 Linux 네트워크 인터페이스도 아니다. 게스트와 호스트 백엔드가 I/O 버퍼를 주고받으려고 쓰는 디스크립터 기반 공유 큐다. 네트워크에서는 보통 게스트에서 호스트로 가는 TX(transmit) virtqueue 와 호스트에서 게스트로 가는 RX(receive) virtqueue 를 쓴다. 페이로드를 매번 전통적인 사용자 공간 API 호출로 넘기는 방식이 아니라, 게스트 메모리의 버퍼와 디스크립터를 효율적으로 공유하고 참조하도록 설계돼 있다.
게스트 입장에서는 이 구조가 보이지 않는다. 가상 머신을 시작할 때 QEMU 가 가상 PCI(Peripheral Component Interconnect) 버스에 virtio NIC 를 노출한다. 그러면 게스트 Linux 가 그 장치를 발견하고 virtio-net 드라이버를 붙여 `ens3` 나 `eth0` 같은 인터페이스를 만든다. 그다음부터 게스트는 QEMU 를 호출한다고 동작하지 않고 자기 NIC 를 쓴다고 동작한다.
## 장치를 만드는 쪽과 패킷을 나르는 쪽
`virsh` 는 사용자가 libvirt 에 가상 머신 관리 명령을 전달하는 명령줄 도구여서 패킷이 지나는 길에는 직접 참여하지 않는다. libvirt 는 그 아래에서 vCPU 와 메모리, 디스크, NIC 모델, MAC 주소, 가상 네트워크, 브리지, QEMU 인자 같은 가상 머신의 수명 주기와 구성을 관리한다. 실제 프로세스로 실행되는 것은 QEMU 다.
QEMU 안의 virtio 장치 모델이 하는 일은 둘로 갈라서 봐야 뒤가 이어진다. 하나는 장치를 만들고 설정하고 관리하는 일이다. virtio-net 장치 모델을 만들어 게스트에게 노출하고, 기능 협상(feature negotiation)을 하고, virtqueue 를 설정하고, 백엔드를 연결한다. 다른 하나는 패킷을 실제로 나르는 처리인데, 이쪽은 QEMU 가 맡을 수도 맡지 않을 수도 있다. QEMU 백엔드를 직접 쓰면 패킷이 QEMU 를 지난다.
```text label="QEMU backend 를 직접 사용하는 경우"
TAP
QEMU virtio backend
virtqueue
Guest
```
vhost-net 을 쓰면 반복되는 패킷 I/O 를 호스트 커널에서 처리하고 QEMU 사용자 공간을 우회한다.
```text label="vhost-net 을 사용하는 경우"
TAP
vhost-net
virtqueue
Guest
```
이 구분에는 경로 이름이 따로 붙어 있다. 설정 경로(control/setup path)와 데이터 경로(data path)다. 여기서 control 은 Kubernetes 의 Control Plane 을 뜻하지 않고, 일반적인 시스템 용어로 설정·제어 경로를 가리킨다. `virsh` 에서 libvirt 와 QEMU, virtio-net 장치 모델로 내려가면서 기능 협상과 virtqueue 설정, vhost-net 설정이 일어나는 쪽이 설정 경로다. 그 설정이 끝난 뒤 패킷이 반복해서 흐르는 쪽이 데이터 경로다.
## 밖에서 들어온 패킷이 Keycloak 까지 올라오는 순서
가장 기본적인 virtio-net 과 vhost-net, TAP, Linux Bridge 조합을 기준으로 하면 수신 방향은 이 순서다.
```text label="수신 방향"
Internet / Client
Physical NIC
Physical NIC Driver
Linux Bridge / Routing / NAT
TAP
vhost-net
RX virtqueue
virtio-net Frontend Driver
Guest TCP/IP Stack
Socket
Keycloak
```
송신은 이 순서를 거꾸로 밟는다. Keycloak 이 소켓에 쓰면 게스트 TCP/IP 스택과 virtio-net 프런트엔드 드라이버를 지나 TX virtqueue 에 실린다. 거기서부터는 vhost-net 과 TAP, Linux Bridge 와 라우팅·NAT, 물리 NIC 드라이버를 차례로 지나 물리 NIC 로 나간다.
맨 앞의 물리 NIC(Network Interface Card, 네트워크 인터페이스 카드) 는 실제 네트워크 링크와 서버를 잇는 하드웨어다. 그 하드웨어를 호스트 커널의 드라이버가 제어하고 Linux 가 인터페이스로 노출하기 때문에, `ip link` 에 보이는 `enp3s0` 같은 인터페이스와 NIC 하드웨어를 같은 것으로 세지 않는다.
그다음의 Linux Bridge 는 호스트 커널 안에 있는 L2 소프트웨어 스위치다. 이더넷 프레임의 목적지 MAC(Media Access Control) 주소를 보고 어느 포트로 넘길지 정하고, MAC 주소를 학습하고, 여러 가상·물리 포트를 잇는다.
라우팅은 다른 계층에서 다른 값을 본다. 브리지가 L2 에서 MAC 주소를 보고 같은 이더넷 네트워크를 잇는다면, 라우팅은 L3 에서 목적지 IP(Internet Protocol) 주소를 보고 어느 인터페이스나 next-hop 으로 패킷을 보낼지 정한다. NAT(Network Address Translation) 는 또 다른 일을 해서 패킷의 IP 와 포트 정보를 바꾼다. 가상 머신이 사설 서브넷을 쓰면 호스트가 NAT 게이트웨이처럼 동작할 수 있다.
경로 그림에서는 셋이 한 줄에 나란히 적히지만 보는 값도 바꾸는 값도 서로 다르다. 그래서 실제 가상 머신 네트워크를 분석할 때는 브리지 기반인지 라우팅 기반인지 NAT 기반인지를 먼저 가른다.
TAP 은 호스트 Linux 커널이 제공하는 가상 이더넷 인터페이스이고 물리 장치가 아니다. `tap0` 나 `vnet0` 같은 이름으로 보이며, 가상 머신의 이더넷 프레임과 호스트 Linux 네트워크를 잇는 접점이 된다. 수신할 때는 Linux Bridge 에서 TAP 을 거쳐 가상 머신으로 들어가고, 송신할 때는 가상 머신에서 TAP 을 거쳐 Linux Bridge 로 나온다.
TAP 을 지나 게스트 안으로 들어오면 그다음은 평범한 Linux 네트워크 스택이다. 게스트의 TCP/IP 스택도 게스트 Linux 커널의 실제 스택이어서, 게스트 커널에는 소켓과 TCP, UDP, IP, 라우팅, neighbor/ARP, 방화벽, 네트워크 드라이버가 그대로 있다. TCP 는 연결 관리와 포트, 시퀀스, 순서 보장, 재전송, 중복 처리, 흐름 제어, 혼잡 제어를 맡는다. IP 계층은 IP 주소와 라우팅을 담당하고, NIC 에 가까운 계층에서는 이더넷 프레임과 MAC 주소를 다룬다.
그 위에서 애플리케이션이 보는 것은 소켓 하나다. 이더넷 프레임이 IP 패킷과 TCP 세그먼트를 지나 소켓에 닿고 그 위에서 HTTP 로 읽히면 Keycloak 이 요청을 처리한다.
```text label="Keycloak 이 요청 하나를 받기까지의 계층"
Ethernet Frame
IP Packet
TCP Segment / Stream
Socket
HTTP
Keycloak
```
그래서 Keycloak 은 virtqueue 와 vhost-net, TAP, Bridge, 물리 NIC 를 직접 알 필요가 없고, 게스트 Linux 가 주는 TCP 소켓 위에서만 동작한다. 아래쪽 계층이 어긋나도 애플리케이션 로그에는 느렸다는 것과 실패했다는 것만 남는 이유가 여기에 있다.
## vhost-net 이 우회하는 것
QEMU 사용자 공간이 패킷마다 I/O 를 처리하면 호스트 커널과 사용자 공간을 오가는 전환 비용이 쌓인다. 초당 지나는 패킷이 많아질수록 그 전환과 스케줄링, 복사, 알림 비용이 커질 수 있다. vhost-net 은 그 반복 경로를 호스트 커널 쪽으로 옮겨서 컨텍스트 스위치와 사용자 공간 오버헤드를 줄인다.
QEMU 가 장치를 만들고 관리하는 주체라는 것과, 흐르는 패킷을 전부 QEMU 가 처리한다는 것은 다른 말이다. CPU 가상화에서 같은 구분을 이미 한 번 한다. QEMU 가 vCPU 를 만들지만 게스트의 `ADD`, `MOV`, `SUB` 를 전부 QEMU 가 실행하지는 않는다. 그 명령은 KVM(Kernel-based Virtual Machine) 과 VMX(Virtual Machine Extensions) 가 실행한다. 네트워크에서도 QEMU 가 가상 NIC 를 만들지만, 패킷 100만 개를 반드시 QEMU 가 하나씩 처리할 필요는 없다.
vhost-net 을 써도 QEMU 는 계속 필요하다. 가상 머신 수명 주기와 가상 하드웨어 모델, virtio 장치 생성, 기능 협상, 큐 설정, 백엔드 연결, 장치 reset, 설정 처리가 QEMU 에 남는다. 그래서 vhost-net 은 QEMU 를 없애는 것이 아니라, QEMU 가 맡던 반복적인 virtio 데이터 경로의 상당 부분을 호스트 커널로 넘긴다고 읽는다.
두 경로에는 이름이 따로 있다. 자주 반복되는 패킷 전달과 데이터 전송이 fast path 다. 장치 초기화와 기능 협상, 큐 설정, 설정 변경, 장치 reset 처럼 드물게 일어나면서 설정과 예외 처리를 맡는 쪽이 control/slow path 다. QEMU 는 뒤쪽에서 계속 중요한 역할을 한다.
![두 줄이 나란히 내려오다 아래에서 만나는 구성도. 왼쪽 줄은 virsh 에서 libvirt 와 QEMU 를 지나 virtio-net Device Model 로 내려가는 control/setup path 이고 오른쪽 줄은 Physical NIC 에서 Physical NIC Driver 와 Linux Bridge, TAP 으로 내려가는 data path 다. Device Model 은 점선으로 vhost-net 과 QEMU virtio backend 를 설정하고 TAP 은 실선으로 그 둘 가운데 한쪽에 packet 을 넘긴다. 두 backend 는 다시 virtqueue 하나로 모여 Guest 로 올라간다.](../../../final/assets/diagrams/packet-control-and-data-paths/packet-control-and-data-paths.svg)
## 이 경로를 일반화할 때 어긋나는 것
경로를 한 줄로 굳혀 놓으면 틀리는 그림이 셋 있다. 첫째는 패킷이 vhost-net 다음에 QEMU 를 반드시 지나는 것처럼 그린 것이다.
```text label="이렇게 일반화하지 않는다"
TAP
vhost-net
QEMU
virtqueue
```
vhost-net 의 중요한 목적 하나가 데이터 경로에서 QEMU 사용자 공간을 우회하는 데 있다. 그래서 vhost-net 을 쓸 때의 fast path 는 TAP 에서 vhost-net 과 virtqueue 를 지나 게스트로 간다. QEMU 는 사라지지 않고 장치의 수명 주기와 설정을 관리한다.
둘째는 Bridge 를 지나는 프레임이 항상 호스트의 TCP/IP 스택을 거친다고 본 것이다. Bridge 가 L2 전달만 하는 경우 프레임이 호스트의 일반적인 L3 TCP/IP 스택을 반드시 거치지는 않는다. VM1 의 TAP 에서 Linux Bridge 를 지나 VM2 의 TAP 으로 가는 경로가 그렇다. 호스트가 라우팅이나 NAT, host-local termination, 방화벽 역할을 하면 그때 L3/Netfilter 경로가 개입한다. 그래서 물리 NIC 에서 호스트 TCP/IP 스택을 거쳐 Bridge 로 간다는 순서를 고정된 경로로 두지 않는다. 실제 경로는 브리지와 라우팅, NAT 구성에 따라 달라진다.
셋째는 호스트 물리 NIC 로 나갈 때 virtio 를 한 번 더 거친다고 본 것이다.
```text label="Host 물리 NIC 로 나가는 경로"
Guest
virtio-net
vhost-net
TAP
Linux Bridge
Intel NIC Driver
Intel Physical NIC
```
게스트 virtio 에서 호스트 virtio 를 거쳐 물리 NIC 로 가는 구조가 아니다. virtio 는 게스트의 가상 I/O 장치와 호스트 백엔드 사이의 인터페이스이고, 물리 NIC 로 나갈 때는 그 카드의 실제 드라이버를 쓴다.
## 복사와 알림, 큐 개수와 오프로드
네트워크 성능에서 큰 비용 하나가 패킷 데이터를 복사하는 일이다. virtio 와 virtqueue, vhost 구조는 버퍼 디스크립터를 써서 불필요한 복사와 컨텍스트 스위치를 줄이는 방향으로 설계돼 있다. 다만 이것을 항상 zero-copy 라고 일반화하지 않는다. 실제로 복사가 일어나는지는 커널 버전과 QEMU 버전, vhost 설정, 오프로드, NIC 의 기능, 패킷이 지나는 경로, GSO/GRO/TSO 에 따라 달라질 수 있다.
게스트와 호스트는 큐에 새 패킷이나 버퍼가 들어왔다는 것을 서로 알려야 한다. 게스트가 보낼 때는 virtqueue 에 디스크립터를 등록하고 호스트 백엔드에 알리면 백엔드가 처리한다. 받을 때는 호스트가 virtqueue 에 버퍼를 반영하고 게스트에 알리면 게스트 드라이버가 처리한다. 패킷마다 인터럽트와 알림이 지나치게 많이 발생하면 오버헤드가 커지기 때문에 batching 과 interrupt moderation, queueing 을 쓴다.
큐를 하나만 쓰면 특정 vCPU 나 처리 경로에 부하가 몰릴 수 있다. virtio-net 은 multi-queue 를 쓸 수 있고, RX 큐를 vCPU 마다 하나씩 붙여 패킷 처리를 병렬로 돌리고 큐 하나에 몰리는 병목을 완화한다. 효과는 부하의 성격과 CPU affinity, IRQ(Interrupt Request) 배치, 큐 설정에 따라 달라진다.
오프로드는 작은 패킷을 하나씩 처리하는 CPU 부담과 나누고 합치는 비용을 줄인다. TSO(TCP Segmentation Offload)와 GSO(Generic Segmentation Offload), GRO(Generic Receive Offload), 그리고 checksum offload 가 대표적이다. 오프로드가 켜져 있으면 `tcpdump` 에서 보이는 패킷 크기나 checksum 이 실제 wire 에서 보이는 것과 다르게 보일 수 있다.
## 이 구조에서 성능이 갈리는 두 조건
초당 지나는 패킷이 많은데 QEMU 사용자 공간이 데이터 경로를 직접 처리하면 CPU 부담이 커질 수 있다. 그때는 아래 다섯 가지를 함께 본다.
```text label="vhost-net 미사용 또는 비효율적 datapath 일 때 관찰"
QEMU CPU usage
vhost thread
packet rate
latency
context switch
```
큐 하나나 vCPU 하나에 패킷 처리가 몰릴 수도 있다. 이때는 큐 개수와 분산 설정을 확인 대상으로 둔다.
```text label="single queue bottleneck 일 때 확인 대상"
virtio multi-queue
IRQ distribution
per-vCPU CPU usage
RSS/RPS/XPS
```
두 조건 모두 호스트 CPU 를 함께 쓴다. vhost-net 과 QEMU 스레드, softirq 가 호스트 CPU 를 쓰기 때문에, 네트워크 문제처럼 보이는 지연이 실은 CPU 스케줄링 문제일 수도 있다.
## 호스트에서 이 경로를 확인하는 명령
여기까지가 구조이고, 이 호스트가 실제로 그 구조인지는 명령으로 하나씩 확인한다. 물리 NIC 와 Bridge 는 인터페이스 목록과 전달 상태로 본다.
```bash label="Physical NIC 와 Linux Bridge"
ip link
ip addr
ethtool <interface>
ip link show type bridge
bridge link
bridge fdb show
```
TAP 과 가상 머신의 NIC 연결, libvirt 가상 네트워크는 libvirt 쪽에서 확인한다. `virsh domiflist` 가 어느 가상 머신이 어느 TAP 에 붙어 있는지 알려 주고, `virsh net-dumpxml` 이 그 네트워크가 어떤 구성인지 알려 준다.
```bash label="TAP 과 libvirt VM NIC · 가상 network"
ip tuntap show
virsh domiflist <domain>
virsh net-list --all
virsh net-info <network>
virsh net-dumpxml <network>
```
라우팅과 게스트 안쪽, 그리고 virtio 장치와 vhost 모듈이 실제로 올라와 있는지는 각각 따로 본다.
```bash label="Routing · Guest NIC · virtio 와 vhost 모듈"
ip route
ip rule
ip neigh
lspci
lsmod | grep virtio
lsmod | grep vhost
```
경로 위의 어느 지점까지 패킷이 보이는지는 계층마다 캡처해서 가른다.
```bash label="계층마다 packet 이 보이는지"
sudo tcpdump -ni <physical-nic>
sudo tcpdump -ni <bridge>
sudo tcpdump -ni <tap-or-vnet>
sudo tcpdump -ni <guest-interface>
```
## 이 문서가 확인하지 않은 것
위의 명령을 이 호스트에서 돌린 출력은 대부분 없다. 네트워크 구성만은 나중에 갈렸다. 실험대를 세운 기록이 이 호스트에 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT 를 골랐다고 적었고, 게스트는 그 NAT 가 만드는 브리지에 붙는다. 브리지를 막은 것은 무선 링크다. 802.11 데이터 프레임은 기본적으로 주소 필드가 3개라, AP(Access Point, 무선 접속 장치) 는 연결된 단말의 MAC 만 알고 있고 그 단말이 자기 것이 아닌 출발지 MAC 을 단 프레임을 보내면 버린다. 브리지된 가상 머신이 보내는 것이 정확히 그런 프레임이다. 우회로 4-address 모드(WDS) 가 있지만 AP 와 클라이언트 드라이버가 모두 지원해야 하고 실제로는 거의 지원되지 않아, 그 기록은 현실적인 우회로 USB 이더넷 어댑터를 들었다. 같은 기록이 NAT 와 브리지와 macvtap 을 견준 표에서 뒤의 둘은 나란히 WiFi 라 불가다. TAP 또는 vnet 인터페이스의 이름과 vhost-net 이 실제로 데이터 경로를 맡는지, multi-queue 가 켜져 있는지는 아직 읽지 않았다. 그래서 이 글은 「이 구조에서는 이렇게 동작한다」까지만 말하고 「이 서버가 그 구조다」라고는 말하지 않는다. QEMU 백엔드와 vhost-net 의 성능 차이가 이 호스트에서 관찰되는지, Keycloak 부하 시험에서 네트워크 가상화가 지연에 영향을 줄 만큼 호스트 CPU 를 쓰는지도 재지 않았다.
이 문서가 Keycloak 테스트 환경에 얹어 그린 경로는 그대로 쓰지 못한다. 그 그림은 호스트의 nginx 가 가상 머신 두 대로 프록시하는 모양인데, 실험대는 그 뒤에 nginx 를 게스트 한 대로 옮기고 호스트에는 커널 DNAT(Destination NAT, 목적지 주소 변환) 만 두었다. 게스트도 둘이 아니라 셋이고, 엣지 한 대와 K3s 노드 두 대다. 브리지와 라우팅·NAT, TAP, vhost-net, virtqueue 로 펼친 뒷부분은 TAP 이름과 vhost-net 사용 여부를 확인하기 전의 가정이어서, 재기 전에는 이 글의 그림으로 올리지 않는다.
여기 적은 것은 가장 기본적인 조합 안에서만 성립한다. 실제 환경은 Bridge 와 NAT, routed network, macvtap, SR-IOV, VFIO(Virtual Function I/O) passthrough, Open vSwitch, Kubernetes CNI(Container Network Interface) 에 따라 달라질 수 있다. 이 서술 자체는 판 번호를 달고 있지 않다. 실험대를 잰 기록이 그 호스트의 libvirt 12.7.0 과 QEMU 11.1.1, 커널 7.2.2-arch1-1 을 적었다. 여기 적은 동작은 그 판에서 읽은 것이 아니라 구조를 서술한 것이라, 세 값은 확인할 때의 조건으로 쓴다. 복사 동작 하나만 봐도 커널 버전과 QEMU 버전, vhost 설정, 오프로드, NIC 의 기능에 따라 달라지므로, 버전이 바뀌어 이 서술이 낡았는지는 위 확인 명령을 실제 호스트에서 돌릴 때 드러난다.
가상 머신 위에 올린 K3s 안쪽도 이 문서 밖이다. K3s 의 CNI 와 Service, Pod 네트워크는 이 네트워크 가상화 위에 더해지는 계층이라 따로 분석한다.
확인 순서는 물리 NIC 에서 시작한다. 그다음 libvirt 가상 네트워크와 Bridge·NAT·Route 구성, 가상 머신별 TAP 또는 vnet, virtio-net 장치, vhost-net 사용 여부, 게스트 NIC 와 route 를 차례로 본다. 이어서 호스트 Nginx 에서 가상 머신까지 패킷이 지나는 길을 `tcpdump` 로 추적하고, VM1 과 VM2 사이의 경로와 Keycloak 요청 때의 패킷 흐름을 확인한다. 마지막으로 부하를 걸었을 때 QEMU 와 vhost 의 CPU 사용량을 비교하고 multi-queue 와 오프로드 설정을 본다. 검증되지 않은 항목은 열린 질문으로 두고, 실제 결과가 나오면 재현 조건을 붙인 기록으로 옮긴다.
<!-- body:end -->