Skip to content

fork: hardlink snapshot mem-file into snapshot forks#221

Draft
sjmiller609 wants to merge 5 commits into
mainfrom
hypeship/snapshot-fork-share-memfile
Draft

fork: hardlink snapshot mem-file into snapshot forks#221
sjmiller609 wants to merge 5 commits into
mainfrom
hypeship/snapshot-fork-share-memfile

Conversation

@sjmiller609
Copy link
Copy Markdown
Collaborator

Summary

  • Snapshot forks now hardlink the source mem-file into the fork's data dir instead of sparse-copying it.
  • The mem-file is skipped from the directory walk via CopyOptions.SkipRelPaths, then os.Link'd into place after the copy returns. Dodges the multi-GB sparse copy and the directory-walk overhead in one step.
  • Falls back to the normal copy when no raw mem-file is present (e.g. compressed-only snapshot whose decompression failed, or a snapshot kind that doesn't carry guest memory).

Why this is safe

  • Snapshot mem-files are immutable.
  • The hypervisor mmaps them MAP_PRIVATE on restore, so fork writes never reach the underlying file — all forks of a snapshot can share the same inode.
  • Hardlinks survive snapshot deletion via inode refcount, so deleting a snapshot never strands a running fork.
  • Same-FS guarantee holds: snapshot dir and fork dir are both under hypeman's data dir.

Stack

Test plan

  • go test ./lib/instances/ -run TestForkSnapshotHardlinksRawMemoryFile
  • go test ./lib/instances/ -run TestForkSnapshotFromCompressedSourceCopiesRawMemory still passes (compressed source still gets a real raw file in the fork)
  • go test ./lib/forkvm/
  • Manual: fork a Standby snapshot, confirm stat -c %i matches between the source mem-file and the fork's mem-file

When a Firecracker fork descends from a Template source, skip copying the
snapshot mem-file and hardlink it to the source's instead. Firecracker
mmaps the mem-file MAP_PRIVATE on restore, so all forks COW from the same
backing inode — no per-fork copy required.

Hardlink rather than symlink: firecracker's restore path temporarily
aliases the source data dir to the fork data dir while loading the
snapshot (withSnapshotSourceDirAlias). A symlink whose target traverses
the source dir would resolve back into the fork dir during that window
and trip ELOOP; a hardlink resolves by inode so the alias has no effect
on it. Hardlinks require both paths on the same filesystem, which holds
for our standard data-dir layout.

Gated to Firecracker only because other hypervisors (cloud-hypervisor,
qemu, vz) don't share MAP_PRIVATE semantics on their snapshot layouts.
Restricted to Template sources because they are explicitly promoted as
fork-only and can never be restored — sharing the mem-file with a
non-Template source would let a later RestoreInstance mutate the file
out from under live forks.

Stacked on hypeship/template-as-state so the Template state both gates
"this snapshot is safe to fan out from" and lets fork counts be derived
at read time.
The two TestForkFirecracker* tests invoke forkInstanceFromStoppedOrStandby
which needs a hypervisor VM starter — firecracker is linux-only, so these
fail on darwin with 'no VM starter for hypervisor type: firecracker'.

Split into _linux_test.go; leave the pure-helper TestInstallForkSharedMemFile_*
tests cross-platform.
Snapshot forks copy the source guest dir into the fork instance dir;
the dominant cost is the multi-GB mem-file. Hardlink it instead and
skip the file from the directory walk via CopyOptions.SkipRelPaths
(introduced for template forks).

This is safe because:
- snapshot mem-files are immutable
- the hypervisor mmaps them MAP_PRIVATE on restore, so fork writes
  never reach the underlying file
- hardlinks survive snapshot deletion via inode refcount, so a deleted
  snapshot never strands a running fork

Falls back to the regular copy walk when no raw mem-file is present.
@sjmiller609 sjmiller609 force-pushed the hypeship/fork-shared-memfile branch from d46be7a to 7b799f7 Compare May 13, 2026 18:13
@sjmiller609 sjmiller609 force-pushed the hypeship/snapshot-fork-share-memfile branch from 6d875e1 to 06abd04 Compare May 13, 2026 18:25
@sjmiller609 sjmiller609 force-pushed the hypeship/fork-shared-memfile branch from 7b799f7 to 355ad7f Compare May 13, 2026 20:15
@sjmiller609 sjmiller609 force-pushed the hypeship/snapshot-fork-share-memfile branch from 06abd04 to 13f3003 Compare May 13, 2026 20:16
@sjmiller609 sjmiller609 force-pushed the hypeship/fork-shared-memfile branch from 355ad7f to a45d471 Compare May 13, 2026 20:39
@sjmiller609 sjmiller609 force-pushed the hypeship/snapshot-fork-share-memfile branch from 13f3003 to 7992660 Compare May 13, 2026 20:40
@sjmiller609 sjmiller609 force-pushed the hypeship/fork-shared-memfile branch from a45d471 to 8b0000c Compare May 14, 2026 18:55
@sjmiller609 sjmiller609 force-pushed the hypeship/snapshot-fork-share-memfile branch from 7992660 to 1bfb7a6 Compare May 15, 2026 14:32
@sjmiller609 sjmiller609 force-pushed the hypeship/fork-shared-memfile branch 2 times, most recently from 0f0137b to ac6bfde Compare May 19, 2026 19:43
Base automatically changed from hypeship/fork-shared-memfile to main May 19, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant