Skip to content

Hold the NBD export to what it tells a client, and let it keep its blocks somewhere other than a file - #895

Open
MayCXC wants to merge 12 commits into
apple:mainfrom
MayCXC:nbd-memory-store
Open

Hold the NBD export to what it tells a client, and let it keep its blocks somewhere other than a file#895
MayCXC wants to merge 12 commits into
apple:mainfrom
MayCXC:nbd-memory-store

Conversation

@MayCXC

@MayCXC MayCXC commented Aug 27, 2026

Copy link
Copy Markdown

Summary

Five places where the NBD server tells a client one thing and does another, read against the protocol document, plus a pluggable block store and one use for it. https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md

The command flags were read past. They sit in the two bytes after the request magic, which were skipped, so an export that said it took force unit access replied to a command carrying it before what it wrote was durable. They are read now, and a command carrying that flag waits.

A read-only export now says so and turns writes away, which the protocol requires of it and which the pod volume API already offers to ask for. A request reaching past the end is refused as invalid rather than served.

Structured replies are offered and answered. A client that asks for replies carrying their own framing gets them, and a read answered that way names the offset it covers and carries a message with any error rather than a bare number. That framing is what lets an export be asked how it is laid out, so the block-descriptor context is offered and answered: a store in memory knows which chunks it holds and a file is asked through the same seeks a sparse copy uses, so the holes a client is told about are the ones really there.

Listing the exports names the one on offer. Every connection serves the one store behind the export, so a flush on any of them covers what was written on the others, which is the condition for saying that a client may spread its work across several.

The store behind an export is chosen rather than fixed to a file, which is what makes a memory-backed store possible: a container's swap area held in host memory rather than on disk.

Dependency and Merge Order

This targets stock apple/containerization:main. Merge order: #863, #872, #878, then this. The head carries their commits because all of them target stock main; after they merge, this branch takes current main and its remaining diff is the six commits above.

Motivation and Context

Discussion: #883, where I ask whether the memory-backed swap area is something you want at all, or whether the useful half here is the protocol fixes plus the pluggable store. The protocol fixes stand on their own and I am glad to send them separately.

Testing

  • make containerization, make check, and swift test clean.
  • make integration: the memory-backed test asserts the release as well as the high water mark. The filler exits while the guest is still up, freeing its swap slots, and a freed cluster is discarded, so the export drops to little more than the swap header. The discards trail the exit, so the test watches the export while the machine is still up rather than sampling it once; sampling after shutdown measures nothing.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

MayCXC added 12 commits August 27, 2026 20:43
The reaper that runs at a concurrency of one deletes everything in the
test directory except the unpacked rootfs it means to preserve. It held
that rootfs path as FileManager reports it, under /var, and compared it
against the entries of contentsOfDirectory, which reports them under
/private/var, so the preserved path never matched any entry and the
rootfs was deleted along with the per-test files. The unpack coordinator
still held it as unpacked, so the next test opened a rootfs that was no
longer there and failed with a missing file error.

Resolve both sides with resolvingSymlinksInPathWithPrivate, which exists
for this difference between the two views of the same directory.
Tests guarded for Linux are compiled out on macOS, so `make test` reports
success without having run them, and nothing says so. The target that
does run them is not mentioned anywhere outside the makefile.
The default kernel the tests fetch had drifted from the one users run.
container installs kata 3.28.0 and its 6.18.15 kernel, while these tests
fetched 3.17.0, so the suite exercised a guest with a different feature
set to the one it is meant to represent. Nested runtimes are the visible
case: 3.17.0 was built without nf_tables, so a docker daemon inside a
container fails there and works on what ships.

Kata moved from xz to zstd between those releases, so the archive is no
longer named for its compression and tar is left to recognise it rather
than being told, which also holds if the format changes again.
A test's rootfs clones, init block, and writable layers land in a
directory the runner creates for it and deletes when it finishes, so a
test holds disk only while it runs at any concurrency. The run-shared
directory holds the unpacked images every bootstrap clones from, and is
removed when the run ends.
A container's storage is its rootfs, an optional writable layer, and its
mounts; a machine's is its containers' plus the volumes they share. One
generic shape describes both the Mount values a machine is configured
with and the AttachedFilesystem values it reports once attached, so
converting between the two is a map over the structure and the roles
cannot drift between configuration and attachment.

