기록 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>
342 lines
26 KiB
Markdown
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 는 뒤쪽에서 계속 중요한 역할을 한다.
|
|
|
|

|
|
|
|
## 이 경로를 일반화할 때 어긋나는 것
|
|
|
|
경로를 한 줄로 굳혀 놓으면 틀리는 그림이 셋 있다. 첫째는 패킷이 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 -->
|