QEMU 공유 파일시스템 virtiofs vs virtio-9p — 설정과 실측 비교

게스트에 소스를 올려 빌드하거나 호스트가 만든 산출물을 게스트에서 바로 읽으려면 디스크 이미지에 복사하는 것보다 호스트 디렉터리를 그대로 공유하는 쪽이 편하다. QEMU에는 이걸 하는 방법이 두 가지 있다. 오래된 virtio-9p(-virtfs)와, FUSE 프로토콜을 virtio로 실어 보내는 virtiofs다. 둘은 성능뿐 아니라 게스트가 보는 소유권과 권한까지 다르게 동작한다. 이 글에서는 한 VM에 두 공유를 동시에 붙여 처리량·메타데이터 연산·소유권 매핑을 같은 조건에서 비교한다.

두 방식의 구조 차이

항목virtio-9pvirtiofs
프로토콜9P2000.L (네트워크 파일 프로토콜)FUSE 요청을 virtio 큐로 전달
호스트 측 구현QEMU 프로세스 안별도 virtiofsd 데몬(vhost-user)
QEMU 장치virtio-9p-pcivhost-user-fs-pci
공유 메모리 요구없음필요 (memory-backend-memfd,share=on)
게스트 마운트mount -t 9pmount -t virtiofs
소유권 처리security_model로 선택호스트 uid/gid 그대로
$ qemu-system-x86_64 --version | head -1
QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.17)
$ qemu-system-x86_64 -device help | grep -iE 'vhost-user-fs|virtio-9p'
name "vhost-user-fs-device", bus virtio-bus
name "vhost-user-fs-pci", bus PCI
name "virtio-9p-device", bus virtio-bus
name "virtio-9p-pci", bus PCI, alias "virtio-9p"

호스트 준비

virtiofsd는 QEMU와 별도 패키지다. 설치 후 소켓 하나를 만들고 공유할 디렉터리를 지정해 띄워둔다.

$ sudo apt install virtiofsd          # Ubuntu 24.04: /usr/libexec/virtiofsd
$ virtiofsd --version
virtiofsd 1.10.0

$ mkdir -p ~/share && echo "hello from host" > ~/share/hello.txt
$ mkdir -p ~/share/sub && seq 1 100 > ~/share/sub/numbers.txt
$ dd if=/dev/urandom of=~/share/big.bin bs=1M count=64 status=none

$ virtiofsd --socket-path=/tmp/vfs.sock --shared-dir=$HOME/share \
            --cache=auto --sandbox=none --xattr &
[INFO  virtiofsd] Waiting for vhost-user socket connection...

--sandbox=none은 root 권한 없이 돌리려고 준 것이다. 기본 샌드박스는 네임스페이스를 만들어야 해서 권한이 필요하다.

QEMU에 두 공유를 동시에 붙이기

virtiofs는 vhost-user-fs-pci가 게스트 메모리에 직접 접근하므로 공유 가능한 메모리 백엔드가 필수다. 이것 없이 띄우면 장치 초기화가 실패한다.

qemu-system-x86_64 \
  -machine q35 -accel tcg -cpu max -m 2048 -smp 4 \
  -object memory-backend-memfd,id=mem,size=2G,share=on \
  -numa node,memdev=mem \
  -drive file=overlay.qcow2,if=virtio,format=qcow2 \
  -netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
  -chardev socket,id=char0,path=/tmp/vfs.sock \
  -device vhost-user-fs-pci,chardev=char0,tag=myfs \
  -virtfs local,path=$HOME/share,mount_tag=hostshare,security_model=mapped-xattr,id=h0 \
  -nographic -serial file:console.log -monitor none

게스트에서 각각 마운트한다. 9p는 msize를 키워야 처리량이 나오는데, 커널이 값을 조정할 수 있다.

# 게스트 안
$ mount -t virtiofs myfs /mnt/vfs
$ mount -t 9p -o trans=virtio,version=9p2000.L,msize=524288 hostshare /mnt/9p
$ mount | grep -E 'virtiofs|9p'
myfs on /mnt/vfs type virtiofs (rw,relatime)
hostshare on /mnt/9p type 9p (rw,relatime,access=client,msize=512000,trans=virtio)

요청한 msize=524288이 512000으로 내려갔다. 커널이 전송 단위에 맞춰 깎은 값이다.

$ cat /mnt/vfs/hello.txt
hello from host
$ ls /mnt/vfs
big.bin  hello.txt  sub

처리량과 메타데이터 연산 비교

