Repository navigation
Commit b99eb5f
authored
Layered/incremental snapshots (#1926)
## Summary
Snapshots share immutable memory layers. A capture copies only the pages
changed since its parent snapshot, and `Sandbox::snapshot` switches the
sandbox to the capture so the next one is incremental too. This makes
`snapshot()` much faster for larger sandboxes. It also improves memory
density: each snapshot holds only its changed pages and shares the rest
with its parent, where `main` holds a full copy of guest memory per
snapshot. See [HIP
0003](proposals/0003-incremental-snapshots/README.md).
Deviations from the merged HIP 0003 draft (the HIP is updated):
* The VM maps each layer whole, one mapping per layer. A layer stays in
a snapshot while it gives at least one page. Its pages that a newer
layer replaced stay mapped, so a guest can read data it overwrote before
a snapshot from the older layers of that snapshot.
* Each blob holds guest data followed by the page tables of the snapshot
that created it, as on `main`. A restore copies the newest page tables
into scratch.
* `Sandbox::snapshot` installs the capture, because otherwise every
later capture copies all pages changed since the last restore again.
Installing removes `map_region` and `map_file_cow` regions, because the
snapshot holds their mapped pages and later blobs may reuse their
addresses.
* Snapshots from earlier versions are rejected instead of loaded as one
layer. This is simpler, and there is little reason to keep backwards
compatibility here.
## Changes
* Snapshot memory is a list of immutable shared blobs, one per layer,
each mapped whole.
* Capture keeps parent layers that still give a page and copies changed
pages into one new blob.
* `Sandbox::snapshot` installs the capture into the running sandbox.
Scratch pages it frees are zeroed.
* The OCI format stores ordered data layers and a transport layer
(config v4, ABI 7). The config names the layer whose page tables a
restore uses.
* Snapshot performance: adjacent page-table walk mappings are merged,
and page-table pages are cached during capture.
* KVM: `KVM_X86_QUIRK_SLOT_ZAP_ALL` is disabled where supported, so
deleting a memory slot flushes only that slot.
* New benchmarks `snapshots/call_and_create` and
`snapshots/restore_alternate` cover the cost moved into the next call
and restores to a non-current snapshot.
## Breaking changes
* `Sandbox::snapshot` removes `map_region` and `map_file_cow` regions
from the sandbox, which only `restore` did before. Region pages the
guest has not mapped become unreachable at the snapshot instead of at
the next restore. Snapshots hold the same pages as before.
* `PtRootFinder` receives a `SnapshotMemoryReader` and returns
`Result<Vec<u64>>`.
* A snapshot holds at most 30 memory layers, and a capture fails when it
would exceed this. This bounds restore cost by limiting the number of
hypercalls to map memory.
## Commits
1. Add `call_and_create` and `restore_alternate` snapshot benchmarks.
Lands first so the benchmarks run before and after.
2. Implement layered and incremental snapshots.
3. Install captured snapshots into the sandbox.
4. vmem: merge adjacent mappings in `walk_va_spaces`.
5. Cache page-table pages in `PageTableReader`.
6. Disable `KVM_X86_QUIRK_SLOT_ZAP_ALL`.
7. PR feedback.
## Benchmarks
Compared to `main`. `restore`, `restore_alternate` and `call_and_create`
compare to the first commit, which adds the last two.
* MSHV: Linux 6.6 mshv.
* KVM: Linux 6.17.
### Highlights
| Benchmark | MSHV | KVM |
|---|---|---|
| `snapshots/create/default` | 440 µs → 249 µs (−43%) | 380 µs → 262 µs
(−31%) |
| `snapshots/create/large` | 171 ms → 1.68 ms (−99%) | 162 ms → 1.7 ms
(−99%) |
| `snapshots/call_and_create/medium` | 38.1 ms → 726 µs (−98%) | 35 ms →
778 µs (−98%) |
| `snapshots/restore_alternate/large` | 4.41 ms → 106 µs (−98%) | 1.66
ms → 131 µs (−92%) |
| `snapshots/restore/large` | 1.2 ms → 91.6 µs (−92%) | 403 µs → 52 µs
(−87%) |
Guest calls show no consistent change.
### Regressions
| Benchmark | MSHV | KVM | Cause |
|---|---|---|---|
| `snapshot_files/load_snapshot_unverified/default` | 88.5 µs → 112 µs
(**+27%**) | 93.3 µs → 123 µs (**+31%**) | Opens and maps 2 data blobs
instead of 1 |
| `snapshot_files/load_snapshot/default` | 617 µs → 701 µs (**+14%**) |
613 µs → 701 µs (**+14%**) | Also hashes 88 KiB more: base layer pages
that the newer layer replaced, and the base layer's page tables |
| `snapshot_files/save_snapshot/default` | 853 µs → 941 µs (**+10%**) |
35.5 ms → 43.4 ms (**+22%**) | Writes, hashes and fsyncs 2 data blobs
and 88 KiB more. fsync is slow on the KVM host |
| `snapshot_files/cold_start_via_snapshot/default` | 1.8 ms → 1.97 ms
(**+9%**) | 2.59 ms → 2.85 ms (**+10%**) | Includes `load_snapshot` |
| `sandboxes/sandbox_from_snapshot/small` | 823 µs → 812 µs (~) | 877 µs
→ 978 µs (**+12%**) | Maps 2 layers instead of 1. Each costs a memory
slot on KVM and a map call on MSHV. MSHV shows it only at `default`
(+11%) |
Kernels before 6.12 cannot disable `KVM_X86_QUIRK_SLOT_ZAP_ALL`, so each
memory slot change flushes all guest mappings. WHP and aarch64 were not
benchmarked.
A saved snapshot stores each layer in full. Base layer pages that a
newer layer replaced stay in the file, which adds 88 KiB for `default`.
Closes #1956
Closes #1551
### Full results
<details>
<summary>All benchmarks on both hosts (click to expand)</summary>
Before → after (change). Negative is faster. **Bold** is a regression. ~
is no consistent change. A change needs agreement between a 0.5 s and a
3 s run, or a 3 s run alone.
#### snapshots
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `call_and_create` | default | 556 µs → 372 µs (−33%) | 471 µs → 409 µs
(−13%) |
| `call_and_create` | small | 2.67 ms → 416 µs (−84%) | 2.65 ms → 466 µs
(−82%) |
| `call_and_create` | medium | 38.1 ms → 726 µs (−98%) | 35 ms → 778 µs
(−98%) |
| `call_and_create` | large | 172 ms → 1.78 ms (−99%) | 161 ms → 1.85 ms
(−99%) |
| `create` | default | 440 µs → 249 µs (−43%) | 380 µs → 262 µs (−31%) |
| `create` | small | 2.57 ms → 291 µs (−89%) | 2.47 ms → 324 µs (−87%) |
| `create` | medium | 41.4 ms → 606 µs (−99%) | 36.9 ms → 606 µs (−98%)
|
| `create` | large | 171 ms → 1.68 ms (−99%) | 162 ms → 1.7 ms (−99%) |
| `restore` | default | 64.9 µs → 65.7 µs (~) | 20.6 µs → 21 µs (~) |
| `restore` | small | 66.3 µs → 67 µs (~) | 21.4 µs → 21.5 µs (~) |
| `restore` | medium | 74.5 µs → 71.1 µs (−5%) | 26.4 µs → 25.7 µs (~) |
| `restore` | large | 1.2 ms → 91.6 µs (−92%) | 403 µs → 52 µs (−87%) |
| `restore_alternate` | default | 164 µs → 78.7 µs (−52%) | 248 µs →
96.5 µs (−61%) |
| `restore_alternate` | small | 270 µs → 79.7 µs (−71%) | 268 µs → 93 µs
(−65%) |
| `restore_alternate` | medium | 901 µs → 83.4 µs (−91%) | 255 µs → 103
µs (−60%) |
| `restore_alternate` | large | 4.41 ms → 106 µs (−98%) | 1.66 ms → 131
µs (−92%) |
#### guest_calls
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `call` | default | 45.8 µs → 45.3 µs (~) | 18.4 µs → 17.3 µs (~) |
| `call` | small | 46.2 µs → 43.8 µs (−5%) | 18.5 µs → 17.7 µs (~) |
| `call` | medium | 47.3 µs → 43.8 µs (~) | 19.8 µs → 17.3 µs (~) |
| `call` | large | 45.4 µs → 44 µs (~) | 18.5 µs → 18 µs (~) |
| `call_with_host_function` | default | 77.5 µs → 75.2 µs (~) | 32.9 µs
→ 34.5 µs (~) |
| `call_with_host_function` | small | 75.4 µs → 74.1 µs (~) | 32.8 µs →
34.7 µs (~) |
| `call_with_host_function` | medium | 74 µs → 75.2 µs (~) | 34.8 µs →
32.8 µs (~) |
| `call_with_host_function` | large | 73.5 µs → 75.2 µs (~) | 33.5 µs →
32.7 µs (~) |
| `call_with_restore` | default | 140 µs → 133 µs (~) | 64 µs → 62.9 µs
(~) |
| `call_with_restore` | small | 141 µs → 140 µs (~) | 64.4 µs → 63.8 µs
(~) |
| `call_with_restore` | medium | 147 µs → 138 µs (~) | 70.4 µs → 67.9 µs
(~) |
| `call_with_restore` | large | 201 µs → 166 µs (~) | 102 µs → 89.1 µs
(~) |
| `different_thread` | - | 75.5 µs → 74 µs (~) | 42.4 µs → 42.9 µs (~) |
| `interrupt_latency` | - | 47.8 µs → 48.9 µs (~) | 32.8 µs → 35.9 µs
(~) |
#### guest_functions_with_large_parameters
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `guest_call_with_large_parameters` | - | 164 ms → 163 ms (~) | 872 ms
→ 916 ms (~) |
#### sample_workloads
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `24K_in_8K_out_c` | - | 56.3 µs → 56.5 µs (~) | 26.9 µs → 27.6 µs (~)
|
| `24K_in_8K_out_rust` | - | 54.6 µs → 56.1 µs (~) | 26.6 µs → 27.9 µs
(~) |
#### sandboxes
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `create_initialized` | default | 1.59 ms → 1.72 ms (~) | 2.62 ms → 2.8
ms (~) |
| `create_initialized` | small | 4.44 ms → 4.35 ms (~) | 4.72 ms → 4.58
ms (~) |
| `create_initialized` | medium | 11.8 ms → 11.8 ms (~) | 9.9 ms → 9.81
ms (~) |
| `create_initialized` | large | 37.8 ms → 38.2 ms (~) | 27.6 ms → 27.5
ms (~) |
| `create_initialized_and_drop` | default | 22.4 ms → 21.4 ms (~) | 10.9
ms → 14.1 ms (~) |
| `create_initialized_and_drop` | small | 29.4 ms → 28.9 ms (~) | 14.8
ms → 13.3 ms (~) |
| `create_initialized_and_drop` | medium | 31.9 ms → 32.2 ms (~) | 19.9
ms → 20.7 ms (~) |
| `create_initialized_and_drop` | large | 58.8 ms → 57.9 ms (~) | 41.7
ms → 39.6 ms (~) |
| `sandbox_from_snapshot` | default | 599 µs → 665 µs (**+11%**) | 865
µs → 971 µs (~) |
| `sandbox_from_snapshot` | small | 823 µs → 812 µs (~) | 877 µs → 978
µs (**+12%**) |
| `sandbox_from_snapshot` | medium | 2.17 ms → 2.02 ms (~) | 975 µs →
1.01 ms (~) |
| `sandbox_from_snapshot` | large | 6.29 ms → 6.06 ms (~) | 1.06 ms →
1.22 ms (**+15%**) |
#### snapshot_files
| Benchmark | Size | MSHV | KVM |
|---|---|---|---|
| `cold_start_via_evolve` | default | 1.67 ms → 1.61 ms (~) | 3.05 ms →
2.96 ms (~) |
| `cold_start_via_evolve` | small | 4.74 ms → 4.55 ms (~) | 5.12 ms →
5.76 ms (~) |
| `cold_start_via_evolve` | medium | 12 ms → 12.3 ms (~) | 10.3 ms → 11
ms (~) |
| `cold_start_via_evolve` | large | 37.6 ms → 38.8 ms (~) | 27.9 ms →
29.1 ms (~) |
| `cold_start_via_snapshot` | default | 1.8 ms → 1.97 ms (**+9%**) |
2.59 ms → 2.85 ms (**+10%**) |
| `cold_start_via_snapshot` | small | 9.14 ms → 8.93 ms (~) | 8.55 ms →
8.95 ms (~) |
| `cold_start_via_snapshot` | medium | 54.7 ms → 53.8 ms (~) | 49.8 ms →
50.9 ms (~) |
| `cold_start_via_snapshot` | large | 204 ms → 205 ms (~) | 193 ms → 194
ms (~) |
| `cold_start_via_snapshot_unverified` | default | 1.31 ms → 1.4 ms (~)
| 2.06 ms → 2.2 ms (~) |
| `cold_start_via_snapshot_unverified` | small | 2.18 ms → 2.08 ms (−4%)
| 2.03 ms → 2.17 ms (~) |
| `cold_start_via_snapshot_unverified` | medium | 3.68 ms → 3.59 ms (~)
| 2.2 ms → 2.25 ms (~) |
| `cold_start_via_snapshot_unverified` | large | 7.7 ms → 7.73 ms (~) |
2.33 ms → 2.59 ms (~) |
| `load_snapshot` | default | 617 µs → 701 µs (**+14%**) | 613 µs → 701
µs (**+14%**) |
| `load_snapshot` | small | 6.65 ms → 6.71 ms (~) | 6.49 ms → 6.68 ms
(~) |
| `load_snapshot` | medium | 50.3 ms → 50.4 ms (~) | 48.1 ms → 48.8 ms
(~) |
| `load_snapshot` | large | 197 ms → 198 ms (~) | 190 ms → 192 ms (~) |
| `load_snapshot_unverified` | default | 88.5 µs → 112 µs (**+27%**) |
93.3 µs → 123 µs (**+31%**) |
| `load_snapshot_unverified` | small | 89.6 µs → 113 µs (**+26%**) |
95.1 µs → 119 µs (**+25%**) |
| `load_snapshot_unverified` | medium | 90.4 µs → 114 µs (**+26%**) |
96.3 µs → 120 µs (**+24%**) |
| `load_snapshot_unverified` | large | 94 µs → 118 µs (**+26%**) | 99 µs
→ 125 µs (**+27%**) |
| `save_snapshot` | default | 853 µs → 941 µs (**+10%**) | 35.5 ms →
43.4 ms (**+22%**) |
| `save_snapshot` | small | 8.97 ms → 9.09 ms (~) | 55.2 ms → 61.5 ms
(~) |
| `save_snapshot` | medium | 68.2 ms → 69.6 ms (~) | 462 ms → 402 ms (~)
|
| `save_snapshot` | large | 270 ms → 271 ms (~) | 2.03 s → 1.75 s (~) |
</details>
---------
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>1 parent b52c0cf commit b99eb5f
34 files changed
Lines changed: 4870 additions & 1635 deletions
File tree
- docs
- proposals/0003-incremental-snapshots
- src
- hyperlight_common/src
- arch
- aarch64
- amd64
- hyperlight_host
- benches
- src
- hypervisor
- gdb
- hyperlight_vm
- virtual_machine/kvm
- mem
- virtq
- sandbox
- snapshot
- file
- memory
- trace
- tests/snapshot_goldens
- tests/rust_guests/simpleguest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
14 | 31 | | |
15 | 32 | | |
16 | 33 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | | - | |
| 25 | + | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
34 | | - | |
35 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
43 | 47 | | |
44 | 48 | | |
45 | 49 | | |
| |||
48 | 52 | | |
49 | 53 | | |
50 | 54 | | |
| 55 | + | |
| 56 | + | |
51 | 57 | | |
52 | 58 | | |
53 | 59 | | |
| |||
95 | 101 | | |
96 | 102 | | |
97 | 103 | | |
98 | | - | |
| 104 | + | |
99 | 105 | | |
100 | 106 | | |
101 | 107 | | |
| |||
138 | 144 | | |
139 | 145 | | |
140 | 146 | | |
141 | | - | |
| 147 | + | |
142 | 148 | | |
143 | 149 | | |
144 | 150 | | |
145 | 151 | | |
146 | | - | |
| 152 | + | |
147 | 153 | | |
148 | 154 | | |
149 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
| 21 | + | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
31 | | - | |
| 30 | + | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
39 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
48 | 51 | | |
49 | 52 | | |
50 | 53 | | |
| |||
0 commit comments