Skip to content

Commit 02493c1

Browse files
authored
PHD: Use ZFS clones for file-backed disks (#640)
When creating a file-backed disk from an artifact, create a ZFS snapshot and of the dataset containing the artifact, then create and mount a clone of the snapshot and back the disk from the clone. This is much, much less expensive than copying the underlying file and markedly improves performance for test runs with disks of nontrivial size. (On my local test machine, a full test run with a 2 GiB Debian nocloud image took 688 seconds before this change and 610 seconds after, about a 10% speedup.) This is unfortunately not likely to move the needle much on CI times because the Alpine test image CI uses is very small. However, this should make it much easier to switch to non-Alpine CI guest(s) without needlessly bloating CI times. Tested with full runs against Debian 11 and Alpine guests.
1 parent aeb2339 commit 02493c1

File tree

7 files changed

+533
-122
lines changed

7 files changed

+533
-122
lines changed

.github/buildomat/jobs/phd-run.sh

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ indir="/input"
2121
indir_suffix="phd-build/out/*.tar.gz"
2222
phddir="$PWD/phd-test"
2323

24+
# Put artifacts on the runner's SSDs (the /work ramdisk is small by design, too
25+
# small for images of any appreciable size).
26+
pfexec zpool create -f phd-artifacts c1t1d0 c2t1d0
27+
artifactdir="/phd-artifacts"
28+
2429
banner 'Inputs'
2530
find $indir -ls
2631

@@ -63,7 +68,7 @@ set +e
6368
--crucible-downstairs-commit auto \
6469
--artifact-toml-path $artifacts \
6570
--tmp-directory $tmpdir \
66-
--artifact-directory $tmpdir | \
71+
--artifact-directory $artifactdir | \
6772
tee /tmp/phd-runner.log)
6873
failcount=$?
6974
set -e

phd-tests/framework/src/disk/file.rs

+79-31
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,86 @@
44

55
//! Abstractions for disks with a raw file backend.
66
7-
use std::path::{Path, PathBuf};
8-
7+
use camino::{Utf8Path, Utf8PathBuf};
98
use propolis_client::types::{FileStorageBackend, StorageBackendV0};
10-
use tracing::{error, info};
9+
use tracing::{debug, error, warn};
1110
use uuid::Uuid;
1211

13-
use crate::guest_os::GuestOsKind;
12+
use crate::{guest_os::GuestOsKind, zfs::ClonedFile as ZfsClonedFile};
13+
14+
/// Describes the method used to create the backing file for a file-backed disk.
15+
#[derive(Debug)]
16+
enum BackingFile {
17+
/// The disk is a ZFS clone of the original artifact.
18+
Zfs(ZfsClonedFile),
19+
20+
/// The disk is a hard copy of the original artifact.
21+
HardCopy(Utf8PathBuf),
22+
}
23+
24+
impl BackingFile {
25+
/// Creates a new backing file from the artifact with the supplied
26+
/// `artifact_path`. If possible, this routine will create a ZFS clone of
27+
/// the dataset containing the file; otherwise it will fall back to creating
28+
/// a hard copy of the original artifact.
29+
fn create_from_source(
30+
artifact_path: &Utf8Path,
31+
data_dir: &Utf8Path,
32+
) -> anyhow::Result<Self> {
33+
match ZfsClonedFile::create_from_path(artifact_path) {
34+
Ok(file) => return Ok(Self::Zfs(file)),
35+
Err(error) => warn!(
36+
%artifact_path,
37+
%error,
38+
"failed to make ZFS clone of backing artifact, will copy it"
39+
),
40+
}
41+
42+
let mut disk_path = data_dir.to_path_buf();
43+
disk_path.push(format!("{}.phd_disk", Uuid::new_v4()));
44+
debug!(
45+
source = %artifact_path,
46+
disk_path = %disk_path,
47+
"Copying source image to create temporary disk",
48+
);
49+
50+
std::fs::copy(artifact_path, &disk_path)?;
51+
Ok(Self::HardCopy(disk_path))
52+
}
53+
54+
/// Yields the path to this disk's backing file.
55+
fn path(&self) -> Utf8PathBuf {
56+
match self {
57+
BackingFile::Zfs(zfs) => zfs.path(),
58+
BackingFile::HardCopy(path) => path.clone(),
59+
}
60+
}
61+
}
62+
63+
impl Drop for BackingFile {
64+
fn drop(&mut self) {
65+
// ZFS clones are cleaned up by their own drop impls.
66+
if let BackingFile::HardCopy(path) = self {
67+
debug!(%path, "deleting hard copy of guest disk image");
68+
if let Err(e) = std::fs::remove_file(&path) {
69+
error!(
70+
?e,
71+
%path,
72+
"failed to delete hard copy of guest disk image"
73+
);
74+
}
75+
}
76+
}
77+
}
1478