Device addresses are allocated walking the same sorted order the devices
are created in, on both backends. Consumers read roles instead of list
positions: the spec builders take a container's mounts without prefix
arithmetic, pod volumes mount by name, and a cloud-hypervisor volume's
virtiofsd is held by a machine-lifetime reference alongside its
containers' reference counts.
A container can be given a swap area, so a workload whose memory exceeds
its limit is reclaimed rather than killed. The area is a raw file on the
host attached as a block device, which is how a guest gets swap it can
write to when its own root is read only, and vminitd formats and enables
it while mounting the container's filesystems.

The area is enabled with discard, and the device backing it is marked non
rotational first. virtio block devices are rotational by default, and the
kernel only tracks a swap area in clusters when its device is non
rotational, so without that the discard flags are accepted and no discard
is ever issued. Together they let the sparse file that backs the area
release the blocks the guest stops using, rather than holding the area's
high water mark for as long as the container runs.
The server answers reads, writes and flushes against whatever holds the
export's blocks, so where they are kept is a choice made when the server
is built rather than the one thing it can do. A file is one such place.
Memory is another, and it differs in where the blocks end up when the
host runs short: memory a process holds is pageable, so those blocks go
to the host's own swap and share the pool the rest of the system draws
on, while a file takes space of its own.

Only the chunks written are held, so an export costs nothing until
something is stored in it. Every connection to an export now serves the
one store behind it, which is what lets a client read back what another
wrote, so a client going away no longer closes it.
A swap area named by an export's URL rather than a path reaches the
guest the same way, so what it pushes out is held by a host process
instead of a file. That is where the two differ: memory a process holds
is pageable, so those pages reach the host's own swap and share the pool
the rest of the system draws on, rather than taking storage of their own.

Enabling an area writes a header to it, so the test asks for more than a
guest that swapped nothing could have put there.
An export that never says it takes trims is never sent one, because the
protocol forbids a client from asking otherwise, so a store grew with
every block ever written to it and gave nothing back however much the
guest had finished with. A swap area is rewritten constantly and never
shrinks on its own, so what a guest has done with is most of what an
area holds.

Saying so and acting on it lets a file punch the hole out and a store
in memory drop the chunk, both of which read back afterwards as the
zeroes the protocol calls for.

https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
The flags a client sends with a command sit in the two bytes after the
request magic, which were read past, so an export that said it took
force unit access replied to one before what it wrote was durable. They
are read now, and a command carrying that flag waits.

An export that only reads says so and turns writes away, which the
protocol requires of it and which the pod volume API already offers to
ask for. A request reaching past the end is refused as invalid rather
than left to fail somewhere in the store, and the errors the protocol
names are all spelled out rather than the three that were in use.

Writing zeroes and caching are what a client reaches for beside them:
zeroes are punched out unless the client asks for the range to stay
written through, and a cache hint has nothing to prepare here, so it is
accepted rather than refused.

https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
A client that asks for replies carrying their own framing gets them, and
a read answered that way names the offset it covers and carries a
message with any error rather than a bare number. That framing is what
lets an export be asked how it is laid out, so the context describing
that is offered and answered: a store in memory knows which chunks it
holds and a file is asked through the same seeks a sparse copy uses, so
the holes a client is told about are the ones really there.

Listing the exports names the one on offer. Every connection serves the
one store behind the export, so a flush on any of them covers what was
written on the others, which is the condition for saying that a client
may spread its work across several.

https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
The memory backed test asserts the release as well as the high water
mark: the filler exits while the guest is still up, freeing its swap
slots, and a freed cluster is discarded, so the export drops to little
more than the swap header. The discards trail the exit, so the test
watches the export while the machine is still up rather than sampling
it once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant