Repository navigation
Conversation
t.TempDir() on macOS produces a path longer than the unix socket limit, so binding the fake QMP socket failed there.
Delete closes the pooled guest-agent connection before the graceful shutdown RPC, and the RPC opens it again. Nothing closed that second connection, so each delete of a running instance left a gRPC client and its goroutines in the pool.
A start that fails after connecting to QMP kills the process but left its client in the pool.
Shutdown and DeleteVM clear the cached balloon target, but a VM that was deleted or shut down by its guest kept it. The entries stayed for the life of the process, and a later start on the same socket path read the previous boot's target back.
startedProcess is also used for swtpm, where the reset ran against a socket path that is never pooled.
A guest that powers off or crashes by itself never goes through stop or delete, so its balloon target stayed cached. The next start on the same socket path reported that target for a process whose balloon was never set. The start path now drops the target with the pooled client, and resetClient, which only dropped the client, is no longer needed.
Cloud Hypervisor keeps the same per-socket balloon target cache as QEMU and only cleared it in Shutdown and DeleteVM. Register a releaser so stop and delete clear it, and clear it when a new process starts so a VMM that exited by itself does not hand its target to the next boot.
For Cloud Hypervisor the hook only clears a balloon target, so there is no client involved.
The cache is keyed by socket path, device and inode, so a recreated socket should miss. On a miss Load fell back to the key last indexed for the path and returned the old process's target anyway. Keep that fallback for the case it is needed for, a socket file that is missing while its VMM is still running.
The start test now reads the target back with the socket file removed, which finds a leftover on any filesystem. Both tests drop what they cached when they fail early.
This branch has not been deployed
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
Deleting a VM leaves two pooled connections and a cached balloon target behind in hypeman. None of them is dropped until hypeman restarts.
QMP client. Hypeman keeps one QMP client per QEMU socket in a process-wide pool (
lib/hypervisor/qemu/pool.go). Pool entries are only dropped byhv.Shutdown(), a QMP error, or a later start that reuses the same socket path. Delete never calls any of those. It shuts the guest down over vsock or kills QEMU directly, and the socket path is unique per instance. So every deleted QEMU VM leaves its QMP client in the pool, which keeps the unix socket fd open. Stop has the same gap when the guest shuts down by itself.Guest-agent connection. Delete closes the pooled guest-agent gRPC connection, then sends the graceful shutdown RPC, which opens and pools it again. Nothing closes that second connection. A gRPC client whose peer is gone keeps three goroutines. Stop and standby already close it after the VM exits. This one is not specific to QEMU.
Balloon target. QEMU and Cloud Hypervisor cache the last balloon target per socket path and only clear it in
Shutdown()andDeleteVM(). A VM that is deleted, or whose guest shuts down or crashes by itself, keeps the entry. When that VM is started again on the same socket path, hypeman reports the previous boot's target for a process whose balloon was never set, and the memory controller only writes a target when it differs from the cached one.On a production GPU host, the count of dead unix connections (
ESTAB, no local path, peer inode 0) tracks the count of deletes since the last hypeman restart: 880 connections against 882 deletes over about 29 hours. Hypeman goroutines on QEMU hosts climb by about 2.5 per deleted VM, and they only reset when hypeman restarts.The goroutines come from the guest-agent connection, not from QMP. QEMU closes its QMP socket cleanly both when it exits and when it is killed, so go-qemu's reader goroutines exit by themselves and only the fd stays open.
Changes
hypervisor.RegisterVMMStateReleaser/hypervisor.ReleaseVMMState: a per-type hook that drops what a hypervisor type caches for a socket whose VMM has exited. Hypervisor types that cache nothing ignore it.resetClient, which only dropped the QEMU client at start, is replaced by this.BalloonTargetCache.Loadno longer hands a recreated socket the previous target. The cache is keyed by socket path, device and inode, but on a missLoadfell back to the key last indexed for the path, which is how the old process's target came back. The fallback now only applies when the socket file is missing, which can happen while its VMM is still running. On a filesystem that reuses the freed inode the key itself still matches, so the clear at start is needed as well.deleteInstanceWithOptionsreleases the client once the VMM is confirmed gone, whether through graceful guest shutdown or kill, and closes the guest-agent connection again at the same point. A failed kill still returns before this.stopInstancereleases the client next to the existing stale-socket cleanup. Standby and the stop fallback already evict throughhv.Shutdown(), so they don't change.Testing
Leaked per deleted VM, measured with real QEMU 7.2 under KVM (4 VMs per mode, goroutines classified from a goroutine profile):
SkipGracefulShutdownmainAll leaked goroutines were
grpcsync.(*CallbackSerializer).run, and none were in go-qemu. With only the QMP release and not the guest-agent close, the fds were gone and the 3 goroutines per graceful delete remained. The scratch test that took these numbers is not part of this PR, and the numbers predate the balloon-target changes.Balloon target after the guest exits by itself and the instance is started again, measured with real QEMU 7.2 and Cloud Hypervisor v51.1 (2048 MiB assigned, target set to 1536 MiB on the first boot):
These were taken before the
Loadfallback change, on a filesystem that does not reuse inodes.TestReleaseVMMStateClosesPooledConnection(qemu): a pooled client connected to a fake QMP server is removed from the pool, the server sees the connection close, and the cached balloon target is gone. Covers both QEMU types. It fails if the releaser isn't registered or doesn't clear the target.TestDeleteAndStopReleaseVMMState(instances): delete and stop of a running instance each call the registered releaser with the instance socket. Each subtest fails if its call site is removed.TestDeleteClosesGuestConn(instances, Linux): deleting a running instance closes the guest-agent connection that the shutdown RPC pooled. It fails without the new close.TestCleanupFailedStartDropsPooledClient(qemu): the failed-start cleanup removes the pooled client. It fails without the release. The call fromstartQEMUProcessinto that cleanup is not covered by a test.TestStartQEMUProcessDropsStaleBalloonTarget(qemu): starting a real QEMU process on a socket path with a cached target leaves no target. It is skipped when QEMU is not installed, and fails without the release at start.TestBalloonTargetCacheMissesForRecreatedSocketandTestBalloonTargetCacheSurvivesMissingSocket(hypervisor): a recreated socket misses, and a missing socket still finds its target. The first fails with the old unconditional fallback, the second with no fallback at all.TestReleaseVMMStateForgetsBalloonTarget(cloudhypervisor): the releaser clears the cached target. It fails if the releaser isn't registered. The clear on Cloud Hypervisor's start path has no unit test; the table above is its only check.go vet ./lib/hypervisor/... ./lib/instances/is clean, andgo test -racepasses for the new tests and the existing lifecycle, stop and delete unit tests.Not covered
A guest RPC that races a delete can still create and pool a guest-agent connection after delete's second close. Exec closes it again when the dial to the dead guest fails. The stat and cp handlers do not. That race predates this PR.