Node lifecycle
Full request/response shapes: Daemon API.
Keyframes
A node that is a fork source needs a keyframe — amemory.bin on disk that its children’s MAP_PRIVATE mappings are views of (CoW fork). Roots get theirs at create/adopt time, and that one is a full dump (~7.8 s for a 3 × 2 GiB Ubuntu cluster; there is no ancestor to clone).
Children are cheaper. Forking from a child clones the nearest ancestor’s memory.bin with a filesystem reflink (FICLONE, ~100 µs) and overwrites only the pages that node has dirtied since it was forked. The keyframe costs O(divergence), not O(guest RAM):
Two things to know: the disk figure is the workload’s, not the clone’s — three k3s nodes dirty a fifth of guest RAM in 30 s of doing nothing, and nothing on this path can make a busy node’s keyframe small. And a reflinked file lies to
du and stat (both count shared extents as if the child owned them), so the daemon records the true physical cost at write time — base_disk_bytes on the tree_prepared reply and in tree_nodes — and the disk budget counts that. A child whose tree root is not on a reflink filesystem falls back to the full dump, loudly (wrote_base_incremental: false).
A child does not need its own keyframe to be forked from later if an ancestor’s suffices — has_base_snapshot in tree_nodes tells you which nodes own one.
Budgets
The agent decides what to keep; k7d enforces budgets so the tree doesn’t eat the machine. Every tree carries aTreeBudget:
When a
tree_create* request omits the budget, the daemon applies a documented default sized for a node running a handful of 256 MiB guests: 8 live VMs, 8 GiB RAM, 32 GiB disk, chain depth 1. k7d quickstart passes its own (32 VMs, 16 GiB, 64 GiB, depth 8) rather than letting the default reject a 4-branch batch.
The budget also sizes the tree’s cgroup jail: pids and memory caps derive from it, so there are no separate knobs to keep in sync.
LRU eviction
Under budget pressure, the daemon suspends the least-recently-active unprotected nodes: their delta goes to disk and their RAM is freed. Suspended nodes can be resumed later. Proactive suspension kicks in at 85% of the RAM budget — kept well below OOM territory and kernel reclaim stalls — and before a keyframe is written the daemon makes disk headroom the same way.tree_protect pins a node (a winner you cannot afford to lose) so budget pressure can’t touch it. Eviction never deletes state — only tree_prune does.
The budget/eviction bookkeeping is one of the two formally verified pieces of k7d: the model is translated to Lean via Aeneas and its invariants are machine-checked. See Formal verification.
Node metadata
tree_nodes returns every node’s metadata (NodeInfo): id, parent, children, state, label, dirty_page_count (pages this node’s delta captures; Σ over members for a cluster), has_base_snapshot (the node owns a keyframe), base_disk_bytes, created_at_unix, last_active_at_unix (what LRU ranks on), protected, payload kind (Vm or Cluster), and vm_count — plus, alongside the list, the tree’s live usage against its budget, live_guest_cids per live node, and live_dilation per live node. tree_list carries usage and budget per tree. tree_watch delivers the same snapshot once, then one event per mutation with the daemon’s latency_ns for forks.
Related pages
- GRPO / tree search — the workflow this design serves
- CoW fork — what a “delta” physically is
- Time warp — clock operations on a tree’s clusters
