Return a machine's freed memory to the host while it runs - #894
Open
MayCXC wants to merge 21 commits into
Open
Conversation
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.
A pod's containers share one virtual machine, so they can share one swap area as well, with the guest kernel deciding whose pages are reclaimed to it rather than each container carrying its own. The area is attached with the pod's other mounts and enabled once, after the agent comes up. A container may cap how much of the area it uses. That cap counts swap alone, while the runtime spec carries memory and swap as a single total, so the container's memory limit is added to it when the spec is built. A cap without a memory limit is rejected when the container is added, because the total cannot be worked out without one. Leaving the cap unset lets a container use the whole area, which is what containers sharing a pool generally want.
A pod works out the memory and swap total its containers are capped at, the way the runtime spec asks for it, and sends that along with the rest of the resources. The guest wrote the memory limit and dropped the rest, so a container drew on the whole of the pod's swap area whatever it had been given. Cgroup v2 wants the swap on its own rather than the total, so the memory limit comes back out of it before the limit is written, and the spec's unlimited and unset values are carried across as the controller spells them. Reading the limit back from the kernel is what the test does, since a limit the guest never applied leaves nothing else to see. https://github.com/opencontainers/cgroups/blob/main/utils.go
An unlimited memory limit leaves nothing to subtract from the memory and swap total, so the swap stands as it was given rather than becoming unlimited alongside it. A memory limit that is negative without being the unlimited sentinel is not a limit at all, and is refused rather than read as one. https://github.com/opencontainers/cgroups/blob/main/utils.go
Copying a file between the host and a container is reachable only for a container in a machine of its own; the same container placed in a pod has no way to be copied into or out of, so a tool that offers `cp` loses it the moment a container joins a pod. What the transfer needs is the machine, the path the container's filesystem sits at in the guest, and a vsock port, and only the path differs between the two cases. It moves to a type that takes those, which both a standalone container and a pod's containers hand it. The round trip is covered against a running pod, including that a file copied into one container reaches that container alone.
A container with a machine of its own is given the size it asked for plus the guest agent's, so a container that asks for a gibibyte has a gibibyte and the agent runs beside it. A pod hands its size to the machine as it stands, so the agent comes out of what the pod asked for and its containers are left with less: a pod given twelve gibibytes reports eleven and a half. The pod's size and the agent's are added the same way, and what a pod was given is what its containers have.
A container with a machine of its own can be given a block device to write into, with its image mounted read-only beneath, so the image is left as it is and every write lands in the layer. The same container placed in a pod has nowhere to be given one. The layer is attached with the container's other block devices, mounted in the guest as the upper layer of an overlay over the image, dropped from the mounts the runtime spec carries, and unmounted with the container. A pod's containers take one whether they were placed before the machine booted or hotplugged into it afterwards.
The file carried the sibling repository's copyright line, so the license header check found no header it recognized.
Stopping a container tears down what it was running and keeps its place: the name still answers for it, and placing another container under it is refused. Removal is the separate act the runtime specification names for giving the place up, taken once the container has stopped, so a name can run again in a machine that outlives what it last ran. https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto
Pages a guest has freed stay with the machine, because the guest has no way to say it is done with them. They leave only when the host runs short and reclaims them as it would any cold memory, which pages out what the guest would have handed over for nothing. Attach a memory balloon to each virtual machine and expose the size the machine should hold as `setTargetMemorySize`. Lowering it hands memory back to the host; raising it, up to the size the machine was created with, returns memory to the guest. Backends without a balloon report the operation as unsupported, in the same shape as pause and hotplug. The guest keeps reporting the same total while the balloon holds its pages, and shows them as no longer free, because the balloon driver has to account for them in case it is asked to give them back.
Attach a balloon to the machines the cloud hypervisor backend creates, with free page reporting turned on, so the guest hands back the pages it frees and the host reclaims them without anything having to choose a balloon size. Kata drives the same device the same way, in src/runtime-rs/crates/hypervisor/ch-config/src/convert.rs. The type follows the Cloud Hypervisor OpenAPI spec, which carries size alongside deflate_on_oom and free_page_reporting.
Cloud Hypervisor sizes the balloon rather than the machine, so the balloon is asked to hold whatever the machine should not, and both backends answer the same request the same way. The resize request follows the Cloud Hypervisor OpenAPI spec, which carries desired_vcpus and desired_ram alongside desired_balloon.
Virtualization asks for the guest to be compacted before the balloon is driven, so that the pages it gives up sit together well enough to be worth taking. Asking a machine to hold less memory now does that first, which puts the recommended order in the one place that drives the device rather than leaving each caller to remember it. The agent already carries the setting the guest needs, so it is the interface that grows rather than the protocol between the two halves. Memory a guest has never touched is not backed on the host, so a balloon that takes only those pages moves nothing at all. The test now fills a tmpfs and frees it, leaving the machine holding pages the guest no longer wants, which is the state the device exists to resolve. https://developer.apple.com/documentation/virtualization/vzvirtiotraditionalmemoryballoondevice
A pod's containers share one machine and one balloon, so bounding them together is the same operation a single container's machine takes, and the pod had no way to ask for it. The machine gathers the guest's free memory and drives the device, since dialing the agent and holding the balloon both already belong to it.
Attaching a balloon leaves the question of what to set it to. The guest cannot answer it: having freed memory, it has no way to say so, and the pages stay charged to the machine. So the reading is taken on the host, from the anonymous memory the guest reports holding, and the rest is asked for back. Reclaiming too far is the failure worth avoiding, because the guest then fetches back pages it was still using. The count of those refaults is already reported, so a rise in it since the last reading is treated as evidence the last target was too tight, and memory is given back rather than the reading being trusted. The decision is a plain function of one reading, so it is tested without a virtual machine.
A loop looks at what a machine's containers hold on a cadence and asks the machine to hold that plus headroom, backing off when the guest refaults, so memory a workload frees goes back proactively instead of waiting for host pressure. The knob is proactiveMemoryReclaim on the pod and container configurations, off by default; the cloud-hypervisor backend reports freed pages continuously either way. The reclaimer reports what it has done, which is the deterministic ground the integration tests assert: the loop follows a fill-and-free with targets while a busy guest keeps what it uses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Attaching a balloon leaves the question of what to set it to. The guest cannot answer it: having freed memory, it has no way to say so, and the pages stay charged to the machine. So the reading is taken on the host, from the anonymous memory the guest reports holding, and the rest is asked for back.
Reclaiming too far is the failure worth avoiding, because the guest then fetches back pages it was still using. The count of those refaults is already reported, so a rise in it since the last reading is treated as evidence the last target was too tight, and memory is given back rather than the reading being trusted. The decision is a plain function of one reading, so it is tested without a virtual machine.
On top of that sits a loop that looks at what a machine's containers hold on a cadence and asks the machine to hold that plus headroom, backing off when the guest refaults, so memory a workload frees goes back proactively instead of waiting for host pressure. The knob is
proactiveMemoryReclaimon the pod and container configurations, off by default; the cloud-hypervisor backend reports freed pages continuously either way. The reclaimer reports what it has done, which is the deterministic ground the integration tests assert.Dependency and Merge Order
This targets stock
apple/containerization:main. Merge order: #863, #872, #878, #885, #886, #893, then this. The head carries their commits because all of them target stockmain; after they merge, this branch takes currentmainand its remaining diff is the two commits above.Motivation and Context
Discussion: #882, where I ask the question this PR is the answer to and would rather have settled first: does a reclaim policy belong in this library at all, or should the library expose
setTargetMemorySizeand the guest's memory reading (#893) and leave the policy to whatever drives it? If the answer is the latter, this PR should be closed and the mechanism in #893 taken alone.Off-by-default behind a configuration flag is the shape I chose given that uncertainty.
Testing
make containerization,make check, andswift testclean, including unit coverage of the target decision with no virtual machine involved.make integration: the loop follows a fill-and-free with targets, while a busy guest keeps what it uses.Type of Change