1579
/// An RAII wrapper for a disk wrapped by a file.
1680
#[derive(Debug)]
1781
pub struct FileBackedDisk {
1882
/// The name to use for instance spec backends that refer to this disk.
1983
backend_name: String,
2084

21-
/// The path at which the disk is stored.
22-
disk_path: PathBuf,
85+
/// The backing file for this disk.
86+
file: BackingFile,
2387

2488
/// The kind of guest OS image this guest contains, or `None` if the disk
2589
/// was not initialized from a guest OS artifact.
@@ -31,23 +95,18 @@ impl FileBackedDisk {
3195
/// the specified artifact on the host file system.
3296
pub(crate) fn new_from_artifact(
3397
backend_name: String,
34-
artifact_path: &impl AsRef<Path>,
35-
data_dir: &impl AsRef<Path>,
98+
artifact_path: &impl AsRef<Utf8Path>,
99+
data_dir: &impl AsRef<Utf8Path>,
36100
guest_os: Option<GuestOsKind>,
37101
) -> Result<Self, super::DiskError> {
38-
let mut disk_path = data_dir.as_ref().to_path_buf();
39-
disk_path.push(format!("{}.phd_disk", Uuid::new_v4()));
40-
info!(
41-
source = %artifact_path.as_ref().display(),
42-
disk_path = %disk_path.display(),
43-
"Copying source image to create temporary disk",
44-
);
45-
46-
std::fs::copy(artifact_path, &disk_path)?;
102+
let artifact = BackingFile::create_from_source(
103+
artifact_path.as_ref(),
104+
data_dir.as_ref(),
105+
)?;
47106

48107
// Make sure the disk is writable (the artifact may have been
49108
// read-only).
50-
let disk_file = std::fs::File::open(&disk_path)?;
109+
let disk_file = std::fs::File::open(artifact.path())?;
51110
let mut permissions = disk_file.metadata()?.permissions();
52111

53112
// TODO: Clippy is upset that `set_readonly(false)` results in
@@ -57,7 +116,7 @@ impl FileBackedDisk {
57116
permissions.set_readonly(false);
58117
disk_file.set_permissions(permissions)?;
59118

60-
Ok(Self { backend_name, disk_path, guest_os })
119+
Ok(Self { backend_name, file: artifact, guest_os })
61120
}
62121
}
63122

@@ -66,7 +125,7 @@ impl super::DiskConfig for FileBackedDisk {
66125
(
67126
self.backend_name.clone(),
68127
StorageBackendV0::File(FileStorageBackend {
69-
path: self.disk_path.to_string_lossy().to_string(),
128+
path: self.file.path().to_string(),
70129
readonly: false,
71130
}),
72131
)
@@ -76,14 +135,3 @@ impl super::DiskConfig for FileBackedDisk {
76135
self.guest_os
77136
}
78137
}
79-
80-
impl Drop for FileBackedDisk {
81-
fn drop(&mut self) {
82-
info!(path = %self.disk_path.display(), "Deleting guest disk image");
83-
if let Err(e) = std::fs::remove_file(&self.disk_path) {
84-
error!(?e,
85-
path = %self.disk_path.display(),
86-
"Failed to delete guest disk image");
87-
}
88-
}
89-
}

phd-tests/framework/src/disk/mod.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
//! They can then pass these disks to the VM factory to connect them to a
99
//! specific guest VM.
1010
11-
use std::{
12-
path::{Path, PathBuf},
13-
sync::Arc,
14-
};
11+
use std::sync::Arc;
1512

1613
use anyhow::Context;
14+
use camino::{Utf8Path, Utf8PathBuf};
1715
use propolis_client::types::StorageBackendV0;
1816
use thiserror::Error;
1917

@@ -108,7 +106,7 @@ pub enum DiskSource<'a> {
108106
/// Propolis backend implementations interact with the disk.
109107
pub(crate) struct DiskFactory {
110108
/// The directory in which disk files should be stored.
111-
storage_dir: PathBuf,
109+
storage_dir: Utf8PathBuf,
112110

113111
/// A reference to the artifact store to use to look up guest OS artifacts
114112
/// when those are used as a disk source.
@@ -127,7 +125,7 @@ impl DiskFactory {
127125
/// their data in `storage_dir` and will look up guest OS images in the
128126
/// supplied `artifact_store`.
129127
pub fn new(
130-
storage_dir: &impl AsRef<Path>,
128+
storage_dir: &impl AsRef<Utf8Path>,
131129
artifact_store: Arc<ArtifactStore>,
132130
port_allocator: Arc<PortAllocator>,
133131
log_mode: ServerLogMode,
@@ -145,11 +143,10 @@ impl DiskFactory {
145143
async fn get_guest_artifact_info(
146144
&self,
147145
artifact_name: &str,
148-
) -> Result<(PathBuf, GuestOsKind), DiskError> {
146+
) -> Result<(Utf8PathBuf, GuestOsKind), DiskError> {
149147
self.artifact_store
150148
.get_guest_os_image(artifact_name)
151149
.await
152-
.map(|(utf8, kind)| (utf8.into_std_path_buf(), kind))
153150
.with_context(|| {
154151
format!("failed to get guest OS artifact '{}'", artifact_name)
155152
})

phd-tests/framework/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ mod port_allocator;
5555
mod serial;
5656
pub mod server_log_mode;
5757
pub mod test_vm;
58+
pub(crate) mod zfs;
5859

5960
/// An instance of the PHD test framework.
6061
pub struct Framework {

0 commit comments

Comments
 (0)