게스트에서 같은 스크립트로 두 마운트를 차례로 측정했다. 측정 환경에 KVM 접근 권한이 없어 TCG(소프트웨어 에뮬레이션)로 돌렸으므로 절대 수치는 KVM 환경을 대표하지 않고, 두 방식이 동일 조건에서 받는 상대 차이만 본다.

for m in /mnt/vfs /mnt/9p; do
    sync; echo 3 > /proc/sys/vm/drop_caches
    dd if=$m/big.bin of=/dev/null bs=1M count=64
    dd if=/dev/zero of=$m/w.bin bs=1M count=32 conv=fsync
    # 파이썬으로 200개 생성 / stat / 삭제 시간 측정
done
===== /mnt/vfs =====  (virtiofs)
  seq read 64M : 67108864 bytes (67 MB, 64 MiB) copied, 2.43277 s, 27.6 MB/s
  seq write 32M: 33554432 bytes (34 MB, 32 MiB) copied, 0.944685 s, 35.5 MB/s
  create 200    : 0.398 s
  stat 200      : 0.053 s
  rmtree 200    : 0.160 s
===== /mnt/9p =====   (virtio-9p)
  seq read 64M : 67108864 bytes (67 MB, 64 MiB) copied, 2.97098 s, 22.6 MB/s
  seq write 32M: 33554432 bytes (34 MB, 32 MiB) copied, 1.67406 s, 20.0 MB/s
  create 200    : 0.778 s
  stat 200      : 0.344 s
  rmtree 200    : 0.524 s
작업virtiofsvirtio-9pvirtiofs 우위
순차 읽기 64MB27.6 MB/s22.6 MB/s1.22배
순차 쓰기 32MB35.5 MB/s20.0 MB/s1.78배
파일 200개 생성0.398 s0.778 s1.96배
stat 200회0.053 s0.344 s6.49배
200개 삭제0.160 s0.524 s3.28배

차이가 가장 큰 쪽은 처리량이 아니라 stat이다. 소스 트리를 빌드하거나 git status를 돌리는 작업은 메타데이터 호출이 대부분이라 이 6배가 체감으로 직결된다.

소유권과 권한이 다르게 보인다

게스트에서 두 마운트에 각각 파일과 디렉터리를 만들고, 호스트와 게스트가 같은 대상을 어떻게 보는지 비교했다. 게스트의 cloud-init은 root로 실행되고, 호스트 공유 디렉터리의 소유자는 uid 1000이다.

# 게스트 안
echo guest > /mnt/vfs/from_guest_vfs.txt
echo guest > /mnt/9p/from_guest_9p.txt
chmod 600 /mnt/vfs/from_guest_vfs.txt /mnt/9p/from_guest_9p.txt
mkdir -p /mnt/vfs/d_vfs /mnt/9p/d_9p
ls -ln /mnt/vfs /mnt/9p
/mnt/9p:                              <- 9p 마운트로 본 같은 디렉터리
-rw-r--r-- 1 1000 1000 67108864 big.bin
drwxr-xr-x 2    0    0     4096 d_9p            <- 9p로 만든 것: root 소유로 보인다
drwxr-xr-x 2 1000 1000     4096 d_vfs
-rw------- 1    0    0        6 from_guest_9p.txt
-rw------- 1 1000 1000        6 from_guest_vfs.txt
-rw-r--r-- 1 1000 1000       16 hello.txt

/mnt/vfs:                             <- virtiofs 마운트로 본 같은 디렉터리
-rw-r--r-- 1 1000 1000 67108864 big.bin
drwx------ 2 1000 1000     4096 d_9p            <- 같은 디렉터리인데 1000 소유, 0700
drwxr-xr-x 2 1000 1000     4096 d_vfs
-rw------- 1 1000 1000        6 from_guest_9p.txt
-rw------- 1 1000 1000        6 from_guest_vfs.txt
-rw-r--r-- 1 1000 1000       16 hello.txt

같은 d_9p가 9p로는 drwxr-xr-x 0 0, virtiofs로는 drwx------ 1000 1000으로 보인다. 호스트에서 확인하면 이유가 나온다.

$ ls -ln ~/share
-rw-r--r-- 1 1000 1000 67108864 big.bin
drwx------ 2 1000 1000     4096 d_9p
drwxr-xr-x 2 1000 1000     4096 d_vfs
-rw------- 1 1000 1000        6 from_guest_9p.txt
-rw------- 1 1000 1000        6 from_guest_vfs.txt

