mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 23:47:28 +00:00
feat(ecstore): runtime-probed io_uring read backend, gray-off by default (backlog#1104) (#4632)
feat(ecstore): add runtime-probed io_uring read backend, gray-off by default (rustfs/backlog#1104) Wire the standalone rustfs-uring crate (https://github.com/rustfs/uring) into LocalIoBackend as an opt-in read backend. When RUSTFS_IO_URING_READ_ENABLE is set AND the per-disk io_uring probe succeeds, positioned reads go through the cancel-safe UringDriver; otherwise — default, or on a restricted host, or on any per-read driver error — reads fall back to StdBackend byte-for-byte, so the default build is unchanged. - Cargo.toml: rustfs-uring as a Linux-only git dependency (pinned rev). The guard scripts/check_no_tokio_io_uring.sh allows an explicit io-uring integration; only the tokio "io-uring" runtime feature is banned. - UringBackend mirrors StdBackend::pread_bytes's resolution/access/bounds preamble and only swaps the raw byte read; the other three trait methods delegate to the inner StdBackend. - Backend selection at LocalDisk construction via build_local_io_backend. - Differential test uring_backend_reads_match_std: with the flag set, every positioned read returns byte-identical data (driver-served when io_uring is available, StdBackend fallback otherwise). Verified: cargo check -p rustfs-ecstore on Linux; the differential test passes under real io_uring (seccomp=unconfined). The runtime errno degradation latch, per-disk probe cache, and productionization are tracked in rustfs/backlog#1101/#1102. Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -112,4 +112,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fail-on-severity: moderate
|
fail-on-severity: moderate
|
||||||
allow-ghsas: GHSA-2f9f-gq7v-9h6m
|
allow-ghsas: GHSA-2f9f-gq7v-9h6m
|
||||||
|
# rustfs-uring is a first-party Apache-2.0 crate pulled as a git
|
||||||
|
# dependency (backlog#1104); its license cannot be read from the git
|
||||||
|
# source, so allow it explicitly to avoid a spurious unknown-license
|
||||||
|
# warning on every Cargo.lock change.
|
||||||
|
allow-dependencies-licenses: pkg:cargo/rustfs-uring
|
||||||
comment-summary-in-pr: always
|
comment-summary-in-pr: always
|
||||||
|
|||||||
Generated
+22
@@ -5326,6 +5326,17 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-uring"
|
||||||
|
version = "0.7.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipconfig"
|
name = "ipconfig"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -9169,6 +9180,7 @@ dependencies = [
|
|||||||
"rustfs-signer",
|
"rustfs-signer",
|
||||||
"rustfs-storage-api",
|
"rustfs-storage-api",
|
||||||
"rustfs-tls-runtime",
|
"rustfs-tls-runtime",
|
||||||
|
"rustfs-uring",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -10015,6 +10027,16 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustfs-uring"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/rustfs/uring?rev=39018c09cde98b380658f1a1511349a3574daddf#39018c09cde98b380658f1a1511349a3574daddf"
|
||||||
|
dependencies = [
|
||||||
|
"io-uring",
|
||||||
|
"libc",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-utils"
|
name = "rustfs-utils"
|
||||||
version = "1.0.0-beta.8"
|
version = "1.0.0-beta.8"
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ aws-smithy-http-client.workspace = true
|
|||||||
# Observability and Metrics
|
# Observability and Metrics
|
||||||
metrics = { workspace = true }
|
metrics = { workspace = true }
|
||||||
|
|
||||||
|
# Runtime-probed io_uring read backend (backlog#1104). Linux-only, git
|
||||||
|
# dependency until rustfs-uring is production-ready and published. The guard
|
||||||
|
# scripts/check_no_tokio_io_uring.sh allows an explicit io-uring integration;
|
||||||
|
# only the tokio "io-uring" runtime feature is banned.
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
rustfs-uring = { git = "https://github.com/rustfs/uring", rev = "39018c09cde98b380658f1a1511349a3574daddf" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "test-util"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "test-util"] }
|
||||||
criterion = { workspace = true, features = ["html_reports"] }
|
criterion = { workspace = true, features = ["html_reports"] }
|
||||||
|
|||||||
@@ -276,6 +276,27 @@ fn is_direct_io_write_enabled() -> bool {
|
|||||||
rustfs_utils::get_env_bool(ENV_RUSTFS_OBJECT_DIRECT_IO_WRITE_ENABLE, DEFAULT_RUSTFS_OBJECT_DIRECT_IO_WRITE_ENABLE)
|
rustfs_utils::get_env_bool(ENV_RUSTFS_OBJECT_DIRECT_IO_WRITE_ENABLE, DEFAULT_RUSTFS_OBJECT_DIRECT_IO_WRITE_ENABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable the runtime-probed io_uring read backend (backlog#1104). Default:
|
||||||
|
/// false (gray-off). The backend is used only when this is set AND the per-disk
|
||||||
|
/// probe succeeds; otherwise, and on any per-read driver error, reads fall back
|
||||||
|
/// to `StdBackend` byte-for-byte.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const ENV_RUSTFS_IO_URING_READ_ENABLE: &str = "RUSTFS_IO_URING_READ_ENABLE";
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const DEFAULT_RUSTFS_IO_URING_READ_ENABLE: bool = false;
|
||||||
|
|
||||||
|
/// io_uring submission-queue depth used when probing a disk (backlog#1104).
|
||||||
|
/// Backpressure caps in-flight at this value, below CQ capacity (2×), so CQ
|
||||||
|
/// overflow is structurally unreachable.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const URING_QUEUE_DEPTH: u32 = 128;
|
||||||
|
|
||||||
|
/// Check if the runtime-probed io_uring read backend is enabled.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn is_io_uring_read_enabled() -> bool {
|
||||||
|
rustfs_utils::get_env_bool(ENV_RUSTFS_IO_URING_READ_ENABLE, DEFAULT_RUSTFS_IO_URING_READ_ENABLE)
|
||||||
|
}
|
||||||
|
|
||||||
const EVENT_DISK_LOCAL_DURABILITY_MODE: &str = "disk_local_durability_mode";
|
const EVENT_DISK_LOCAL_DURABILITY_MODE: &str = "disk_local_durability_mode";
|
||||||
|
|
||||||
/// Process-wide durability tier for commit-point fsync work on the local disk.
|
/// Process-wide durability tier for commit-point fsync work on the local disk.
|
||||||
@@ -2253,6 +2274,176 @@ impl LocalIoBackend for StdBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runtime-probed io_uring read backend (backlog#1104).
|
||||||
|
///
|
||||||
|
/// Wraps a [`StdBackend`] for everything except positioned reads, which go
|
||||||
|
/// through rustfs-uring's cancel-safe `UringDriver`. Constructed only when
|
||||||
|
/// `RUSTFS_IO_URING_READ_ENABLE` is set AND the per-disk probe succeeds; on any
|
||||||
|
/// per-read driver error a read falls back to the inner `StdBackend`, so
|
||||||
|
/// behavior never regresses. The read preamble (path resolution, access checks,
|
||||||
|
/// bounds) mirrors `StdBackend::pread_bytes` exactly — only the raw byte read
|
||||||
|
/// differs.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) struct UringBackend {
|
||||||
|
root: PathBuf,
|
||||||
|
inner: StdBackend,
|
||||||
|
driver: Arc<rustfs_uring::UringDriver>,
|
||||||
|
fallback_logged: std::sync::atomic::AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
impl std::fmt::Debug for UringBackend {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("UringBackend")
|
||||||
|
.field("root", &self.root)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
impl UringBackend {
|
||||||
|
/// Probe io_uring on `root`; `Some(backend)` if usable, `None` to fall back
|
||||||
|
/// to `StdBackend`. A restricted-environment errno degrades quietly; an
|
||||||
|
/// unexpected errno is surfaced as a warning (both still fall back).
|
||||||
|
pub(crate) fn try_new(root: PathBuf) -> Option<Self> {
|
||||||
|
match rustfs_uring::UringDriver::probe_and_start(URING_QUEUE_DEPTH) {
|
||||||
|
Ok(driver) => {
|
||||||
|
info!(
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
|
||||||
|
root = %root.display(),
|
||||||
|
"io_uring read backend enabled"
|
||||||
|
);
|
||||||
|
Some(Self {
|
||||||
|
inner: StdBackend::new(root.clone()),
|
||||||
|
root,
|
||||||
|
driver: Arc::new(driver),
|
||||||
|
fallback_logged: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if err.is_expected_restriction() {
|
||||||
|
debug!(
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
|
||||||
|
root = %root.display(),
|
||||||
|
error = ?err,
|
||||||
|
"io_uring unavailable (restricted environment); using StdBackend"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
|
||||||
|
root = %root.display(),
|
||||||
|
error = ?err,
|
||||||
|
"io_uring probe failed unexpectedly; using StdBackend"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Positioned read via io_uring. Mirrors `StdBackend::pread_bytes`'s
|
||||||
|
/// resolution/access/bounds preamble, then reads the range with the driver
|
||||||
|
/// (whole-range: the driver resubmits short reads for positioned reads).
|
||||||
|
async fn pread_uring(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<Bytes> {
|
||||||
|
let Some(end_offset) = offset.checked_add(length) else {
|
||||||
|
return Err(DiskError::FileCorrupt);
|
||||||
|
};
|
||||||
|
let root = self.root.clone();
|
||||||
|
let volume_owned = volume.to_owned();
|
||||||
|
let path_owned = path.to_owned();
|
||||||
|
|
||||||
|
let (file, offset_u64) = tokio::task::spawn_blocking(move || -> Result<(std::fs::File, u64)> {
|
||||||
|
let volume_dir = local_disk_bucket_path(&root, &volume_owned)?;
|
||||||
|
if !skip_access_checks(&volume_owned) {
|
||||||
|
access_std(&volume_dir).map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
|
||||||
|
}
|
||||||
|
let file_path = local_disk_object_path(&root, &volume_owned, &path_owned)?;
|
||||||
|
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||||
|
let file = std::fs::File::open(&file_path).map_err(DiskError::from)?;
|
||||||
|
let meta = file.metadata().map_err(DiskError::from)?;
|
||||||
|
let end_offset_u64 = u64::try_from(end_offset).map_err(|_| DiskError::FileCorrupt)?;
|
||||||
|
if meta.len() < end_offset_u64 {
|
||||||
|
return Err(DiskError::FileCorrupt);
|
||||||
|
}
|
||||||
|
let offset_u64 = u64::try_from(offset).map_err(|_| DiskError::FileCorrupt)?;
|
||||||
|
Ok((file, offset_u64))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| DiskError::other(format!("uring pread join error: {e}")))??;
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return Ok(Bytes::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = self
|
||||||
|
.driver
|
||||||
|
.read_at(Arc::new(file), offset_u64, length)
|
||||||
|
.await
|
||||||
|
.map_err(DiskError::from)?;
|
||||||
|
if bytes.len() != length {
|
||||||
|
return Err(DiskError::other("io_uring returned a short read"));
|
||||||
|
}
|
||||||
|
Ok(Bytes::from(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl LocalIoBackend for UringBackend {
|
||||||
|
async fn pread_bytes(
|
||||||
|
&self,
|
||||||
|
volume: &str,
|
||||||
|
path: &str,
|
||||||
|
offset: usize,
|
||||||
|
length: usize,
|
||||||
|
metrics: Option<MmapCopyStageMetrics>,
|
||||||
|
) -> Result<Bytes> {
|
||||||
|
match self.pread_uring(volume, path, offset, length).await {
|
||||||
|
Ok(bytes) => Ok(bytes),
|
||||||
|
Err(err) => {
|
||||||
|
if !self.fallback_logged.swap(true, Ordering::Relaxed) {
|
||||||
|
debug!(
|
||||||
|
component = LOG_COMPONENT_ECSTORE,
|
||||||
|
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
|
||||||
|
error = ?err,
|
||||||
|
"io_uring read fell back to StdBackend (logged once per disk)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.inner.pread_bytes(volume, path, offset, length, metrics).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_read_stream(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<FileReader> {
|
||||||
|
self.inner.open_read_stream(volume, path, offset, length).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_full_read(&self, volume: &str, path: &str) -> Result<FileReader> {
|
||||||
|
self.inner.open_full_read(volume, path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_write(&self, volume: &str, path: &str, mode: WriteMode) -> Result<FileWriter> {
|
||||||
|
self.inner.open_write(volume, path, mode).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the local read backend: the runtime-probed io_uring backend when
|
||||||
|
/// enabled and the per-disk probe succeeds, otherwise the default
|
||||||
|
/// [`StdBackend`] (backlog#1104). Enabling io_uring is opt-in and falls back
|
||||||
|
/// byte-for-byte, so the default build is unchanged.
|
||||||
|
fn build_local_io_backend(root: PathBuf) -> Arc<dyn LocalIoBackend> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if is_io_uring_read_enabled()
|
||||||
|
&& let Some(backend) = UringBackend::try_new(root.clone())
|
||||||
|
{
|
||||||
|
return Arc::new(backend);
|
||||||
|
}
|
||||||
|
Arc::new(StdBackend::new(root))
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LocalDisk {
|
pub struct LocalDisk {
|
||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
pub format_path: PathBuf,
|
pub format_path: PathBuf,
|
||||||
@@ -2513,7 +2704,7 @@ impl LocalDisk {
|
|||||||
startup_cleanup_ready,
|
startup_cleanup_ready,
|
||||||
startup_cleanup_notify,
|
startup_cleanup_notify,
|
||||||
exit_signal: None,
|
exit_signal: None,
|
||||||
io_backend: Arc::new(StdBackend::new(root.clone())),
|
io_backend: build_local_io_backend(root.clone()),
|
||||||
};
|
};
|
||||||
let (info, _root) = get_disk_info(root.clone()).await.inspect_err(|err| {
|
let (info, _root) = get_disk_info(root.clone()).await.inspect_err(|err| {
|
||||||
log_startup_disk_error("get_disk_info", &root, err);
|
log_startup_disk_error("get_disk_info", &root, err);
|
||||||
@@ -10914,6 +11105,64 @@ mod test {
|
|||||||
assert_eq!(Bytes::from(all), content, "open_full_read mismatch");
|
assert_eq!(Bytes::from(all), content, "open_full_read mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// io_uring read backend (backlog#1104): with `RUSTFS_IO_URING_READ_ENABLE`
|
||||||
|
/// set, every positioned read returns byte-identical data (the driver serves
|
||||||
|
/// it when io_uring is available; on a restricted host the backend degrades
|
||||||
|
/// to `StdBackend` and the bytes still match).
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn uring_backend_reads_match_std() {
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
const FILE_LEN: usize = 64 * 1024;
|
||||||
|
let mut state = 0x9e3779b97f4a7c15u64;
|
||||||
|
let content: Vec<u8> = (0..FILE_LEN)
|
||||||
|
.map(|_| {
|
||||||
|
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||||
|
(state >> 33) as u8
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let content = Bytes::from(content);
|
||||||
|
|
||||||
|
let page = mmap_page_size().expect("page size should be available") as usize;
|
||||||
|
let ranges = [
|
||||||
|
(0usize, FILE_LEN),
|
||||||
|
(1, 17),
|
||||||
|
(page - 1, 2),
|
||||||
|
(page, page),
|
||||||
|
(2 * page - 1, page + 2),
|
||||||
|
(FILE_LEN - 7, 7),
|
||||||
|
(0, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// The env var must be set before LocalDisk::new (the backend is chosen
|
||||||
|
// at construction).
|
||||||
|
let got_ranges = temp_env::async_with_vars([(ENV_RUSTFS_IO_URING_READ_ENABLE, Some("true"))], async {
|
||||||
|
let root_dir = tempdir().expect("operation should succeed");
|
||||||
|
let endpoint = Endpoint::try_from(root_dir.path().to_string_lossy().as_ref()).expect("operation should succeed");
|
||||||
|
let disk = LocalDisk::new(&endpoint, false).await.expect("operation should succeed");
|
||||||
|
disk.make_volume("test-volume").await.expect("operation should succeed");
|
||||||
|
disk.write_all("test-volume", "blob.bin", content.clone())
|
||||||
|
.await
|
||||||
|
.expect("operation should succeed");
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (offset, length) in ranges {
|
||||||
|
out.push(
|
||||||
|
disk.read_file_mmap_copy("test-volume", "blob.bin", offset, length)
|
||||||
|
.await
|
||||||
|
.expect("io_uring-enabled read must succeed (driver or fallback)"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for ((offset, length), got) in ranges.into_iter().zip(got_ranges) {
|
||||||
|
let expected = content.slice(offset..offset + length);
|
||||||
|
assert_eq!(got, expected, "uring read mismatch at offset={offset} length={length}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The O_DIRECT read path must return the same bytes as the buffered
|
/// The O_DIRECT read path must return the same bytes as the buffered
|
||||||
/// path for unaligned shard sizes and ranges, and must silently fall
|
/// path for unaligned shard sizes and ranges, and must silently fall
|
||||||
/// back (never error) on filesystems that reject O_DIRECT (e.g. tmpfs).
|
/// back (never error) on filesystems that reject O_DIRECT (e.g. tmpfs).
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ allow-git = [
|
|||||||
# Pinned to a specific commit in workspace Cargo.toml.
|
# Pinned to a specific commit in workspace Cargo.toml.
|
||||||
# "https://github.com/rustfs/s3s",
|
# "https://github.com/rustfs/s3s",
|
||||||
"https://github.com/apache/datafusion.git",
|
"https://github.com/apache/datafusion.git",
|
||||||
|
# Runtime-probed io_uring read backend (backlog#1104), Linux-only.
|
||||||
|
# Pinned to a specific commit in crates/ecstore/Cargo.toml.
|
||||||
|
"https://github.com/rustfs/uring",
|
||||||
]
|
]
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
|
|||||||
Reference in New Issue
Block a user