perf(hypervisor): speed up incremental snapshots with PM_FILE (pagemap bit 61) - #1343
perf(hypervisor): speed up incremental snapshots with PM_FILE (pagemap bit 61)#1343fslongjin wants to merge 1 commit into
PM_FILE (pagemap bit 61)#1343Conversation
e62483c to
d6c30b0
Compare
Review: perf(hypervisor): speed up incremental snapshots with
|
|
On the review findings:
|
Cut metadata scanning from one /proc/kpageflags syscall per present page to a single sequential pagemap read. Classify CoW pages with must_save = swapped || (present && !PM_FILE). Use bit 61 on Linux 6.6.44+ / 6.11+ / 7+; keep kpageflags on older kernels. Signed-off-by: jinlong <jinlong@tencent.com>
d6c30b0 to
07fb06c
Compare
| const KPF_ANON: u64 = 1 << 12; | ||
|
|
||
| /// `true` if this pagemap entry must be written into an incremental snapshot. | ||
| pub(crate) fn pagemap_entry_is_cow_anon(entry: u64) -> bool { |
There was a problem hiding this comment.
The kernel sets PM_FILE (bit 61) when page_mapcount(page) != 1 || page_is_file_cache(page) (fs/proc/task_mmu.c). For the fast-restore case — a private CoW page with mapcount 1 — present && !PM_FILE is equivalent to the old present && KPF_ANON. But they diverge when an anonymous page has more than one PTE reference:
- A KSM-merged anonymous page has mapcount > 1 →
PM_FILE = 1whileKPF_ANON = 1. The old kpageflags path saved it; this predicate skips it → under-save → silent snapshot corruption. - Cube supports
mergeable=on(MADV_MERGEABLEon guest RAM viacreate_userspace_mapping,docs/memory.md), and fast-restored RAM is a MAP_PRIVATE baseline mapping whose CoW pages are mergeable — so on a host runningksmd, a Guest-written page can be merged and then dropped here.
Pagemap alone can't distinguish a KSM page from a file page (both have PM_FILE = 1), so the version gate can't protect against this. Consider also gating the bit-61 path on !config.mergeable (or breaking KSM before the scan), or documenting that bit-61 snapshots require non-mergeable guest RAM.
| } | ||
|
|
||
| /// Whether this kernel is known to set `PM_FILE` on file PMDs. | ||
| pub(crate) fn supports_pm_file_pmd(self) -> bool { |
There was a problem hiding this comment.
KernelRelease::parse takes only the leading major.minor.patch, so a string like 6.6.44-rc1 is treated as final 6.6.44, and any vendor kernel that reports ≥ 6.6.44 / ≥ 6.11 without carrying the PM_FILE backport is also gated to bit 61. Both directions are safe — a wrongly-enabled kernel only over-saves file huge pages (valid but larger snapshot), a wrongly-disabled one falls back to kpageflags — but the PR's "writes the same amount of data (within 1%)" claim assumes the gate is accurate. A one-line note that the gate means "≥ this version, assuming the vendor ships the upstream stable backport" would help future readers.
|
Can we add some integration cases to cover the two methods of obtaining anon pages? @fslongjin |
Summary
Incremental snapshots only need to save the pages the Guest has written. On newer host kernels, this PR replaces "query
/proc/kpageflagsper present page" with "a single sequential pagemap read," deciding via pagemap bit 61 (PM_FILE).CAP_SYS_ADMINis no longer required.kpageflagspath; behavior unchanged.The snapshot file format and the baseline-file convention are unchanged. Existing snapshots need no migration.
Performance:
create_snapshot()), same size points as the scan: 64 MiB wash (155 → 154 ms); 256 MiB 209 → 178 ms (−31 ms); 1 GiB 384 → 305 ms (−79 ms). Almost no Guest writes is also a wash. The end-to-end speedup is far smaller than the scan's, because writing the dirty pages to the snapshot file dominates total time.Background
Cube's fast restore backs Guest RAM by a read-only baseline file. When the Guest only reads a page, that page comes straight from the baseline; once the Guest writes, the kernel copies it into a process-private page (copy-on-write, CoW). An incremental snapshot only needs to save these private pages back; untouched pages are reused from the baseline.
So "pick the pages to save" is equivalent to "pick the pages the Guest has written."
flowchart LR base["read-only baseline file"] --> fault{"how does the Guest touch this page?"} fault -->|"never accessed"| skip1["no PTE: don't save"] fault -->|"read only"| filePage["still a baseline page: don't save"] fault -->|"writes"| cow["copied to a private page: must save"] cow --> swap{"swapped out?"} swap -->|yes| saveSwap["swapped page: still must save"] swap -->|no| saveRam["in memory and not a file page: save"]Why the old path is slow
The old implementation reads pagemap once, then for every present page queries
/proc/kpageflagsonce to ask "is this an anonymous page?"When Guest RAM is large but few pages were actually written, this becomes hundreds of thousands of 8-byte random reads, just to confirm "this page need not be saved." Even with few dirty pages, the incremental snapshot can stall here. Also,
/proc/kpageflagsis usually only openable by root; withoutCAP_SYS_ADMINthe whole step fails.flowchart TD start["pick pages to save"] --> readPm["one sequential pagemap read"] readPm --> loop["for each present page"] loop --> seek["query /proc/kpageflags"] seek --> kpf{"anonymous page?"} kpf -->|yes| save["write to incremental"] kpf -->|no| skip["skip"]The new path
The key observation: the old path actually answers two questions—"is this page in memory?" and "is this a private page the Guest wrote?". The first is already answered by pagemap (bit 63,
present); the second detours to/proc/kpageflagsforKPF_ANON, so every present page issues another syscall.But in the same 64-bit pagemap entry, bit 61 (
PM_FILE) already marks whether this is a file page. A page the Guest wrote is CoW'd into a private anonymous page, which is not a file page, soPM_FILE = 0. In other words, the answer the old path went to kpageflags for is already in the pagemap read it already did:On kernels that correctly set
PM_FILEon file huge pages, this predicate selects exactly the same set of pages as the oldpresent && KPF_ANON—neither fewer nor more—just trading "one kpageflags query per page" for "a bit already in the one sequential read." Since we no longer need to pull a PFN out of pagemap to query another file,CAP_SYS_ADMINis no longer required either.flowchart TD start["pick pages to save"] --> gate{"host kernel supports PM_FILE?"} gate -->|yes| bit61["one sequential pagemap read<br/>decide by bit 61"] gate -->|no| kpf["pagemap + per-page kpageflags<br/>still needs CAP_SYS_ADMIN"] bit61 --> out["list of pages to save"] kpf --> outChoosing by kernel version
Correct
PM_FILEmarking on file huge pages was fixed only later (upstream3f9f022). So we can't simply check "version ≥ 6.6.44": 6.7–6.10 are numerically newer, but mainline only merged the fix around 6.11.This PR's enablement:
The first scan emits one log line for verification:
pagemap_anon: kernel=... path=bit61|kpageflags.Performance
Scan alone: ~30×
Measures only the "pick pages to save" step, no disk writes. Release build, Linux 7.0, read all pages sequentially then write 10%.
Production path: same sizes as the scan (64 / 256 / 1 GiB)
PVM host kernel Linux
6.6.69-opencloudos9.cubesandbox.pvm.host-gb85200d80fa2, 2 GiB sandbox, same template. A fresh sandbox each round, measuring only the firstcreate_snapshot(). 1 warm-up + 5 measured rounds per size; the table shows p50. Guest dirtying usesddinto/dev/shm(same sizes as the scan table). Logs confirm the new path uses bit 61; at each size the new path writes the same amount of data as the old one (within 1%), so it neither under-saves (which would corrupt the snapshot) nor degrades into a full memory dump.The end-to-end speedup is far below the scan's 30×, as expected: total
create_snapshot()time is dominated by writing dirty pages to the snapshot file; scanning is just one part of it. At 64 MiB the scan tax is still buried in noise; at 256 MiB it shows (~30 ms); at ~1 GiB the old path's per-page query cost across hundreds of thousands of present pages surfaces (~80 ms), matching the order of magnitude of "1 GiB scan 67 ms → 2 ms" above.Correctness
Under-saving would silently corrupt a snapshot, which is this PR's main concern.
PM_FILEcorrectly, a file huge page can be misclassified as anonymous, causing over-saving (the snapshot is still valid, just larger). This PR gates those kernels out by version and keeps them on kpageflags.Compatibility
path=log line so you can confirm at runtime which path was taken.