资讯详情

Kata Containers virtiofs性能测试:从9p到DAX的共享文件系统优化

📅 2026/10/21 5:03:13 | 华诺云谱 👁 阅读
Kata Containers virtiofs性能测试:从9p到DAX的共享文件系统优化
在云原生安全隔离这个方向上Kata Containers 算是一个绕不开的方案。它用轻量虚拟机给容器提供硬件级隔离但虚拟机这层带来的开销一直是个老大难问题其中文件系统共享又是最容易被吐槽的一环。virtiofs 就是用来替代老一代 9p 共享方案的这两年也逐步成了 Kata 的默认文件系统共享方式。最近我基于某内部隔离项目做了一轮 virtiofs 的完整测试从配置、压测到机制梳理都过了一遍这篇文章围绕实际测试过程和技术分析展开把能直接复用的经验写出来给准备上手或者正在排查 virtiofs 性能问题的同学一些参考。Kata Containers 本身的架构大家应该不陌生shim 拉起 hypervisorhypervisor 再启动一个精简 guest 内核容器进程跑在 guest 里。宿主机和 guest 之间必然要共享文件否则镜像没法挂、目录没法传这一步绕不过去。而共享文件这件事恰恰是虚拟化场景里最难做好的部分之一。1. 项目背景为什么 Kata 容器需要 virtiofs1.1 Kata 的隔离模型与文件共享痛点Kata Containers 的隔离模型决定了它和普通 runc 容器有本质区别。runc 直接在宿主机内核上通过 namespace 和 cgroup 做隔离文件系统共享是不存在问题的大家共用同一个内核页缓存。而 Kata 每个 Pod 对应一个微型虚拟机有独立的 guest 内核宿主机上的 rootfs、容器镜像层、挂载目录要进入 guest就必须有一条“跨虚拟机”的文件传输通道。这条通道做得不好后果很直接容器启动变慢镜像层解包耗时编译类任务、数据库应用、Node/Java 这类依赖 mmap 的进程在读写文件时会明显感受到延迟抖。说白了隔离性上去了IO 性能如果垮掉业务方是不愿意接的。Kata 早期用的是 virtio-9p。9p 是一个很老的文件协议设计初衷不是面向虚拟化场景的高性能共享它能用但不好用。在 Kata 2.x 的默认配置中新版本已经逐步把默认共享方案切到 virtiofs9p 被标记为废弃状态。这个转变的本质原因只有一个9p 的性能和语义已经满足不了容器的实际负载需求。1.2 9p 共享方案的时代局限9p 慢不是某一次实现的问题而是协议设计和 virtio 传输层的耦合方式决定的。九年前我第一次用 Kata 的前身跑 MySQL 容器时性能数据非常惨烈。后来看协议栈才明白原因guest 里每次 open、lookup、readdir 这类元数据操作都要通过 virtio 环形队列发请求到宿主机侧的 qemu 进程qemu 再转发给 9p 服务端。一次路径查找可能涉及多次往返每一次往返都有虚拟机退出的上下文切换开销。对于高 IOPS 的小文件操作来说这几乎是灾难。9p 的另一个硬伤是不支持像样的 mmap 语义。很多现代运行时比如 Node.js、Java、Python 的某些库加载文件时会优先走 mmap9p 上 mmap 要么回退到普通 read要么出现频繁的缺页中断和同步开销最终表现为启动时间暴涨、内存占用异常。我在测试一个内部配置解析服务时同样一份文件在 9p 共享路径下读取耗时是本地磁盘的十几倍换成 virtiofs 之后才回到正常水平。9p 还有 flush 和 fsync 语义过重的问题。guest 侧每次写操作后如果触发 fsync宿主机侧必须完成一次完整的落盘确认导致大量小写入场景下吞吐上不去延迟抖动明显。这些问题不是靠调参数能解决的属于协议级别的瓶颈所以 virtiofs 的出现是必然。2. virtiofs 技术原理拆解2.1 从 FUSE 到 virtiofsd一条新的共享链路virtiofs 这个名字容易让人误以为它是 virtio 家族里的一个“块设备方案”实际上它走的是 FUSE 协议的路子。FUSE 大家更熟悉的是用户态文件系统比如 sshfs、s3fs 这些guest 内核里的 VFS 请求被转发到用户态 daemon由 daemon 用宿主机普通文件系统接口去实现真正读写。virtiofs 把 FUSE 的思路搬到了虚拟化场景但传输层不是 /dev/fuse 那套字符设备而是 virtio 设备。QEMU 侧实现了一个名为 vhost-user-fs 的设备这个设备通过 vhost-user socket 与宿主机上的 virtiofsd 通信。virtiofsd 是一个用户态守护进程负责接收 guest 内核 virtio_fs 驱动发来的 FUSE 请求然后以普通进程的身份在宿主机上执行 open、read、write、lookup 等操作。链路大概是这样Guest 应用进程 - Guest 内核 VFS - virtio_fs 驱动 - vhost-user-fs 设备QEMU - vhost-user socket - 宿主机 virtiofsd - 宿主机文件系统ext4/xfs 等注意 virtiofsd 是用户态进程跑在宿主机上但它对文件的访问走的是宿主机正常的内核文件系统栈所以宿主机页缓存是能命中的。这意味着同样的文件如果已经被宿主机缓存virtiofs 的性能会很接近本地这是它比 9p 有质的提升的关键原因之一。2.2 DAX 机制与缓存策略virtiofs 最吸引人的特性是 DAXDirect Access。所谓 DAX简单说就是宿主机和 guest 之间开一段共享内存窗口guest 里的文件内容可以通过这段窗口直接映射到宿主机页缓存上不需要再做一次 guest 侧缓存拷贝。没有 DAX 的时候guest 内核要把文件内容读入自己的页缓存宿主机那边 virtiofsd 也会在宿主机的页缓存里保留一份两边的缓存是重复的既浪费内存又增加一次跨虚拟机拷贝。开启 DAX 之后guest 里通过 mmap 映射文件时直接映射到宿主机页缓存的物理内存区域guest 不再单独维护一份文件页缓存数据路径大大缩短。这个机制对 mmap 密集型的应用是重大利好。我测试过一个 Java 服务加载阶段要 mmap 几十个 jar 文件关闭 DAX 时 guest 内存里能看到大量 page cache 占用切换 DAX 后 guest 内存占用明显下降文件加载耗时也缩短了 40% 以上。DAX 缓存窗口的大小由 Kata 配置里的virtio_fs_cache_size控制单位是 MiB。设置 0 表示关闭 DAX设置成 4096 就表示给这个窗口预留 4GiB 的共享内存。需要注意的是这段内存是宿主机物理内存的一部分规划时要预留出来不能无脑调大。还有缓存模式参数常见的是cachealways、cacheauto、cachenone。cachealways表示 guest 侧总是使用自己的页缓存来缓存共享文件适合绝大多数读写混合场景cachenone表示 guest 侧不做缓存每次读写都直接穿透到 virtiofsd适合有一些特殊一致性要求的场景cacheauto则是在两者之间根据文件打开方式动态选择但实际使用中自动判断有时会带来不可预期的行为所以我一般建议先固定为cachealways。2.3 与 9p 的关键差异对比virtiofs 相对于 9p 的优势可以总结成协议和缓存两个层面。协议层面virtiofs 复用 FUSE 协议但做了大量虚拟化场景的适配增强。它支持 readdirplus也就是一次 round trip 可以把目录项和 inode 元数据一起拿到省掉了 lookup 的多次往返支持更大的 IO 请求合并顺序读场景下吞吐更高还支持多个请求并发处理不像 9p 在某些实现里会有串行化的瓶颈。缓存层面virtiofs 可以通过 DAX 避免双份页缓存同时保留了宿主机页缓存的命中能力。9p 虽然也能在宿主机侧做缓存但整个协议栈的效率和缓存命中的可控性差了很远。对比维度virtio-9pvirtiofs传输协议9pPlan 9 文件协议FUSE 协议扩展元数据操作效率lookup/readdir 多次往返效率低readdirplus 合并请求效率高mmap 支持弱容易退化为普通 read支持良好DAX 下可直映射宿主机页缓存宿主机缓存利用较弱可以深度利用宿主机页缓存双页缓存问题存在DAX 模式下可消除Kata 默认状态废弃路径默认推荐9p 并没有完全消失某些老旧配置或者特殊场景下还能看到它但新项目完全没必要再往 9p 上投入。3. 测试环境与配置实操3.1 环境准备先交代测试环境方便对比复现。我跑测试的机器是 x86_64 架构的服务器16 核 CPU64GB 内存存储盘是本地 NVMe SSD顺序读的裸盘极限大约在 2.8GB/s 左右。宿主内核版本是 5.15 LTS容器运行时用的 containerdKata Containers 版本是 2.4.x 系列QEMU 侧使用 vhost-user-fs 设备。这里有个前提条件必须确认宿主内核和 QEMU 需要支持 vhost-user-fs。如果内核太老或者 QEMU 版本偏低Kata 启动时会在 virtiofs 初始化阶段直接报错。建议先跑一次kata-runtime env确认当前配置支持的 shared filesystem 类型再动手改配置。3.2 让 Kata 切换到 virtiofsKata 的共享文件系统配置在/etc/kata-containers/configuration.toml或者对应发行版的配置文件里。在 containerd 场景下一般是/usr/share/defaults/kata-containers/configuration.toml作为默认模板自定义配置放/etc/kata-containers/configuration.toml覆盖。核心配置项如下[hypervisor.qemu] path /usr/bin/qemu-system-x86_64 kernel /usr/share/kata-containers/vmlinux.container image /usr/share/kata-containers/kata-containers.img machine_type q35 # 共享文件系统类型virtiofs 是新一代方案 shared_fs virtiofs # virtiofsd 可执行文件路径注意不同发行版路径不同 virtio_fs_daemon /usr/libexec/kata-containers/kata-virtiofsd # DAX 缓存窗口大小单位 MiB设置为 0 表示关闭 DAX virtio_fs_cache_size 4096 # 传给 virtiofsd 的额外参数 virtio_fs_extra_args [-o, cachealways, --thread-pool-size16]版本差异要注意。Kata 2.4 之后部分发行版打包的配置项名称从shared_fs virtio-fs变成了shared_fs virtiofsdaemon 路径也有两种QEMU 自带的 C 实现通常叫virtiofsd位置在/usr/libexec/virtiofsdKata 自带的 Rust 实现叫kata-virtiofsd位置可能不同。所以不要照抄路径先确认自己系统上实际存在的二进制文件。virtio_fs_extra_args里的参数风格也分两种。C 版 virtiofsd 用-o cachealways这种风格Rust 版更习惯--cachealways。如果传参方式不对virtiofsd 启动时会直接报参数解析失败。建议先手动执行一下 daemon 的--help看一眼实际支持的参数。我没有一上来就开 DAX而是先把virtio_fs_cache_size保持为 0 跑了一轮基线再设置成 4096 对比。原因是如果一上来就开 DAX性能确实好但很难定位到底哪些提升来自 DAX哪些来自 virtiofs 本身的协议优化。分开跑才能得出清晰结论。3.3 验证配置生效配置改完以后第一件事不是急着跑容器而是确认 Kata 真的用了 virtiofs。验证入口有几个# 查看 Kata 当前环境的共享文件系统配置 kata-runtime env | grep -i -A2 shared # 启动一个容器后检查宿主机 qemu 进程是否挂了 vhost-user-fs ps -ef | grep qemu | grep vhost-user-fskata-runtime env的输出里能看到Shared filesystem字段如果显示virtiofs说明配置层已经生效。第二个命令更直接容器跑起来以后宿主机上能看到类似-chardev socket,idchar0,path/run/kata-containers/.../virtiofs.sock这样的 qemu 参数以及-device vhost-user-fs-pci这样的设备参数基本可以确定共享链路已经建立。还有一招进容器内部看挂载信息# 在容器内执行 mount | grep virtiofs # 或者看 /proc/mounts cat /proc/mounts | grep virtiofs如果 Kata 的 rootfs 是走 virtiofs 挂进来的能看到类似virtiofs on / type virtiofs的挂载记录。注意 Kata guest 内部可能有 overlayfs 组合rootfs 底层仍然对应 virtiofs 挂载点所以 grep 的时候别只搜根目录。还有一个容易踩的坑如果你改了/etc/kata-containers/configuration.toml但 containerd 的 shim v2 二进制加载的是另一个路径比如/opt/kata/...那配置可能根本没被读到。确认方法是在宿主机查 containerd shim 进程的环境变量或者启动参数里有没有指定 kata 配置目录。4. 性能测试与实测数据4.1 测试工具与压测方法文件系统性能测试我主要用了 fio 和 dbench。fio 负责常规吞吐和 IOPS 指标dbench 负责模拟多客户端元数据压力。两个工具可以覆盖大部分业务负载特征。先看 fio。我分别测了顺序读、顺序写、随机读、随机写四类场景命令模板如下# 顺序读 fio -nameseqread -ioenginelibaio -direct1 -bs128k -rwread \ -size4G -numjobs8 -iodepth32 -time_based -runtime60 \ -group_reporting -filename/tmp/fiotest/seqread # 顺序写 fio -nameseqwrite -ioenginelibaio -direct1 -bs128k -rwwrite \ -size4G -numjobs8 -iodepth32 -time_based -runtime60 \ -group_reporting -filename/tmp/fiotest/seqwrite # 随机读4k 块大小 fio -namerandread -ioenginelibaio -direct1 -bs4k -rwrandread \ -size4G -numjobs8 -iodepth32 -time_based -runtime60 \ -group_reporting -filename/tmp/fiotest/randread # 随机写4k 块大小 fio -namerandwrite -ioenginelibaio -direct1 -bs4k -rwrandwrite \ -size4G -numjobs8 -iodepth32 -time_based -runtime60 \ -group_reporting -filename/tmp/fiotest/randwrite这里有个关键点测试文件必须落在 virtiofs 共享目录里别直接在容器根目录写Kata 容器内/tmp有些场景可能挂的是 guest 内部本地盘测出来就不是 virtiofs 的数据了。我的做法是在宿主机上准备一个共享目录启动容器时挂载进去然后容器内 fio 写到这个挂载点。dbench 命令相对简单dbench -t 60 -c 8 /tmp/fiotestdbench 的得分表示模拟负载下的平均吞吐单位是 MB/s。它主要测试 create、delete、rename、stat 这类元数据操作正好暴露 9p 和 virtiofs 在元数据路径上的差距。4.2 实测数据汇总下面这组数据来自我上述环境绝对值只代表这台机器但相对趋势在不同机器上是稳定的。测试项宿主机 nativeKata 9pKata virtiofs(关闭 DAX)Kata virtiofs(开启 DAX)顺序读 128k2.81 GB/s0.52 GB/s1.86 GB/s2.43 GB/s顺序写 128k1.72 GB/s0.26 GB/s1.05 GB/s1.38 GB/s随机读 4k IOPS182k2.8k11.3k26.7k随机写 4k IOPS94k1.6k6.4k12.2kdbench 吞吐186 MB/s58 MB/s121 MB/s147 MB/s数据看下来最直观的感受是9p 和 virtiofs 完全不在一个数量级。顺序读从 0.52GB/s 提升到接近 2.43GB/s虚拟化损耗从接近 80% 下降到大约 15% 左右。随机读 IOPS 从 2.8k 提升到 26.7k提升接近十倍这个量级的改善足够让很多应用改变对 Kata 的刻板印象。4.3 结果解读与技术分析为什么 virtiofs 能把顺序读拉到接近 native 的水平我分析有几个原因叠加。首先是大 IO 请求合并virtiofs 对连续读做了合并处理可以一批一批地把大量数据页传过去而 9p 每次请求能带的数据量非常有限。其次是宿主机页缓存命中virtiofsd 读文件时走宿主机内核缓存测试文件反复读几轮之后后面的读操作实际上是在读宿主机内存速度自然快。DAX 的增益也很清晰开启 DAX 之后随机读 IOPS 从 11.3k 提升到 26.7k比例非常可观。因为 guest 侧不再需要把文件内容从共享窗口拷贝到自己的页缓存缺页中断和拷贝开销都减小了。dbench 从 121MB/s 提升到 147MB/s说明元数据密集型场景也受益明显。不过也要理性看待随机 IOPS 距离 native 还有很大差距。原因不难理解fio 随机读测试使用 libaio direct IO每笔请求都要穿透 virtiofsd 用户态经过 FUSE 协议解析、宿主机系统调用、结果回传整个路径开销肯定比直接在宿主机内核里跑要大。这个差距是 virtiofs 的协议特性决定的不是配置调优能完全消除的。再补充一个没写进表格的观察关闭 DAX 时在 guest 内存里能看到大量 page cache 占用尤其跑完一轮随机写之后guest 内存几乎被缓存吃掉了。开启 DAX 之后guest 侧页缓存明显减小这部分内存可以留给业务进程。对于内存水位敏感的容器这是比 IOPS 更值得关注的变化。还有一个小细节读多写少的场景里9p 的读性能其实没有烂到不能用的程度但一旦涉及小文件随机读写或者大量元数据操作就原形毕露。如果只是做个简单的配置文件挂载9p 勉强也能跑但容器镜像层、日志目录、临时目录这些高频路径9p 完全扛不住。这也是为什么 Kata 把默认方案切到 virtiofs 之后整体体验改观很大的原因。5. 常见问题、调优实践与使用建议5.1 参数调优让 virtiofs 更适合你的负载调试过程里我发现几个参数对最终性能影响很大值得拿出来单独说。第一是virtio_fs_cache_size的选择。这个值开得越大DAX 窗口能映射的宿主内存越多guest 里 mmap 大文件时越不容易出现窗口不足导致的回退。但它是宿主机物理内存的直接占用开得太大反而挤压其他容器的内存。我这边 64GB 内存的机器压测场景设置 4096MiB 够用如果是大内存机器且有大量 mmap 需求可以尝试 8192。如果设置后启动失败优先怀疑是内存分配不了调小再试。第二是virtio_fs_extra_args里的线程池大小。C 版和 Rust 版默认值不一样我在 Rust 版上测试时默认线程数偏保守并发上不去。后来手动指定--thread-pool-size16配合 fio 的numjobs8和iodepth32随机读 IOPS 提升非常明显。经验法则是线程池大小不要超过 CPU 核心数的两倍不然线程切换开销会吃掉收益。第三是 cache 模式的取舍。默认情况下 Kata 对 virtiofs 没有强制指定 cache 模式不同版本和不同 daemon 的行为会有差异。我建议对大部分应用显式加上cachealways避免 guest 侧对共享文件完全不做缓存导致读放大。如果业务对数据一致性要求极其苛刻接受不了任何缓存窗口期再考虑cachenone但性能代价很大。还要注意 host 机内存压力的影响。virtiofs 开启 DAX 后如果宿主机内存本身很紧张页缓存会被内核回收DAX 窗口的命中率会大幅下降。测试时最好用 cgroup 或 systemd 服务把宿主机的无关负载隔离掉否则成绩会非常不稳定。5.2 高频问题速查故障现象常见原因排查与解决启动容器报 virtiofsd 不存在daemon 路径配置错误或未安装对应组件which virtiofsd/which kata-virtiofsd确认实际路径修正virtio_fs_daemonQEMU 启动失败提示 vhost-user-fs 不支持QEMU 版本太老或编译时未启用 vhost-user-fs升级 QEMU确认支持vhost-user-fs-pci设备启动成功但容器内看不到挂载共享文件系统类型虽然改了但 containerd 加载的不是目标配置文件检查 kata 配置目录优先级确认 shim 进程加载路径性能提升不明显DAX 未开启或 cache 模式不对确认virtio_fs_cache_size 0显式设置cachealwaysguest 内存占满关闭 DAX 导致双份页缓存开启 DAX让 guest 直接映射宿主机页缓存随机写 IOPS 极低virtiofsd 线程池过小fio numjobs 跑不满提高--thread-pool-size提高numjobs和iodepth容器内 mmap 大文件失败DAX 窗口不足映射回退到普通路径调大virtio_fs_cache_size或检查宿主机内存余量有一个最容易忽略的问题容器内部跑 fio 时用了--direct1但 virtiofs 对 direct IO 的支持在不同内核版本上有差异。如果发现某个测试场景的 IOPS 异常低先确认 guest 内核里 virtio_fs 驱动是否支持 O_DIRECT不支持的话 fio 会静默回退到 buffered IO数据含义完全变了。我的做法是额外跑一轮不加direct1的对照两轮对比之下才能确定是不是 direct 路径的问题。5.3 什么时候该用 virtiofs什么时候不该virtiofs 最适合的场景是隔离要求高、文件共享频繁、应用依赖 mmap 或大量小文件读写。比如 Java 应用加载 class 和 jar、Node 应用读模块、编译任务读写构建缓存、日志组件频繁创建和轮转文件这些场景 virtiofs 的表现都足够好。不太适合的场景是大规模的高 IOPS 随机读写尤其是数据库的数据文件目录。不管 virtiofs 怎么调优用户态 FUSE 协议栈的开销始终存在2.6 万 IOPS 看着不错但和 native 的 18 万 IOPS 比还是有数量级差距。数据库这类对单机性能极致敏感的服务更应该考虑 Kata 的块设备直通方案或者干脆把数据落到独立的虚拟块设备上不要走共享文件系统。另外如果业务对页缓存一致性有强需求比如多个容器要同时读写同一个共享文件目录并且要求读写可见性的强一致virtiofs 的缓存模式选型要很谨慎。cachealways模式下一个 guest 写入的数据不一定会立刻出现在另一个 guest 的读路径里因为两侧页缓存都参与进来了。这种场景可以考虑cachenone配合 DAX或者不要在多个 Kata 容器间共享同一个读写目录。对我个人来说Kata Containers 的定位始终是安全隔离不是榨干单机 IO 性能。virtiofs 的价值在于把共享文件系统的短板补齐到业务可接受的程度。从这个角度看它已经完成得很出色了。最后分享一个我在测试中养成的习惯任何针对共享文件系统的压测都先跑一轮宿主机 native 基线再分别跑 9p、virtiofs 关 DAX、virtiofs 开 DAX 三组最后把数据放在一起对比。不要只测 virtiofs 一组因为没有对照的测试很容易得出“性能没想象中好”或者“性能已经很好了”这样缺乏依据的结论。各组之间的差距才是理解这套架构开销的真正钥匙。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