Skip to content

keep a microVM container's rootfs upper on a virtio-blk ext4 disk #3153

Description

On the microVM runtime with EROFS lower layers (#2440), the guest mounts read-only image layers over virtio-pmem with DAX, while the container's writable overlay upperdir and workdir still live on the virtio-fs share (<cid>/rw/fs and <cid>/rw/work) served by virtiofsd.

Keeping the writable upper on virtio-fs costs us in two places:

  1. Every guest write, create, rename, stat, or unlink in the upper is a FUSE round-trip through virtiofsd and a host VFS syscall on the node's ext4 filesystem. Agent workloads (git checkout, pip install, builds, test runs) are dominated by small-file metadata operations, and under concurrent snapshot writeback virtiofsd blocks on the same host ext4 journal as atelet.
  2. Checkpointing walks fs/ and work/ file by file to build rootfs-rw-<cid>.tar, and restore untars every file back onto the host filesystem. The cost scales with file count rather than byte size.

Proposal

Give each EROFS microVM container its own sparse ext4 disk image (<cid>.upper.ext4) attached over virtio-blk:

  1. Cold boot the seeded (resolv.conf, hosts, hostname) upper into a 4 GiB sparse journal-less ext4 image, attaches it read-write as /dev/vd{a,b,...}; Kata agent mount it with noinit_itable at /run/kata-containers/upper/<cid>. The container overlay takes upperdir and workdir from there.
  2. Checkpoint hard-links <cid>.upper.ext4 into the checkpoint directory as rootfs-disk-<cid>.ext4 in place of the rootfs-rw tar. In a MEMORY snapshot, the guest's dirty ext4 pages are captured in memory-ranges. For a ROOTFS snapshot, the worker removes the container and destroys the sandbox before archiving so the guest unmounts and flushes the disk cleanly.
  3. Restore copies rootfs-disk-<cid>.ext4 out of the restore directory with copy_file_range and rewrites the disk path in Cloud Hypervisor's config.json.

Benchmarks

  • Below PoC & micro-benchmarks are handled in-guest fio and POSIX metadata on /work at overlay upper
  • 2 vCPU / 2 GiB microVM actor on an c3-highcpu-192-metal node
  • medians across 6 virtio-fs runs and 3 virtio-blk runs):
Workload on /work (overlay upper) virtio-fs upper virtio-blk upper
Sequential write
(1 GiB, 1 MiB bs, psync, end_fsync=1)
734 MiB/s (1,394 ms) 1,108 MiB/s (923 ms)
4 KiB random write
(1 GiB, O_DIRECT, libaio qd=16)
17.8k IOPS (p50 819 us, p99 1,011 us) 73.2k IOPS (p50 73 us, p99 148 us)
4 KiB random read
(1 GiB, O_DIRECT, libaio qd=16)
101.4k IOPS (p50 76 us, p99 444 us) 196.3k IOPS (p50 50 us, p99 89 us)
fio smallfiles
(10,000 x 4 KiB create + write + close)
3,178 ms (3.15k IOPS) 101 ms (98.0k IOPS)
Metadata: create 20,000 empty files 6,405 ms 214 ms
Metadata: ls -l over 20,000 files 664 ms 54 ms
Metadata: rm -rf 20,000 files + sync 2,456 ms 286 ms

SWE-perf (swebench-astropy-7336, 150 users for 10 min on c3-highcpu-192-metal, 3 worker pods, MEMORY snapshots to GCS):

150 users, 10 min (24 MB upper) virtio-fs upper virtio-blk upper
tasks done / suspends / failures 474 / 2,140 / 0 580 / 2,552 / 0 (+22% tasks)
ResumeActor p50 / p99 (s) 1.4 / 39.0 1.4 / 4.4
SuspendActor p50 / p99 (s) 2.1 / 88.0 2.7 / 97.0
TaskCEL p50 / p99 (s) 7.5 / 44.0 6.7 / 8.1
TaskWallClock p50 / p99 (s) 61.0 / 288.0 47.0 / 178.0
worker checkpoint avg (s) 0.41 0.31
worker restore avg / sampled p99 (s) 0.52 / 6.37 0.28 / 0.66
restore upper step (upper_join) p50 / p99 (s) 0.03 / 5.71 (untar) 0.02 / 0.05 (copy_file_range)
snapshot upper avg (MB) 24.2 (tar) 24.8 (ext4 image)
node IO PSI full avg 39.4% 32.6%

Results

  1. In-guest small-file writes and metadata ops (create, stat, unlink) are 9x to 31x faster since they stay in the guest ext4 filesystem instead of doing a FUSE round-trip per syscall.
  2. On OnDemand memory restore, Cloud Hypervisor mmaps memory-ranges in ~0.2 s, so untarring a 24 MB upper (5.71 s p99) is 90% of the worker restore tail (6.37 s p99). Copying one sparse image takes 0.05 s p99 (0.66 s worker restore p99) and cuts virtiofsd and worker D-state stalls by >95% at 150 users.

Trade-offs to discuss

  1. Disk and inode limits: the 4 GiB sparse image is formatted with 65,536 inodes (~150 KiB initial size), so containers exceeding 4 GiB or 65k files hit ENOSPC. We can raise the default virtual size / inode count (unallocated inode tables are lazy and cost very little on disk) or size it from the actor template.
  2. Discard on delete: deleted files inside the guest don't free blocks in the host sparse file unless we mount the upper disk with discard (so guest deletes punch holes on the host).
  3. Packaging: needs a static mke2fs (e2fsprogs 1.47+ for -d) in the worker image or SandboxConfig asset

Related:
#2440 serve container image lower layers as EROFS to microVM (DAX) and gVisor
#1954 / #3026 drop per-file fsync when tarring rootfs-upper

kind/design
area/microVM
area/storage