$ # 각 항목의 xattr 확인 (python os.listxattr/getxattr)
big.bin             mode=0644 uid=1000 gid=1000  xattr={}
d_9p                mode=0700 uid=1000 gid=1000  xattr={'user.virtfs.uid': b'\x00\x00\x00\x00',
                                                        'user.virtfs.gid': b'\x00\x00\x00\x00',
                                                        'user.virtfs.mode': b'\xedA\x00\x00'}
d_vfs               mode=0755 uid=1000 gid=1000  xattr={}
from_guest_9p.txt   mode=0600 uid=1000 gid=1000  xattr={'user.virtfs.uid': b'\x00\x00\x00\x00',
                                                        'user.virtfs.gid': b'\x00\x00\x00\x00',
                                                        'user.virtfs.mode': b'\x80\x81\x00\x00'}
from_guest_vfs.txt  mode=0600 uid=1000 gid=1000  xattr={}
hello.txt           mode=0644 uid=1000 gid=1000  xattr={}

security_model=mapped-xattr은 게스트의 uid/gid/mode를 호스트 파일의 user.virtfs.* xattr에 넣고, 호스트 파일 자체는 QEMU를 실행한 사용자 소유로 만든다. virtiofs는 매핑 없이 호스트 값을 그대로 보여준다.

security_model호스트 파일 소유자게스트가 보는 uid필요 권한
passthrough게스트가 지정한 uid/gid게스트가 지정한 값QEMU가 root여야 함
mapped-xattrQEMU 실행 사용자xattr에 저장된 값일반 사용자 가능, 호스트 FS가 xattr 지원
mapped-fileQEMU 실행 사용자별도 메타데이터 파일의 값일반 사용자 가능
noneQEMU 실행 사용자호스트 값일반 사용자 가능, 소유권 변경 실패 무시

xattr 자체는 두 방식 모두 게스트에서 설정·조회가 됐다.

[/mnt/vfs] mode=644 uid=1000 gid=1000 inode=305662
  symlink: OK
  xattr  : OK -> b'abc'
[/mnt/9p]  mode=644 uid=1000 gid=1000 inode=305664
  symlink: OK
  xattr  : OK -> b'abc'

주의사항

상황증상대응
메모리 백엔드 없이 virtiofs 사용vhost-user-fs-pci 초기화 실패-object memory-backend-memfd,share=on + -numa node,memdev=mem를 함께 준다
virtiofsd를 띄우지 않고 QEMU 실행소켓 연결 실패로 QEMU가 뜨지 않는다virtiofsd를 먼저 띄우고 소켓 파일 존재를 확인한다
root 없이 virtiofsd 실행샌드박스 생성 실패--sandbox=none을 주되, 격리가 사라지는 것을 감수한다
9p에서 msize를 기본값으로 둠순차 처리량이 크게 떨어진다msize를 키운다. 커널이 값을 깎을 수 있으니 mount 출력으로 확인한다
mapped-xattr로 만든 파일을 호스트에서 그대로 씀소유권·권한이 호스트 기준과 다르고 user.virtfs.* xattr이 붙는다호스트와 게스트가 같은 권한을 봐야 하면 virtiofs를 쓴다
호스트 파일시스템이 xattr 미지원mapped-xattr이 동작하지 않는다mapped-file이나 virtiofs로 바꾼다
여러 쓰기 주체가 동시에 공유 디렉터리 수정--cache=auto에서 캐시 일관성 문제가 생길 수 있다호스트도 동시에 쓰면 --cache=never를 검토한다
TCG에서 측정한 수치를 그대로 인용에뮬레이션 비용이 섞여 절대값이 의미 없다KVM 환경에서 다시 측정한다(/dev/kvm 접근에는 kvm 그룹 권한이 필요하다)

마무리

  • 메타데이터 연산에서 virtiofs가 크게 앞선다. 같은 TCG 조건에서 stat 200회가 0.053초 대 0.344초로 6.5배 차이였다.
  • 순차 처리량 차이는 읽기 1.22배, 쓰기 1.78배로 메타데이터보다 작다.
  • virtiofs는 virtiofsd 데몬과 공유 메모리 백엔드가 추가로 필요하지만, 설정은 한 번이고 소유권이 호스트와 일치한다.
  • 9p의 mapped-xattr은 게스트 소유권을 user.virtfs.* xattr에 저장하므로, 같은 파일이 마운트에 따라 다른 uid/mode로 보인다.
  • 소스 트리를 공유해 빌드하는 용도라면 virtiofs를 먼저 고려하고, 데몬을 띄울 수 없는 환경에서만 9p를 쓴다.

참고

답글 남기기