mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[GH-ISSUE #440] [Design Proposal] Online node upgrades without interrupting running sandboxes #108
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#108
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @yingdi-shan on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/kvcache-ai/AgentENV/issues/440
Affected component
Cross-cutting / other (Orchestrator / sandbox lifecycle, ublk / ublk-daemon, Firecracker runtime, API server / proxy, Network, Build / installation)
Problem statement
Upgrading AgentENV on a node interrupts every sandbox running on it: sandboxes are paused (or lost), and every envd stream, WebSocket, and egress connection breaks. Releases therefore cost users availability, and even a one-line ublk-daemon fix costs as much as a full release.
The cause is that all runtime state of a running sandbox belongs to the server container or ends with it:
kill_on_drop(true), and they live in the server container's cgroupsrc/privileges.rs:111,src/sandbox/firecracker/instance.rs:570Recoveryisunimplemented!()storage/ublk-daemon/src/main.rs:341,storage/ublk/src/main.rs:35runtime_path(/run/aenv), container-local in Kubernetesconfig/default.toml:13,src/sandbox/network/manager.rs:91atexithookdocs/src/internals/networking.md:200:8000listener inside the serversrc/sandbox/network/egress_proxy.rs,src/api/proxy.rssrc/orchestrator/store/in_memory.rsAENV_NODE_IDis the pod name, which changes on every pod replacementdeploy/k8s/base/agentenv-daemonset.yamlRelated: #365 (durable-data compatibility during rollout), #302 (pooled ublk devices), roadmap §7 (runtime stability).
Use case
Kubernetes deployments (
deploy/k8s/base) hosting long-lived, interactive agent sandboxes that stream long commands through envd or expose WebSocket services through the proxy. Components change at different rates: the server every release, the ublk daemon / OverlayBD often and independently, Firecracker occasionally, the pod spec and host kernel rarely.Current behavior and workarounds
The pod is deleted, the node becomes
LINGERING,preStopwaits (up to 3600 s) for sandboxes to finish, SIGTERM pauses the rest, and the replacement pod reloads them under a new node ID. Auto-resume sandboxes come back on their next request.This falls short because:
preStopuses up the grace period, pause-all gets about 2 s and running sandboxes are lost;Paused;maxUnavailable: 1, a cluster rollout can take hours per node.Desired behavior
Server upgrade: no Firecracker process restarts (same pid and boot ID), no sandbox changes state, existing connections keep flowing, the node ID stays the same, and API calls during the handover either succeed or return a retryable status.
ublk-daemon upgrade: devices move to the new daemon one at a time with a bounded stall (target p99 ≤ 100 ms per device), zero guest I/O errors, no data loss in writable uppers, and shared memory devices keep their reference counts and COW.
Firecracker upgrade: new VMs use the new binary and existing VMs keep theirs. Optional forced migration pauses and resumes sandboxes one at a time, keeping their IDs.
Failures: a
kill -9at any handover step converges to exactly one owner per component, no orphaned processes, devices, netns, or runtime dirs, and every sandbox inRunning,Paused, or a terminal state. Each component can roll back from N to N-1 on its own.Proposed approach
1. Pod layout
Kubernetes can change a container's image on a live pod and restart only that container, while the pod's netns stays alive. Split the pod so the VMs never live in a container that gets upgraded:
vmmis an idle container that owns the cgroup and mount namespace of every Firecracker process. The server still sets up each VM as today but creates it withclone3(CLONE_INTO_CGROUP)intovmm, so restarting the server no longer kills VMs. The pause process reaps orphans, and kubelet still accounts for VM resources./var/lib/aenvstays on hostPath;runtime_pathmoves to a sharedemptyDirwithBidirectionalpropagation. The pod netns, veths, and iptables rules survive container restarts, and the pod name (the node ID) stops changing on routine releases.2. Runtime records and adoption
Running, persist a versionedRuntimeRecord(LocalKvStore,Sync) and update it on every transition. It holds lifecycle metadata, the Firecracker pid, start time, version, and socket, the envd address, netns slot, and egress policy, the ublk devices, and the volume mounts.Runningwithout calling extensionstartagain; tear down or recover partial ones; sweep anything without a record, and discard warm-pool slots and pooled devices.3. Server handover
Running), flushes the records, and passes its listener fds (:8000and the egress listeners) overSCM_RIGHTS.flock+ epoch). N+1 takes it, adopts the sandboxes, and accepts on the inherited listeners, so no connection is refused. Requests that arrive during adoption wait for a bounded time.4. ublk handover
This uses the kernel's ublk server-upgrade support (
UBLK_U_CMD_QUIESCE_DEV, documented in the 6.17 uapi for "upgrade ublk server application").UBLK_F_USER_RECOVERY | UBLK_F_USER_RECOVERY_REISSUE | UBLK_F_QUIESCEwhen supported.UBLK_F_QUIESCEmust be added to the vendoredlibublk-rs-sys, asUBLK_F_UPDATE_SIZEis instorage/ublk/src/ublk_caps.rs./dev/ublkcN; the new daemon runsSTART_USER_RECOVERY…END_USER_RECOVERY. If that fails, the old daemon takes the device back and the handover aborts.REISSUEcan replay writes, so OverlayBD upper writes, flushes, and discards must be idempotent (verified in P0).QUIESCE, restart a single daemon with one stall across all devices; withoutUSER_RECOVERY, pause and resume sandboxes one at a time.5. Firecracker and rollout
vmmimage never changes for Firecracker releases.vmm, and kernel changes still recreate the pod using today's drain path.OnDeleteplus a small rollout controller (possibly inservices/) or OpenKruise Advanced DaemonSet. Settings inAENV_*env vars move to the mounted config file so routine changes don't recreate pods.6. Delivery plan
AUTO_BUF_REG); spawning intovmm; in-place restarts and rollout mechanismfio --verifyclean, kernel floor, spawn path and rollout mechanism chosenRuntimeRecord, adoption, orphan sweep, shared runtime dirkill -9of the server at any lifecycle stage re-adopts VMs with no state changevmmcontainervmm; single server slot updated in placeOrder: P0 → P1 → P2 → (P3 ∥ P4) → P5 → P6. P3 goes first if ublk releases are the priority.
Compatibility and operational impact
API/config changes: The public API is unchanged. A new
[upgrade]section holds slot identity, deadlines, request hold time, and drain cap.runtime_pathmust point to the shared volume in Kubernetes,AENV_*settings move to the config file, and the gateway retries the handover status.Snapshot or storage format changes: None to snapshots, templates, or repositories.
RuntimeRecordis node-local derived state. Devices from earlier builds lack recovery flags, so the first rollout needs one drain per node; it also changes the pod spec.New host/runtime requirements: A kernel with
UBLK_F_QUIESCEfor per-device handover (older kernels use the fallbacks);shareProcessNamespace, aBidirectionalemptyDir, and a rollout controller or OpenKruise; memory for one idle standby slot per component.Upgrade and rollback considerations: Each component rolls back independently to N-1 by updating its standby slot. An aborted handover leaves the old slot in charge.
vmmneedsoom_score_adj=-1000,memory.oom.group=0, and no liveness probe, because restarting it kills every VM.preStopmust stay shorter than the grace period so pause-all can finish.Alternatives considered
aenv-runtimed): works, but adds a service, an RPC layer, and a second DaemonSet.hostNetwork,hostPID, systemd scopes): one container, but ties AgentENV to host systemd, hides VMs from kubelet, and pod deletion no longer cleans up.execveself-restart, CRIU, or passing protocol state: loses in-process HTTP/2 and WebSocket state, bypasses image rollout, or breaks with io_uring, ublk, and KVM fds.Non-goals
Contribution
I can provide more design details
Pre-submission checklist