Activity

  1. ajaysundark commented on Oct 23, 2026

    @ajaysundark
    ContributorAuthor
  2. ajaysundark commented on Oct 23, 2026

    @ajaysundark
    ContributorAuthor

    Other general virtio-fs vs virtio-blk perf discussions:

    This also aligns with our interest on giving dedicated block-based snapshot storage for actors -- when an actor has its own attached block disk, the ext4 upper image can simply live directly on that disk. Together, they completely eliminate the need to tar files inside the VM and eliminate the need to copy the resulting snapshot across the network to GCS.

  3. BenTheElder commented on Oct 23, 2026

    @BenTheElder
    Collaborator

    Packaging: needs a static mke2fs (e2fsprogs 1.47+ for -d) in the worker image or SandboxConfig asset

    We need to take a hard look at avoiding that.

    Also, not entirely sure ext4 is what we want, and we have to consider some other metrics missing here, like small durdir lower bound size.

    However I do think we want to move away from virtiofsd for actors without CSI volumes, post-GA.

    Together, they completely eliminate the need to tar files inside the VM and eliminate the need to copy the resulting snapshot across the network to GCS.

    That does not follow, nothing about this eliminates network bandwidth. If anything the opposite, tar is smaller.

    There are some other useful properties to block though, unlike tar we can let the object store plugin write this directly with a parallel download while having no subsequent decode step to coordinate.

  4. BenTheElder commented on Oct 23, 2026

    @BenTheElder
    Collaborator

    Also there's a significant trade-off in that we have to set an upper bound, and that influences the metadata overhead IIRC. Tar metadata scales with what the user uses and doesn't really have an upper bound. We're still getting signal on how users will use durabledirs. 4GB may be too small.

    Previously I discarded this for those reasons. I do think we MUST revisit it, but there are some dimensions missing in the op.

  5. ajaysundark commented on Oct 23, 2026

    @ajaysundark
    ContributorAuthor

    3. > Packaging: needs a static mke2fs (e2fsprogs 1.47+ for -d) in the worker image or SandboxConfig asset

    We need to take a hard look at avoiding that.
    Also, not entirely sure ext4 is what we want, and we have to consider some other metrics missing here, like small durdir lower bound size.
    

    Agreed on avoiding additional assets. The guest already ships mkfs.ext4 today, I had to use mke2fs to seed the /etc/resolv.conf, hosts, hostname files etc. We may be able to handle this directly in the worker.

    On the fs choice, I was experimenting few options. journal-less ext4 was great for small lower-bound cases (empty 4GB sparse image takes ~150KB of allocated blocks). I also looked at xfs, which is better performing, but enforces some minimum size restrictions. We need to weigh the tradeoffs.

    This issue / proposal is primarily for evaluating blk-storage for rootfs-upper first.

    However I do think we want to move away from virtiofsd for actors without CSI volumes, post-GA.
    > Together, they completely eliminate the need to tar files inside the VM and eliminate the need to copy the resulting snapshot across the network to GCS.
    
    
    That does not follow, nothing about this eliminates network bandwidth. If anything the opposite, tar is smaller.
    There are some other useful properties to block though, unlike tar we can let the object store plugin write this directly with a parallel download _while_ having no subsequent decode step to coordinate.
    

    Fair point, that was overstated in the description. I could reword it better. I was thinking along #1951 as local NVMe caching where the block image could stay on a volume / snapshot backend.

    Also there's a significant trade-off in that we have to set an upper bound, and that influences the metadata overhead IIRC. Tar metadata scales with what the user uses and doesn't really have an upper bound. We're still getting signal on how users will use durabledirs. 4GB may be too small.

    agreed, I also called out the cap limitations, we need to look into it further. 4GB was arbitrary. My agent PoC with 'lazy_itable_init' left unallocated block groups/sparse, so even if virtual cap as a larger 32 or 64 GiB wouldnt cost us as much. We maybe able to size the cap w. pod ephemeral storage limits?

    Previously I discarded this for those reasons. I do think we MUST revisit it, but there are some dimensions missing in the op.

    That makes sense. I've also been experimenting with CoW backing for the virtio-blk uppers, for eg: thin-pool with CoW and XFS/reflink. The results are very convincing for golden-clones and restore. Happy to add the results to the issue to open up further discussion / evaluate overall fit for rootfs upper

  6. BenTheElder commented on Oct 24, 2026

    @BenTheElder
    Collaborator

    Agreed on avoiding additional assets. The guest already ships mkfs.ext4 today, I had to use mke2fs to seed the /etc/resolv.conf, hosts, hostname files etc. We may be able to handle this directly in the worker.

    I dug up my old prototypes with ext4, and confirmed that I'd already worked around that and we can re-use that part if we go with ext4.

    Basically I took an ext4-in-go library designed to create read-only layers for hcsshim and forked it to support rw (not really in scope for upstream). It's not totally terrible, though I wouldn't necessarily go that route off the cuff ... it's a fair bit of code, and still missing features, does perform kinda decently and avoids the packaging issues:

    main...BenTheElder:ext4-writer

    There's an alternative the possibility of pre-provisioning a "golden ext4" as a cached sandbox asset and just patching the UUID which might be more elegant (and faster, if we're using fixed size sparse disks anyhow)

    main...BenTheElder:substrate:ext4-golden-uuid

    I think I might like that more ... it's not much code to patch the UUID (so each disk can be unique against the pre-provisioned golden) and still avoids packaging issues. However, only if we selected a fixed size (or maybe a small number of fixed sizes).

    On the fs choice, I was experimenting few options. journal-less ext4 was great for small lower-bound cases (empty 4GB sparse image takes ~150KB of allocated blocks). I also looked at xfs, which is better performing, but enforces some minimum size restrictions. We need to weigh the tradeoffs.

    Yeah, they have some .... very interesting tradeoffs. We probably need to tag in some more folks to consider as we get closer.

    agreed, I also called out the cap limitations, we need to look into it further. 4GB was arbitrary. My agent PoC with 'lazy_itable_init' left unallocated block groups/sparse, so even if virtual cap as a larger 32 or 64 GiB wouldnt cost us as much. We maybe able to size the cap w. pod ephemeral storage limits?

    .. maybe, though then the cap depends on the worker, in a way behavior generally shouldn't depend on the worker ... (sure the worker and the node ultimately do have actual limits, but the same actor template landing on different workers getting different disk sizes is ... surprising)

    That makes sense. I've also been experimenting with CoW backing for the virtio-blk uppers, for eg: thin-pool with CoW and XFS/reflink. The results are very convincing for golden-clones and restore. Happy to add the results to the issue to open up further discussion / evaluate overall fit for rootfs upper

    Yeah, this also depends on some work Michelle Au (@msau42) is prototyping that changes a few of the dimensions, like ... we might not really care about the cost to write any form of durabledir in some environments in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/microVMkind/featureAn enhancement / feature request or implementation

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions