Skip to content

Hyperlight helper for Fiberd

Hyperlight helper for Fiberd

A Lampyr home is a grant Pod of fiberd-k8s-controller running Fiberd’s release image (grantImage in lampyr-install). Fiberd’s stock hyperlight backend drives a helper process over a line protocol (PROTOCOL.md) on fd 3. Lampyr ships that helper, lampyr-hluk-helper, as the image ghcr.io/hyperlight-dev/lampyr/hluk-helper (helperImage), and ly deploy seals each Revision’s snapshot into a Fiberd template the helper restores (ADR 0006).

Grant Pods no longer run Lampyr’s own Fiberd build (cmd/fiberd-home) or its Go backend (internal/fiberdintegration/hluk), which pulled the snapshot artifact itself and started one hluk snapshot run process per Fiber; both have been removed, along with the grant issuer that signed their grants.

Inside a home Pod

One grant has one home Pod. Lampyr’s serving controller names the helper image in the grant’s helperImage, and fiberd-k8s-controller puts the helper there: an init container copies /fiberd-helper/helper from the image into a shared emptyDir. Fiberd, since commit 549f616 (see its PROTOCOL.md), pulls the grant’s template, unseals it, unpacks the hluk snapshot read-only, and starts the helper with --template-dir <dir> once the helper advertises the template-dir capability (below). Only the helper’s tests run it by hand against a template directory.

The helper loads the snapshot once and answers READY with the four facts its snapshots depend on (the helper with its hyperlight-unikraft commit, the hyperlight_host crate, the hypervisor and the CPU vendor). Fiberd records them as the platform of every park, so a park only resumes under a matching helper. A template built by another hyperlight-unikraft commit or on another CPU vendor is refused at warm.

--version prints the same four facts on its first line and template-dir on a second. That line is how Fiberd tells a helper that restores the template it unpacks: it passes --template-dir only to a helper that prints it, and reads the facts from the first line alone. ly build runs the helper’s --version where the snapshot is made, refuses a helper without the line, and records the first line in the artifact’s metadata as runtime.fiberdHelper, which zygotectl seals with. Being metadata, the facts are part of the Revision ID, so a build for another helper is another Revision. The helper takes its flags as Fiberd spells them: one or two dashes, and a value after = or as the next argument.

Each CLONE restores a sandbox from the shared snapshot on a thread of its own, starts the app with PORT set, and answers CLONED once the app listens and the Fiber’s endpoint is bound. PARK writes the sandbox to the directory Fiberd names, RESUME restores one from a park, and KILL ends it. Every end sends EXITED, after the sandbox’s memory is freed. KILL interrupts the guest until the Fiber has ended, so it also ends a guest that spins as it serves, or as a CLONE or RESUME restores it, before it answers.

Reaching a Fiber

Fiberd names a unix socket as each Fiber’s endpoint, and a hyperlight-unikraft guest serves on a host TCP socket, so the helper bridges the socket to the app’s port. Fiberd’s host relays a TCP port to each unix endpoint, because the hyperlight backend serves unix only, so fiber-router keeps dialing Fibers over TCP. Grant Pods run Fiberd’s image, which relays on 30000-32767, and the grant Pods’ NetworkPolicy (lampyr-grant-homes) opens only that range and Fiberd’s API to fiber-router.

The app’s own port comes from the helper’s range, 10000-19999 by default, outside the kernel’s ephemeral ports and Fiberd’s endpoint range. A port belongs to one Fiber at a time, and a Fiber serves only once every listener on its port is the helper’s own; each connection checks those listeners are still the guest’s. Guests get no network egress, so a Fiber cannot reach another’s port. Apps that bind 0.0.0.0 are still reachable on the Pod’s address until hyperlight-unikraft can bind guest listeners to loopback (hyperlight-unikraft#255); the Pod’s NetworkPolicy keeps that range closed meanwhile.

Who a Fiber is

Before its endpoint exists, a Fiber’s fence and generation are in /run/fiberd/incarnation and a CLONE payload in /run/fiberd/payload, a read-only mount. hyperlight-unikraft reseeds the guest’s random generators from host entropy on every restore, so every clone draws different bytes. Fiberd’s seed is checked and dropped; it never reaches the guest, the log or a park.

Latency

In fresh mode, Lampyr’s default, every request clones a Fiber, so clone to serving is what a request waits for. With the release build that time is about 40-60 ms for Rust, 120-190 ms for Node.js and 330-440 ms for Python on a busy host, no slower than the old backend’s hluk snapshot run. Python’s is its app starting after the restore; a snapshot warmup that does more of that work shortens it.

A clone without a deadline of its own gets Fiberd’s fixed 500 ms, which Python clones miss once several start at once, and the clone then fails rather than waits. Fiberd#97 sizes the default from a probe clone instead.

Bounded restores

Fiberd’s hyperlight backend does not cap concurrent restores (only its gVisor backend has MaxRestores), and a restore is CPU-bound until its app listens. An unbounded burst of cold requests starves the grant Pod: every restore slows, all of them miss their deadlines, and the Fibers already serving get no CPU. Lampyr’s own Fiberd home bounded restores for this; grant Pods run stock Fiberd, so the helper bounds them instead, where it can tell a restore from the rest of a Fiber’s life.

At most --max-restores Fibers restore at once, by default two per CPU the helper may use, at most 16. A grant Pod requests CPU without a limit, so that is two per node CPU; a CPU quota on its cgroup would lower it. A CLONE or RESUME waits for a slot in arrival order, and its deadline counts from its arrival, the wait included. From the restores that finished, the helper keeps a running average of how long one takes, from its slot to its app listening; a restore that timed out counts as a lower bound, and one that failed or was killed not at all. A command whose deadline cannot cover the restores ahead of it and its own is refused at once with an ERROR, which Fiberd takes as a failed birth, as is one left too little time after its wait. A helper with nothing restoring or waiting never refuses, so a stale estimate cannot shut it out. Fiberd’s probe, the first CLONE after READY, which it times to size its default deadlines (four times the probe, floored at 500 ms for a create and 2 s for a resume, capped at 5 s), never waits and is never refused, so the deadlines it sizes are a restore’s alone. A KILL can overtake a birth, a queued one too: the Fiber leaves the queue and answers EXITED without restoring.

Memory

W, what a Fiber dirtied since the warm snapshot, is hyperlight-unikraft’s count (AppSandbox::dirty_bytes, from hyperlight’s scratch allocator) since its sandbox was last restored or snapshotted, plus, for a resumed Fiber, what its park recorded, and, when a sync park failed after its snapshot was taken and the Fiber serves again, what was counted before that snapshot. The helper reports it with CLONED, then between guest steps, when it has changed, at most every 100 ms, so a guest still inside one step is held only by the grant’s memory ceiling. A count that cannot be read means the sandbox is broken: the CLONE or RESUME is an ERROR if it has not answered yet, a serving Fiber ends with EXITED <fence> exit:1, and a PARK that cannot read it before its snapshot is an ERROR, as any failed park is.

The count is how far the guest’s page allocator has moved, not what the guest wrote. W after a clone includes everything the app wrote as it started (about 23 MiB with the helper’s Python test fixture). What the kernel writes on a restore to resume the guest is not counted, but W still grows as a resumed app runs again (with the Python fixture, about 1.4 MiB idle and 1.7 MiB more on its first request). After a sync park that failed after its snapshot was taken, what the kernel writes to pick up again on the next entry is counted (with the Python fixture, W grew about 4.4 MiB by the end of the next request). hyperlight-unikraft’s own figures for a small guest function call are about 0.6 MiB after a restore and 1.3 MiB after a snapshot. The count runs ahead of what the guest wrote by up to about 1 MiB, as the kernel’s frame allocator takes its memory a chunk at a time, and by more around a large allocation, whose chunk is several times its size. And it adds up across a Fiber’s parks and resumes: what a resumed guest writes counts again, pages it had written before included. Lampyr’s serving controller and ly set a grant’s wBudget to the service’s memory. Since W can count more than the Fiber uses, a Fiber can reach it without using that much, above all one parked and resumed many times, so the service’s memory needs headroom.

The guest controls the count: a grant’s wBudget stops a Fiber that reports too much, but one that reports too little is held only by the grant’s memory ceiling. That ceiling applies to the grant’s cgroup, which holds the whole helper: pressure there can make Fiberd refuse new clones, and going past it ends the helper, and with it every Fiber of the grant, rather than refusing one clone.

Orphaned Fibers

The helper keeps a Fiber until Fiberd ends it with KILL; it never times one out itself. Fiberd does not reclaim a single unreleased Fiber either: its lease covers the whole grant. So a Fiber that fiber-router loses track of, after a router restart, keeps its slot under fibers.max and its memory until the grant is yielded or revoked. Reclaiming it needs fiber-router to release what it held, or Fiberd to bound how long a Fiber may go unreleased.

Limits

  • Each park adds a memory layer for the pages no earlier layer holds (those the Fiber wrote since its last snapshot), and an earlier layer stays while it still backs a mapped page; hyperlight 0.19 caps a snapshot at 30 data layers. A Fiber whose every park-and-resume cycle keeps pages no later cycle rewrites can no longer be parked after about 28 cycles; one that keeps nothing levels off well below the cap. A clone, which starts from the warm snapshot, is unaffected.
  • Fibers are threads of one helper process, so they share the grant’s cgroup, and a helper crash ends every Fiber of the grant. Moving to a process per Fiber, each in its own cgroup leaf, waits on how Fiberd hands a helper per-Fiber cgroups.
  • KVM only: hyperlight-unikraft is built without MSHV.