From beb5cfc11b01f4bb532be8f2b3fb91502ef42cba Mon Sep 17 00:00:00 2001 From: Jiang Liu Date: Sat, 21 Sep 2024 14:20:48 +0800 Subject: [PATCH 1/7] storage: add helper copy_file_range Add helper copy_file_range() which: - avoid copy data into userspace - may support reflink on xfs etc Signed-off-by: Jiang Liu --- storage/src/utils.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++ utils/src/digest.rs | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/storage/src/utils.rs b/storage/src/utils.rs index 726ad921cf4..ebdb0488c66 100644 --- a/storage/src/utils.rs +++ b/storage/src/utils.rs @@ -6,6 +6,7 @@ use std::alloc::{alloc, Layout}; use std::cmp::{self, min}; use std::io::{ErrorKind, IoSliceMut, Result}; +use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::io::RawFd; use std::slice::from_raw_parts_mut; @@ -97,6 +98,68 @@ pub fn copyv>( Ok((copied, (dst_index, dst_offset))) } +/// The copy_file_range system call performs an in-kernel copy between file descriptors src and dst +/// without the additional cost of transferring data from the kernel to user space and back again. +/// +/// There may be additional optimizations for specific file systems. It copies up to len bytes of +/// data from file descriptor fd_in to file descriptor fd_out, overwriting any data that exists +/// within the requested range of the target file. +#[cfg(target_os = "linux")] +pub fn copy_file_range( + src: impl AsFd, + src_off: u64, + dst: impl AsFd, + dst_off: u64, + mut len: usize, +) -> Result<()> { + let mut src_off = src_off as i64; + let mut dst_off = dst_off as i64; + + while len > 0 { + let ret = nix::fcntl::copy_file_range( + src.as_fd().as_raw_fd(), + Some(&mut src_off), + dst.as_fd().as_raw_fd(), + Some(&mut dst_off), + len, + )?; + if ret == 0 && len > 0 { + return Err(eio!("reach end of file when copy file range")); + } + len -= ret; + } + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +pub fn copy_file_range( + src: impl AsFd, + src_off: u64, + dst: impl AsFd, + dst_off: u64, + len: usize, +) -> Result<()> { + let mut buf = vec![0u8; len]; + + let ret = nix::sys::uio::pread(src.as_fd().as_raw_fd(), &mut buf, src_off as libc::off_t)?; + if ret == len { + let ret = nix::sys::uio::pwrite(dst.as_fd().as_raw_fd(), &buf, dst_off as libc::off_t)?; + if ret == len { + return Ok(()); + } + } + + Err(eio!("failed to copy data between files")) +} + +pub fn get_path_from_file(file: &impl AsRawFd) -> String { + match std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd())) { + Ok(v) => v.display().to_string(), + Err(_) => "".to_string(), + } +} + /// An memory cursor to access an `FileVolatileSlice` array. pub struct MemSliceCursor<'a> { pub mem_slice: &'a [FileVolatileSlice<'a>], @@ -239,6 +302,8 @@ pub fn check_digest(data: &[u8], digest: &RafsDigest, digester: digest::Algorith #[cfg(test)] mod tests { use super::*; + use std::io::Write; + use vmm_sys_util::tempfile::TempFile; #[test] fn test_copyv() { @@ -372,4 +437,15 @@ mod tests { assert_eq!(cursor.index, 2); assert_eq!(cursor.offset, 0); } + + #[test] + fn test_copy_file_range() { + let mut src = TempFile::new().unwrap().into_file(); + let dst = TempFile::new().unwrap(); + + let buf = vec![8u8; 4096]; + src.write_all(&buf).unwrap(); + copy_file_range(&src, 0, dst.as_file(), 4096, 4096).unwrap(); + assert_eq!(dst.as_file().metadata().unwrap().len(), 8192); + } } diff --git a/utils/src/digest.rs b/utils/src/digest.rs index 12e74486f3b..2f04ca69909 100644 --- a/utils/src/digest.rs +++ b/utils/src/digest.rs @@ -217,7 +217,7 @@ impl AsRef<[u8]> for RafsDigest { impl fmt::Display for RafsDigest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for c in &self.data { - write!(f, "{:02x}", c).unwrap() + write!(f, "{:02x}", c)?; } Ok(()) } From 38b57082205307f45ea62d72e05b1c6a4cd1bb73 Mon Sep 17 00:00:00 2001 From: Yadong Ding Date: Sat, 21 Sep 2024 14:34:24 +0800 Subject: [PATCH 2/7] storage: improve copy_file_range - improve copy_file_range when target os is not linux - add more comprehensive tests Signed-off-by: Yadong Ding --- storage/src/utils.rs | 93 +++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/storage/src/utils.rs b/storage/src/utils.rs index ebdb0488c66..85e397cf0a0 100644 --- a/storage/src/utils.rs +++ b/storage/src/utils.rs @@ -1,15 +1,9 @@ // Copyright 2020 Ant Group. All rights reserved. +// Copyright 2024 Nydus Developers. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! Utility helpers to support the storage subsystem. -use std::alloc::{alloc, Layout}; -use std::cmp::{self, min}; -use std::io::{ErrorKind, IoSliceMut, Result}; -use std::os::fd::{AsFd, AsRawFd}; -use std::os::unix::io::RawFd; -use std::slice::from_raw_parts_mut; - use fuse_backend_rs::abi::fuse_abi::off64_t; use fuse_backend_rs::file_buf::FileVolatileSlice; #[cfg(target_os = "macos")] @@ -19,6 +13,13 @@ use nydus_utils::{ digest::{self, RafsDigest}, round_down_4k, }; +use std::alloc::{alloc, Layout}; +use std::cmp::{self, min}; +use std::io::{ErrorKind, IoSliceMut, Result}; +use std::os::fd::{AsFd, AsRawFd}; +use std::os::unix::io::RawFd; +use std::path::PathBuf; +use std::slice::from_raw_parts_mut; use vm_memory::bytes::Bytes; use crate::{StorageError, StorageResult}; @@ -123,7 +124,7 @@ pub fn copy_file_range( Some(&mut dst_off), len, )?; - if ret == 0 && len > 0 { + if ret == 0 { return Err(eio!("reach end of file when copy file range")); } len -= ret; @@ -135,28 +136,51 @@ pub fn copy_file_range( #[cfg(not(target_os = "linux"))] pub fn copy_file_range( src: impl AsFd, - src_off: u64, + mut src_off: u64, dst: impl AsFd, - dst_off: u64, - len: usize, + mut dst_off: u64, + mut len: usize, ) -> Result<()> { - let mut buf = vec![0u8; len]; + let buf_size = 4096; + let mut buf = vec![0u8; buf_size]; - let ret = nix::sys::uio::pread(src.as_fd().as_raw_fd(), &mut buf, src_off as libc::off_t)?; - if ret == len { - let ret = nix::sys::uio::pwrite(dst.as_fd().as_raw_fd(), &buf, dst_off as libc::off_t)?; - if ret == len { - return Ok(()); + while len > 0 { + let bytes_to_read = buf_size.min(len); + let read_bytes = nix::sys::uio::pread( + src.as_fd().as_raw_fd(), + &mut buf[..bytes_to_read], + src_off as libc::off_t, + )?; + + if read_bytes == 0 { + return Err(eio!("reach end of file when read in copy_file_range")); } + + let write_bytes = nix::sys::uio::pwrite( + dst.as_fd().as_raw_fd(), + &buf[..read_bytes], + dst_off as libc::off_t, + )?; + if write_bytes == 0 { + return Err(eio!("reach end of file when write in copy_file_range")); + } + + src_off += read_bytes as u64; + dst_off += read_bytes as u64; + len -= read_bytes; } - Err(eio!("failed to copy data between files")) + Ok(()) } -pub fn get_path_from_file(file: &impl AsRawFd) -> String { - match std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd())) { - Ok(v) => v.display().to_string(), - Err(_) => "".to_string(), +pub fn get_path_from_file(file: &impl AsRawFd) -> Option { + let path = PathBuf::from("/proc/self/fd").join(file.as_raw_fd().to_string()); + match std::fs::read_link(&path) { + Ok(v) => Some(v.display().to_string()), + Err(e) => { + warn!("Failed to get path from file descriptor: {}", e); + None + } } } @@ -447,5 +471,30 @@ mod tests { src.write_all(&buf).unwrap(); copy_file_range(&src, 0, dst.as_file(), 4096, 4096).unwrap(); assert_eq!(dst.as_file().metadata().unwrap().len(), 8192); + + let small_buf = vec![8u8; 2048]; + let mut small_src = TempFile::new().unwrap().into_file(); + small_src.write_all(&small_buf).unwrap(); + assert!(copy_file_range(&small_src, 0, dst.as_file(), 4096, 4096).is_err()); + + let empty_src = TempFile::new().unwrap().into_file(); + assert!(copy_file_range(&empty_src, 0, dst.as_file(), 4096, 4096).is_err()); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_get_path_from_file() { + let temp_file = TempFile::new().unwrap(); + let file = temp_file.as_file(); + let path = get_path_from_file(file).unwrap(); + assert_eq!(path, temp_file.as_path().display().to_string()); + } + + #[cfg(not(target_os = "linux"))] + #[test] + fn test_get_path_from_file_non_linux() { + let temp_file = TempFile::new().unwrap(); + let file = temp_file.as_file(); + assert_eq!(get_path_from_file(file).is_none()); } } From 57985b860cec3e85244ce2f2fa051e41b8ec15c1 Mon Sep 17 00:00:00 2001 From: Jiang Liu Date: Sat, 21 Sep 2024 14:35:08 +0800 Subject: [PATCH 3/7] storage: implement CasManager to support chunk dedup at runtime Implement CasManager to support chunk dedup at runtime. The manager provides to major interfaces: - add chunk data to the CAS database - check whether a chunk exists in CAS database and copy it to blob file by copy_file_range() if the chunk exists. Signed-off-by: Jiang Liu --- storage/src/cache/dedup/db.rs | 5 +- storage/src/cache/dedup/mod.rs | 174 +++++++++++++++++++++++++++++++++ storage/src/cache/mod.rs | 6 ++ 3 files changed, 183 insertions(+), 2 deletions(-) diff --git a/storage/src/cache/dedup/db.rs b/storage/src/cache/dedup/db.rs index 6daff37c70b..c505cde8480 100644 --- a/storage/src/cache/dedup/db.rs +++ b/storage/src/cache/dedup/db.rs @@ -8,7 +8,7 @@ use std::path::Path; use r2d2::{Pool, PooledConnection}; use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{Connection, DropBehavior, OptionalExtension, Transaction}; +use rusqlite::{Connection, DropBehavior, OpenFlags, OptionalExtension, Transaction}; use super::Result; @@ -24,7 +24,8 @@ impl CasDb { } pub fn from_file(db_path: impl AsRef) -> Result { - let mgr = SqliteConnectionManager::file(db_path); + let mgr = SqliteConnectionManager::file(db_path) + .with_flags(OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE); let pool = r2d2::Pool::new(mgr)?; let conn = pool.get()?; diff --git a/storage/src/cache/dedup/mod.rs b/storage/src/cache/dedup/mod.rs index f52a8fcc1de..0763c955b07 100644 --- a/storage/src/cache/dedup/mod.rs +++ b/storage/src/cache/dedup/mod.rs @@ -2,11 +2,26 @@ // // SPDX-License-Identifier: Apache-2.0 +use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; +use std::fs::{File, OpenOptions}; use std::io::Error; +use std::path::Path; +use std::sync::{Arc, Mutex, RwLock}; + +use nydus_utils::digest::RafsDigest; + +use crate::cache::dedup::db::CasDb; +use crate::device::{BlobChunkInfo, BlobInfo}; +use crate::utils::copy_file_range; mod db; +lazy_static::lazy_static!( + static ref CAS_MGR: Mutex>> = Mutex::new(None); +); + /// Error codes related to local cas. #[derive(Debug)] pub enum CasError { @@ -47,3 +62,162 @@ impl From for CasError { /// Specialized `Result` for local cas. type Result = std::result::Result; + +pub struct CasMgr { + db: CasDb, + fds: RwLock>>, +} + +impl CasMgr { + pub fn new(db_path: impl AsRef) -> Result { + let db = CasDb::from_file(db_path.as_ref())?; + + Ok(CasMgr { + db, + fds: RwLock::new(HashMap::new()), + }) + } + + pub fn set_singleton(mgr: CasMgr) { + *CAS_MGR.lock().unwrap() = Some(Arc::new(mgr)); + } + + pub fn get_singleton() -> Option> { + CAS_MGR.lock().unwrap().clone() + } + + /// Deduplicate chunk data from existing data files. + /// + /// If any error happens, just pretend there's no source data available for dedup. + pub fn dedup_chunk( + &self, + blob: &BlobInfo, + chunk: &dyn BlobChunkInfo, + cache_file: &File, + ) -> bool { + let key = Self::chunk_key(blob, chunk); + if key.is_empty() { + return false; + } + + if let Ok(Some((path, offset))) = self.db.get_chunk_info(&key) { + let guard = self.fds.read().unwrap(); + let mut d_file = guard.get(&path).cloned(); + drop(guard); + + // Open the source file for dedup on demand. + if d_file.is_none() { + match OpenOptions::new().read(true).open(&path) { + Err(e) => warn!("failed to open dedup source file {}, {}", path, e), + Ok(f) => { + let mut guard = self.fds.write().unwrap(); + match guard.entry(path) { + Entry::Vacant(e) => { + let f = Arc::new(f); + e.insert(f.clone()); + d_file = Some(f); + } + Entry::Occupied(f) => { + // Somebody else has inserted the file, use it + d_file = Some(f.get().clone()); + } + } + } + } + } + + if let Some(f) = d_file { + match copy_file_range( + f, + offset, + cache_file, + chunk.uncompressed_offset(), + chunk.uncompressed_size() as usize, + ) { + Ok(_) => { + return true; + } + Err(e) => warn!("{e}"), + } + } + } + + false + } + + /// Add an available chunk data into the CAS database. + pub fn record_chunk( + &self, + blob: &BlobInfo, + chunk: &dyn BlobChunkInfo, + path: impl AsRef, + ) -> Result<()> { + let key = Self::chunk_key(blob, chunk); + if key.is_empty() { + return Ok(()); + } + + let path = path.as_ref().canonicalize()?; + let path = path.display().to_string(); + self.record_chunk_raw(&key, &path, chunk.uncompressed_offset()) + } + + pub fn record_chunk_raw(&self, chunk_id: &str, path: &str, offset: u64) -> Result<()> { + self.db.add_blob(path)?; + self.db.add_chunk(chunk_id, offset, path)?; + Ok(()) + } + + fn chunk_key(blob: &BlobInfo, chunk: &dyn BlobChunkInfo) -> String { + let id = chunk.chunk_id(); + if *id == RafsDigest::default() { + String::new() + } else { + blob.digester().to_string() + ":" + &chunk.chunk_id().to_string() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::device::BlobFeatures; + use crate::test::MockChunkInfo; + use crate::RAFS_DEFAULT_CHUNK_SIZE; + use std::io::{Read, Write}; + use vmm_sys_util::tempfile::TempFile; + + #[test] + fn test_cas_chunk_op() { + let dbfile = TempFile::new().unwrap(); + let tmpfile2 = TempFile::new().unwrap(); + let src_path = tmpfile2.as_path().display().to_string(); + let mgr = CasMgr::new(dbfile.as_path()).unwrap(); + + let blob = BlobInfo::new( + 1, + src_path.clone(), + 8192, + 8192, + RAFS_DEFAULT_CHUNK_SIZE as u32, + 1, + BlobFeatures::empty(), + ); + let mut chunk = MockChunkInfo::new(); + chunk.block_id = RafsDigest { data: [3u8; 32] }; + chunk.uncompress_offset = 0; + chunk.uncompress_size = 8192; + let chunk = Arc::new(chunk) as Arc; + + let buf = vec![0x9u8; 8192]; + let mut src_file = tmpfile2.as_file().try_clone().unwrap(); + src_file.write_all(&buf).unwrap(); + mgr.record_chunk(&blob, chunk.as_ref(), &src_path).unwrap(); + + let mut tmpfile3 = TempFile::new().unwrap().into_file(); + assert!(mgr.dedup_chunk(&blob, chunk.as_ref(), &tmpfile3)); + let mut buf2 = vec![0x0u8; 8192]; + tmpfile3.read_exact(&mut buf2).unwrap(); + assert_eq!(buf, buf2); + } +} diff --git a/storage/src/cache/mod.rs b/storage/src/cache/mod.rs index 7d91862b78d..6a7417992f0 100644 --- a/storage/src/cache/mod.rs +++ b/storage/src/cache/mod.rs @@ -659,6 +659,12 @@ pub(crate) trait BlobCacheMgr: Send + Sync { fn check_stat(&self); } +#[cfg(feature = "dedup")] +pub use dedup::CasMgr; + +#[cfg(not(feature = "dedup"))] +pub struct CasMgr {} + #[cfg(test)] mod tests { use crate::device::{BlobChunkFlags, BlobFeatures}; From f737a3cf863928cfea91d5ddd90fa6698f39c6d6 Mon Sep 17 00:00:00 2001 From: Yadong Ding Date: Fri, 27 Sep 2024 09:41:22 +0800 Subject: [PATCH 4/7] storage: add garbage collection in CasMgr - Changed `delete_blobs` method in `CasDb` to take an immutable reference (`&self`) instead of a mutable reference (`&mut self`). - Updated `dedup_chunk` method in `CasMgr` to correctly handle the deletion of non-existent blob files from both the file descriptor cache and the database. - Implemented the `gc` (garbage collection) method in `CasMgr` to identify and remove blobs that no longer exist on the filesystem, ensuring the database and cache remain consistent. Signed-off-by: Yadong Ding --- storage/src/cache/dedup/db.rs | 2 +- storage/src/cache/dedup/mod.rs | 100 +++++++++++++++++++++++++++++++++ storage/src/utils.rs | 30 +++++++--- 3 files changed, 123 insertions(+), 9 deletions(-) diff --git a/storage/src/cache/dedup/db.rs b/storage/src/cache/dedup/db.rs index c505cde8480..f0cf493296b 100644 --- a/storage/src/cache/dedup/db.rs +++ b/storage/src/cache/dedup/db.rs @@ -129,7 +129,7 @@ impl CasDb { Ok(conn.last_insert_rowid() as u64) } - pub fn delete_blobs(&mut self, blobs: &[String]) -> Result<()> { + pub fn delete_blobs(&self, blobs: &[String]) -> Result<()> { let delete_blobs_sql = "DELETE FROM Blobs WHERE BlobId = (?1)"; let delete_chunks_sql = "DELETE FROM Chunks WHERE BlobId = (?1)"; let mut conn = self.get_connection()?; diff --git a/storage/src/cache/dedup/mod.rs b/storage/src/cache/dedup/mod.rs index 0763c955b07..64bb17d6e82 100644 --- a/storage/src/cache/dedup/mod.rs +++ b/storage/src/cache/dedup/mod.rs @@ -124,6 +124,15 @@ impl CasMgr { } } } + } else if d_file.as_ref().unwrap().metadata().is_err() { + // If the blob file no longer exists, delete if from fds and db. + let mut guard = self.fds.write().unwrap(); + guard.remove(&path); + let blob_ids: &[String] = &[path]; + if let Err(e) = self.db.delete_blobs(&blob_ids) { + warn!("failed to delete blobs: {}", e); + } + return false; } if let Some(f) = d_file { @@ -176,6 +185,33 @@ impl CasMgr { blob.digester().to_string() + ":" + &chunk.chunk_id().to_string() } } + + /// Check if blobs in the database still exist on the filesystem and perform garbage collection. + pub fn gc(&self) -> Result<()> { + let all_blobs = self.db.get_all_blobs()?; + let mut blobs_not_exist = Vec::new(); + for (_, file_path) in all_blobs { + if !std::path::Path::new(&file_path).exists() { + blobs_not_exist.push(file_path); + } + } + + // If there are any non-existent blobs, delete them from the database. + if !blobs_not_exist.is_empty() { + self.db.delete_blobs(&blobs_not_exist).map_err(|e| { + warn!("failed to delete blobs: {}", e); + e + })?; + } + + let mut guard = self.fds.write().unwrap(); + for path in blobs_not_exist { + // Remove the non-existent blob paths from the cache. + guard.remove(&path); + } + + Ok(()) + } } #[cfg(test)] @@ -220,4 +256,68 @@ mod tests { tmpfile3.read_exact(&mut buf2).unwrap(); assert_eq!(buf, buf2); } + + #[test] + fn test_cas_dedup_chunk_failed() { + let dbfile = TempFile::new().unwrap(); + let mgr = CasMgr::new(dbfile.as_path()).unwrap(); + + let new_blob = BlobInfo::new( + 1, + "test_blob".to_string(), + 8192, + 8192, + RAFS_DEFAULT_CHUNK_SIZE as u32, + 1, + BlobFeatures::empty(), + ); + + let mut chunk = MockChunkInfo::new(); + chunk.block_id = RafsDigest::default(); + chunk.uncompress_offset = 0; + chunk.uncompress_size = 8192; + let chunk = Arc::new(chunk) as Arc; + + let tmpfile = TempFile::new().unwrap().into_file(); + + assert!(!mgr.dedup_chunk(&new_blob, chunk.as_ref(), &tmpfile)); + } + + #[test] + fn test_cas_gc() { + let dbfile = TempFile::new().unwrap(); + let mgr = CasMgr::new(dbfile.as_path()).unwrap(); + + let tmpfile = TempFile::new().unwrap(); + let blob_path = tmpfile + .as_path() + .canonicalize() + .unwrap() + .display() + .to_string(); + let blob = BlobInfo::new( + 1, + blob_path.clone(), + 8192, + 8192, + RAFS_DEFAULT_CHUNK_SIZE as u32, + 1, + BlobFeatures::empty(), + ); + let mut chunk = MockChunkInfo::new(); + chunk.block_id = RafsDigest { data: [3u8; 32] }; + chunk.uncompress_offset = 0; + chunk.uncompress_size = 8192; + let chunk = Arc::new(chunk) as Arc; + mgr.record_chunk(&blob, chunk.as_ref(), &blob_path).unwrap(); + + let all_blobs_before_gc = mgr.db.get_all_blobs().unwrap(); + assert_eq!(all_blobs_before_gc.len(), 1); + + drop(tmpfile); + mgr.gc().unwrap(); + + let all_blobs_after_gc = mgr.db.get_all_blobs().unwrap(); + assert_eq!(all_blobs_after_gc.len(), 0); + } } diff --git a/storage/src/utils.rs b/storage/src/utils.rs index 85e397cf0a0..d5ad9f074f1 100644 --- a/storage/src/utils.rs +++ b/storage/src/utils.rs @@ -18,8 +18,11 @@ use std::cmp::{self, min}; use std::io::{ErrorKind, IoSliceMut, Result}; use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::io::RawFd; +#[cfg(target_os = "linux")] use std::path::PathBuf; use std::slice::from_raw_parts_mut; +#[cfg(target_os = "macos")] +use std::{ffi::CStr, mem, os::raw::c_char}; use vm_memory::bytes::Bytes; use crate::{StorageError, StorageResult}; @@ -173,6 +176,7 @@ pub fn copy_file_range( Ok(()) } +#[cfg(target_os = "linux")] pub fn get_path_from_file(file: &impl AsRawFd) -> Option { let path = PathBuf::from("/proc/self/fd").join(file.as_raw_fd().to_string()); match std::fs::read_link(&path) { @@ -184,6 +188,22 @@ pub fn get_path_from_file(file: &impl AsRawFd) -> Option { } } +#[cfg(target_os = "macos")] +pub fn get_path_from_file(file: &impl AsRawFd) -> Option { + let fd = file.as_raw_fd(); + let mut buf: [c_char; 1024] = unsafe { mem::zeroed() }; + + let result = unsafe { fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr()) }; + + if result == -1 { + warn!("Failed to get path from file descriptor"); + return None; + } + + let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) }; + cstr.to_str().ok().map(|s| s.to_string()) +} + /// An memory cursor to access an `FileVolatileSlice` array. pub struct MemSliceCursor<'a> { pub mem_slice: &'a [FileVolatileSlice<'a>], @@ -481,20 +501,14 @@ mod tests { assert!(copy_file_range(&empty_src, 0, dst.as_file(), 4096, 4096).is_err()); } - #[cfg(target_os = "linux")] #[test] fn test_get_path_from_file() { let temp_file = TempFile::new().unwrap(); let file = temp_file.as_file(); let path = get_path_from_file(file).unwrap(); assert_eq!(path, temp_file.as_path().display().to_string()); - } - #[cfg(not(target_os = "linux"))] - #[test] - fn test_get_path_from_file_non_linux() { - let temp_file = TempFile::new().unwrap(); - let file = temp_file.as_file(); - assert_eq!(get_path_from_file(file).is_none()); + let invalid_fd: RawFd = -1; + assert!(get_path_from_file(&invalid_fd).is_none()); } } From c2e5bfad78f97fd2f60e97c293b5dc5db2116ee1 Mon Sep 17 00:00:00 2001 From: Yadong Ding Date: Fri, 27 Sep 2024 09:42:00 +0800 Subject: [PATCH 5/7] storage: enable chunk deduplication for file cache Enable chunk deduplication for file cache. It works in this way: - When a chunk is not in blob cache file yet, inquery CAS database whether other blob data files have the required chunk. If there's duplicated data chunk in other data files, copy the chunk data into current blob cache file by using copy_file_range(). - After downloading a data chunk from remote, save file/offset/chunk-id into CAS database, so it can be reused later. Co-authored-by: Jiang Liu Co-authored-by: Yading Ding Signed-off-by: Yadong Ding --- Cargo.toml | 3 +++ src/bin/nydusd/main.rs | 23 +++++++++++++++++++-- storage/Cargo.toml | 1 - storage/src/cache/cachedfile.rs | 33 ++++++++++++++++++++++++++++-- storage/src/cache/filecache/mod.rs | 29 ++++++++++++++++++++++---- storage/src/cache/fscache/mod.rs | 29 ++++++++++++++++++++++---- 6 files changed, 105 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fbd64232437..9656f5a92c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ default = [ "backend-s3", "backend-http-proxy", "backend-localdisk", + "dedup", ] virtiofs = [ "nydus-service/virtiofs", @@ -116,6 +117,8 @@ backend-oss = ["nydus-storage/backend-oss"] backend-registry = ["nydus-storage/backend-registry"] backend-s3 = ["nydus-storage/backend-s3"] +dedup = ["nydus-storage/dedup"] + [workspace] members = [ "api", diff --git a/src/bin/nydusd/main.rs b/src/bin/nydusd/main.rs index fc5e4b7a6b8..ab138442f6b 100644 --- a/src/bin/nydusd/main.rs +++ b/src/bin/nydusd/main.rs @@ -26,6 +26,7 @@ use nydus_service::{ create_daemon, create_fuse_daemon, create_vfs_backend, validate_threads_configuration, Error as NydusError, FsBackendMountCmd, FsBackendType, ServiceArgs, }; +use nydus_storage::cache::CasMgr; use crate::api_server_glue::ApiServerController; @@ -50,7 +51,7 @@ fn thread_validator(v: &str) -> std::result::Result { } fn append_fs_options(app: Command) -> Command { - app.arg( + let mut app = app.arg( Arg::new("bootstrap") .long("bootstrap") .short('B') @@ -87,7 +88,18 @@ fn append_fs_options(app: Command) -> Command { .help("Mountpoint within the FUSE/virtiofs device to mount the RAFS/passthroughfs filesystem") .default_value("/") .required(false), - ) + ); + + #[cfg(feature = "dedup")] + { + app = app.arg( + Arg::new("dedup-db") + .long("dedup-db") + .help("Database file for chunk deduplication"), + ); + } + + app } fn append_fuse_options(app: Command) -> Command { @@ -750,6 +762,13 @@ fn main() -> Result<()> { dump_program_info(); handle_rlimit_nofile_option(&args, "rlimit-nofile")?; + #[cfg(feature = "dedup")] + if let Some(db) = args.get_one::("dedup-db") { + let mgr = CasMgr::new(db).map_err(|e| eother!(format!("{}", e)))?; + info!("Enable chunk deduplication by using database at {}", db); + CasMgr::set_singleton(mgr); + } + match args.subcommand_name() { Some("singleton") => { // Safe to unwrap because the subcommand is `singleton`. diff --git a/storage/Cargo.toml b/storage/Cargo.toml index a45ba0a1fe1..8636d5cb53c 100644 --- a/storage/Cargo.toml +++ b/storage/Cargo.toml @@ -58,7 +58,6 @@ regex = "1.7.0" toml = "0.5" [features] -default = ["dedup"] backend-localdisk = [] backend-localdisk-gpt = ["gpt", "backend-localdisk"] backend-localfs = [] diff --git a/storage/src/cache/cachedfile.rs b/storage/src/cache/cachedfile.rs index d30bcb1762b..a2432cb5fa7 100644 --- a/storage/src/cache/cachedfile.rs +++ b/storage/src/cache/cachedfile.rs @@ -13,6 +13,7 @@ use std::collections::HashSet; use std::fs::File; use std::io::{ErrorKind, Read, Result}; use std::mem::ManuallyDrop; +use std::ops::Deref; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; @@ -29,7 +30,7 @@ use tokio::runtime::Runtime; use crate::backend::BlobReader; use crate::cache::state::ChunkMap; use crate::cache::worker::{AsyncPrefetchConfig, AsyncPrefetchMessage, AsyncWorkerMgr}; -use crate::cache::{BlobCache, BlobIoMergeState}; +use crate::cache::{BlobCache, BlobIoMergeState, CasMgr}; use crate::device::{ BlobChunkInfo, BlobInfo, BlobIoDesc, BlobIoRange, BlobIoSegment, BlobIoTag, BlobIoVec, BlobObject, BlobPrefetchRequest, @@ -184,8 +185,10 @@ pub(crate) struct FileCacheEntry { pub(crate) blob_info: Arc, pub(crate) cache_cipher_object: Arc, pub(crate) cache_cipher_context: Arc, + pub(crate) cas_mgr: Option>, pub(crate) chunk_map: Arc, pub(crate) file: Arc, + pub(crate) file_path: Arc, pub(crate) meta: Option, pub(crate) metrics: Arc, pub(crate) prefetch_state: Arc, @@ -233,13 +236,16 @@ impl FileCacheEntry { } fn delay_persist_chunk_data(&self, chunk: Arc, buffer: Arc) { + let blob_info = self.blob_info.clone(); let delayed_chunk_map = self.chunk_map.clone(); let file = self.file.clone(); + let file_path = self.file_path.clone(); let metrics = self.metrics.clone(); let is_raw_data = self.is_raw_data; let is_cache_encrypted = self.is_cache_encrypted; let cipher_object = self.cache_cipher_object.clone(); let cipher_context = self.cache_cipher_context.clone(); + let cas_mgr = self.cas_mgr.clone(); metrics.buffered_backend_size.add(buffer.size() as u64); self.runtime.spawn_blocking(move || { @@ -291,6 +297,11 @@ impl FileCacheEntry { }; let res = Self::persist_cached_data(&file, offset, buf); Self::_update_chunk_pending_status(&delayed_chunk_map, chunk.as_ref(), res.is_ok()); + if let Some(mgr) = cas_mgr { + if let Err(e) = mgr.record_chunk(&blob_info, chunk.deref(), file_path.as_ref()) { + warn!("failed to record chunk state for dedup, {}", e); + } + } }); } @@ -1051,13 +1062,21 @@ impl FileCacheEntry { trace!("dispatch single io range {:?}", req); let mut blob_cci = BlobCCI::new(); for (i, chunk) in req.chunks.iter().enumerate() { - let is_ready = match self.chunk_map.check_ready_and_mark_pending(chunk.as_ref()) { + let mut is_ready = match self.chunk_map.check_ready_and_mark_pending(chunk.as_ref()) { Ok(true) => true, Ok(false) => false, Err(StorageError::Timeout) => false, // Retry if waiting for inflight IO timeouts Err(e) => return Err(einval!(e)), }; + if !is_ready { + if let Some(mgr) = self.cas_mgr.as_ref() { + is_ready = mgr.dedup_chunk(&self.blob_info, chunk.deref(), &self.file); + if is_ready { + self.update_chunk_pending_status(chunk.deref(), true); + } + } + } // Directly read chunk data from file cache into user buffer iff: // - the chunk is ready in the file cache // - data in the file cache is plaintext. @@ -1454,6 +1473,16 @@ impl FileCacheEntry { } } +impl Drop for FileCacheEntry { + fn drop(&mut self) { + if let Some(cas_mgr) = &self.cas_mgr { + if let Err(e) = cas_mgr.gc() { + warn!("cas_mgr gc failed: {}", e); + } + } + } +} + /// An enum to reuse existing buffers for IO operations, and CoW on demand. #[allow(dead_code)] enum DataBuffer { diff --git a/storage/src/cache/filecache/mod.rs b/storage/src/cache/filecache/mod.rs index e6b8c5b80da..11c7ac1143d 100644 --- a/storage/src/cache/filecache/mod.rs +++ b/storage/src/cache/filecache/mod.rs @@ -21,8 +21,9 @@ use crate::cache::state::{ BlobStateMap, ChunkMap, DigestedChunkMap, IndexedChunkMap, NoopChunkMap, }; use crate::cache::worker::{AsyncPrefetchConfig, AsyncWorkerMgr}; -use crate::cache::{BlobCache, BlobCacheMgr}; +use crate::cache::{BlobCache, BlobCacheMgr, CasMgr}; use crate::device::{BlobFeatures, BlobInfo}; +use crate::utils::get_path_from_file; pub const BLOB_RAW_FILE_SUFFIX: &str = ".blob.raw"; pub const BLOB_DATA_FILE_SUFFIX: &str = ".blob.data"; @@ -209,10 +210,19 @@ impl FileCacheEntry { reader.clone() }; + // Turn off chunk deduplication in case of cache data encryption is enabled or is tarfs. + let cas_mgr = if mgr.cache_encrypted || mgr.cache_raw_data || is_tarfs { + warn!("chunk deduplication trun off"); + None + } else { + CasMgr::get_singleton() + }; + let blob_compressed_size = Self::get_blob_size(&reader, &blob_info)?; let blob_uncompressed_size = blob_info.uncompressed_size(); let is_legacy_stargz = blob_info.is_legacy_stargz(); + let blob_file_path = format!("{}/{}", mgr.work_dir, blob_id); let ( file, meta, @@ -221,7 +231,6 @@ impl FileCacheEntry { is_get_blob_object_supported, need_validation, ) = if is_tarfs { - let blob_file_path = format!("{}/{}", mgr.work_dir, blob_id); let file = OpenOptions::new() .create(false) .write(false) @@ -231,7 +240,6 @@ impl FileCacheEntry { Arc::new(BlobStateMap::from(NoopChunkMap::new(true))) as Arc; (file, None, chunk_map, true, true, false) } else { - let blob_file_path = format!("{}/{}", mgr.work_dir, blob_id); let (chunk_map, is_direct_chunkmap) = Self::create_chunk_map(mgr, &blob_info, &blob_file_path)?; // Validation is supported by RAFS v5 (which has no meta_ci) or v6 with chunk digest array. @@ -266,6 +274,7 @@ impl FileCacheEntry { ); return Err(einval!(msg)); } + let load_chunk_digest = need_validation || cas_mgr.is_some(); let meta = if blob_info.meta_ci_is_valid() || blob_info.has_feature(BlobFeatures::IS_CHUNKDICT_GENERATED) { @@ -275,7 +284,7 @@ impl FileCacheEntry { Some(blob_meta_reader), Some(runtime.clone()), false, - need_validation, + load_chunk_digest, )?; Some(meta) } else { @@ -307,6 +316,16 @@ impl FileCacheEntry { (Default::default(), Default::default()) }; + let mut blob_data_file_path = String::new(); + if cas_mgr.is_some() { + blob_data_file_path = if let Some(path) = get_path_from_file(&file) { + path + } else { + warn!("can't get path from file"); + "".to_string() + } + } + trace!( "filecache entry: is_raw_data {}, direct {}, legacy_stargz {}, separate_meta {}, tarfs {}, batch {}, zran {}", mgr.cache_raw_data, @@ -322,8 +341,10 @@ impl FileCacheEntry { blob_info, cache_cipher_object, cache_cipher_context, + cas_mgr, chunk_map, file: Arc::new(file), + file_path: Arc::new(blob_data_file_path), meta, metrics: mgr.metrics.clone(), prefetch_state: Arc::new(AtomicU32::new(0)), diff --git a/storage/src/cache/fscache/mod.rs b/storage/src/cache/fscache/mod.rs index 5b2285c9b0e..6b22386b2cd 100644 --- a/storage/src/cache/fscache/mod.rs +++ b/storage/src/cache/fscache/mod.rs @@ -15,13 +15,13 @@ use tokio::runtime::Runtime; use crate::backend::BlobBackend; use crate::cache::cachedfile::{FileCacheEntry, FileCacheMeta}; +use crate::cache::filecache::BLOB_DATA_FILE_SUFFIX; use crate::cache::state::{BlobStateMap, IndexedChunkMap, RangeMap}; use crate::cache::worker::{AsyncPrefetchConfig, AsyncWorkerMgr}; -use crate::cache::{BlobCache, BlobCacheMgr}; +use crate::cache::{BlobCache, BlobCacheMgr, CasMgr}; use crate::device::{BlobFeatures, BlobInfo, BlobObject}; use crate::factory::BLOB_FACTORY; - -use crate::cache::filecache::BLOB_DATA_FILE_SUFFIX; +use crate::utils::get_path_from_file; const FSCACHE_BLOBS_CHECK_NUM: u8 = 1; @@ -240,9 +240,18 @@ impl FileCacheEntry { }; let blob_compressed_size = Self::get_blob_size(&reader, &blob_info)?; + // Turn off chunk deduplication in case of tarfs. + let cas_mgr = if is_tarfs { + warn!("chunk deduplication trun off"); + None + } else { + CasMgr::get_singleton() + }; + let need_validation = mgr.need_validation && !blob_info.is_legacy_stargz() && blob_info.has_feature(BlobFeatures::INLINED_CHUNK_DIGEST); + let load_chunk_digest = need_validation || cas_mgr.is_some(); let blob_file_path = format!("{}/{}", mgr.work_dir, blob_meta_id); let meta = if blob_info.meta_ci_is_valid() { FileCacheMeta::new( @@ -251,7 +260,7 @@ impl FileCacheEntry { Some(blob_meta_reader), None, true, - need_validation, + load_chunk_digest, )? } else { return Err(enosys!( @@ -266,13 +275,25 @@ impl FileCacheEntry { )?)); Self::restore_chunk_map(blob_info.clone(), file.clone(), &meta, &chunk_map); + let mut blob_data_file_path = String::new(); + if cas_mgr.is_some() { + blob_data_file_path = if let Some(path) = get_path_from_file(&file) { + path + } else { + warn!("can't get path from file"); + "".to_string() + } + } + Ok(FileCacheEntry { blob_id, blob_info: blob_info.clone(), cache_cipher_object: Default::default(), cache_cipher_context: Default::default(), + cas_mgr, chunk_map, file, + file_path: Arc::new(blob_data_file_path), meta: Some(meta), metrics: mgr.metrics.clone(), prefetch_state: Arc::new(AtomicU32::new(0)), From a9388490ad8e57ad66065e7b5c14874f5dcbe8eb Mon Sep 17 00:00:00 2001 From: Jiang Liu Date: Fri, 27 Sep 2024 09:43:20 +0800 Subject: [PATCH 6/7] docs: add documentation for cas Add documentation for cas. Signed-off-by: Jiang Liu --- docs/data-deduplication.md | 23 +- docs/images/chunk_dedup_l2_cache.drawio | 265 ++++++++++++++++++++++++ docs/images/chunk_dedup_l2_cache.png | Bin 0 -> 66805 bytes 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 docs/images/chunk_dedup_l2_cache.drawio create mode 100644 docs/images/chunk_dedup_l2_cache.png diff --git a/docs/data-deduplication.md b/docs/data-deduplication.md index 45b259ad204..1b6e0305ec3 100644 --- a/docs/data-deduplication.md +++ b/docs/data-deduplication.md @@ -164,4 +164,25 @@ So Nydus provides a node level CAS system to reduce data downloaded from the reg The node level CAS system helps to achieve O4 and O5. -# Node Level CAS System (WIP) +# Node Level CAS System (Experimental) +Data deduplication can also be achieved when accessing Nydus images. The key idea is to maintain information about data chunks available on local host by using a database. +When a chunk is needed but not available in the uncompressed data blob files yet, we will query the database using chunk digest as key. +If a record with the same chunk digest already exists, it will be reused. +We call such a system as CAS (Content Addressable Storage). + +## Chunk Deduplication by Using CAS as L2 Cache +In this chunk deduplication mode, the CAS system works as an L2 cache to provide chunk data on demand, and it keeps Nydus bootstrap blobs as is. +It works in this way: +1. query the database when a chunk is needed but not available yet +2. copy data from source blob to target blob using `copy_file_range` if a record with the same chunk digest +3. download chunk data from remote if there's no record in database +4. insert a new record into the database for just downloaded chunk so it can be reused later. + +![chunk_dedup_l2cache](images/chunk_dedup_l2_cache.png) + +A data download operation can be avoided if a chunk already exists in the database. +And if the underlying filesystem support data reference, `copy_file_range` will use reference instead of data copy, thus reduce storage space consumption. +This design has benefit of robustness, the target blob file doesn't have any dependency on the database and source blob files, so ease garbage collection. +But it depends on capability of underlying filesystem to reduce storage consumption. + +## Chunk Deduplication by Rebuilding Nydus Bootstrap (WIP) diff --git a/docs/images/chunk_dedup_l2_cache.drawio b/docs/images/chunk_dedup_l2_cache.drawio new file mode 100644 index 00000000000..c7d1417615b --- /dev/null +++ b/docs/images/chunk_dedup_l2_cache.drawio @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/chunk_dedup_l2_cache.png b/docs/images/chunk_dedup_l2_cache.png new file mode 100644 index 0000000000000000000000000000000000000000..e931e3f6927850b4bc10f00084fd0a0849e9f690 GIT binary patch literal 66805 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

hBM%UdQ_ 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 TGodkOz>kim!HrV&JCFYlA{8_c literal 0 HcmV?d00001 From a9b8fe4ffcd34983b4c873f4b0486d947fd429b5 Mon Sep 17 00:00:00 2001 From: Yadong Ding Date: Tue, 1 Oct 2024 14:09:33 +0800 Subject: [PATCH 7/7] smoke: add smoking test for cas and chunk dedup Add smoking test case for cas and chunk dedup. Signed-off-by: Yadong Ding --- smoke/go.mod | 2 + smoke/go.sum | 6 ++ smoke/tests/cas_test.go | 140 ++++++++++++++++++++++++++++++++ smoke/tests/chunk_dedup_test.go | 125 ++++++++++++++++++++++++++++ smoke/tests/texture/layer.go | 28 +++++++ smoke/tests/tool/context.go | 1 + smoke/tests/tool/nydusd.go | 6 +- storage/src/cache/cachedfile.rs | 13 ++- 8 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 smoke/tests/cas_test.go create mode 100644 smoke/tests/chunk_dedup_test.go diff --git a/smoke/go.mod b/smoke/go.mod index dbf95cf434f..d68c7193fba 100644 --- a/smoke/go.mod +++ b/smoke/go.mod @@ -12,6 +12,7 @@ require ( github.com/pkg/xattr v0.4.9 github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.15.0 + github.com/mattn/go-sqlite3 v1.14.23 ) require ( @@ -27,6 +28,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-sqlite3 v1.14.23 // indirect github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect diff --git a/smoke/go.sum b/smoke/go.sum index 0fa56cfafd8..67e97a62e87 100644 --- a/smoke/go.sum +++ b/smoke/go.sum @@ -10,6 +10,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= +github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= @@ -53,6 +54,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -67,7 +69,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -135,6 +140,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/smoke/tests/cas_test.go b/smoke/tests/cas_test.go new file mode 100644 index 00000000000..6520f6f5bdf --- /dev/null +++ b/smoke/tests/cas_test.go @@ -0,0 +1,140 @@ +// Copyright 2024 Nydus Developers. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + + "github.com/dragonflyoss/nydus/smoke/tests/texture" + "github.com/dragonflyoss/nydus/smoke/tests/tool" + "github.com/dragonflyoss/nydus/smoke/tests/tool/test" + "github.com/stretchr/testify/require" +) + +type CasTestSuite struct{} + +func (c *CasTestSuite) TestCasTables() test.Generator { + scenarios := tool.DescartesIterator{} + scenarios.Dimension(paramEnablePrefetch, []interface{}{false, true}) + + return func() (name string, testCase test.Case) { + if !scenarios.HasNext() { + return + } + scenario := scenarios.Next() + + return scenario.Str(), func(t *testing.T) { + c.testCasTables(t, scenario.GetBool(paramEnablePrefetch)) + } + } +} + +func (c *CasTestSuite) testCasTables(t *testing.T, enablePrefetch bool) { + ctx, layer := texture.PrepareLayerWithContext(t) + ctx.Runtime.EnablePrefetch = enablePrefetch + ctx.Runtime.ChunkDedupDb = filepath.Join(ctx.Env.WorkDir, "cas.db") + + nydusd, err := tool.NewNydusdWithContext(*ctx) + require.NoError(t, err) + err = nydusd.Mount() + require.NoError(t, err) + defer nydusd.Umount() + nydusd.Verify(t, layer.FileTree) + + db, err := sql.Open("sqlite3", ctx.Runtime.ChunkDedupDb) + require.NoError(t, err) + defer db.Close() + + for _, expectedTable := range []string{"Blobs", "Chunks"} { + var count int + query := fmt.Sprintf("SELECT COUNT(*) FROM %s;", expectedTable) + err := db.QueryRow(query).Scan(&count) + require.NoError(t, err) + if expectedTable == "Blobs" { + require.Equal(t, count, 1) + } else { + require.Equal(t, count, 8) + } + } +} + +func (c *CasTestSuite) TestCasGc() test.Generator { + scenarios := tool.DescartesIterator{} + scenarios.Dimension(paramEnablePrefetch, []interface{}{false, true}) + + return func() (name string, testCase test.Case) { + if !scenarios.HasNext() { + return + } + scenario := scenarios.Next() + + return scenario.Str(), func(t *testing.T) { + c.testCasGc(t, scenario.GetBool(paramEnablePrefetch)) + } + } +} + +func (c *CasTestSuite) testCasGc(t *testing.T, enablePrefetch bool) { + ctx, layer := texture.PrepareLayerWithContext(t) + defer ctx.Destroy(t) + config := tool.NydusdConfig{ + NydusdPath: ctx.Binary.Nydusd, + MountPath: ctx.Env.MountDir, + APISockPath: filepath.Join(ctx.Env.WorkDir, "nydusd-api.sock"), + ConfigPath: filepath.Join(ctx.Env.WorkDir, "nydusd-config.fusedev.json"), + ChunkDedupDb: filepath.Join(ctx.Env.WorkDir, "cas.db"), + } + nydusd, err := tool.NewNydusd(config) + require.NoError(t, err) + + err = nydusd.Mount() + defer nydusd.Umount() + require.NoError(t, err) + + config.BootstrapPath = ctx.Env.BootstrapPath + config.MountPath = "/mount" + config.BackendType = "localfs" + config.BackendConfig = fmt.Sprintf(`{"dir": "%s"}`, ctx.Env.BlobDir) + config.BlobCacheDir = ctx.Env.CacheDir + config.CacheType = ctx.Runtime.CacheType + config.CacheCompressed = ctx.Runtime.CacheCompressed + config.RafsMode = ctx.Runtime.RafsMode + config.EnablePrefetch = enablePrefetch + config.DigestValidate = false + config.AmplifyIO = ctx.Runtime.AmplifyIO + err = nydusd.MountByAPI(config) + require.NoError(t, err) + + nydusd.VerifyByPath(t, layer.FileTree, config.MountPath) + + db, err := sql.Open("sqlite3", config.ChunkDedupDb) + require.NoError(t, err) + defer db.Close() + + // Mock nydus snapshotter clear cache + os.RemoveAll(filepath.Join(ctx.Env.WorkDir, "cache")) + time.Sleep(1 * time.Second) + + nydusd.UmountByAPI(config.MountPath) + + for _, expectedTable := range []string{"Blobs", "Chunks"} { + var count int + query := fmt.Sprintf("SELECT COUNT(*) FROM %s;", expectedTable) + err := db.QueryRow(query).Scan(&count) + require.NoError(t, err) + require.Zero(t, count) + } +} + +func TestCas(t *testing.T) { + test.Run(t, &CasTestSuite{}) +} diff --git a/smoke/tests/chunk_dedup_test.go b/smoke/tests/chunk_dedup_test.go new file mode 100644 index 00000000000..eca4d362e86 --- /dev/null +++ b/smoke/tests/chunk_dedup_test.go @@ -0,0 +1,125 @@ +// Copyright 2024 Nydus Developers. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/dragonflyoss/nydus/smoke/tests/texture" + "github.com/dragonflyoss/nydus/smoke/tests/tool" + "github.com/dragonflyoss/nydus/smoke/tests/tool/test" +) + +const ( + paramIteration = "iteration" +) + +type ChunkDedupTestSuite struct{} + +type BackendMetrics struct { + ReadCount uint64 `json:"read_count"` + ReadAmountTotal uint64 `json:"read_amount_total"` + ReadErrors uint64 `json:"read_errors"` +} + +func (c *ChunkDedupTestSuite) TestChunkDedup() test.Generator { + scenarios := tool.DescartesIterator{} + scenarios.Dimension(paramIteration, []interface{}{1}) + + file, _ := os.CreateTemp("", "cas-*.db") + defer os.Remove(file.Name()) + + return func() (name string, testCase test.Case) { + if !scenarios.HasNext() { + return + } + scenario := scenarios.Next() + + return scenario.Str(), func(t *testing.T) { + c.testRemoteWithDedup(t, file.Name()) + } + } +} + +func (c *ChunkDedupTestSuite) testRemoteWithDedup(t *testing.T, dbPath string) { + ctx, layer := texture.PrepareLayerWithContext(t) + defer ctx.Destroy(t) + ctx.Runtime.EnablePrefetch = false + ctx.Runtime.ChunkDedupDb = dbPath + + nydusd, err := tool.NewNydusdWithContext(*ctx) + require.NoError(t, err) + err = nydusd.Mount() + require.NoError(t, err) + defer nydusd.Umount() + nydusd.Verify(t, layer.FileTree) + metrics := c.getBackendMetrics(t, filepath.Join(ctx.Env.WorkDir, "nydusd-api.sock")) + require.Zero(t, metrics.ReadErrors) + + ctx2, layer2 := texture.PrepareLayerWithContext(t) + defer ctx2.Destroy(t) + ctx2.Runtime.EnablePrefetch = false + ctx2.Runtime.ChunkDedupDb = dbPath + + nydusd2, err := tool.NewNydusdWithContext(*ctx2) + require.NoError(t, err) + err = nydusd2.Mount() + require.NoError(t, err) + defer nydusd2.Umount() + nydusd2.Verify(t, layer2.FileTree) + metrics2 := c.getBackendMetrics(t, filepath.Join(ctx2.Env.WorkDir, "nydusd-api.sock")) + require.Zero(t, metrics2.ReadErrors) + + require.Greater(t, metrics.ReadCount, metrics2.ReadCount) + require.Greater(t, metrics.ReadAmountTotal, metrics2.ReadAmountTotal) +} + +func (c *ChunkDedupTestSuite) getBackendMetrics(t *testing.T, sockPath string) *BackendMetrics { + transport := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + dialer := &net.Dialer{ + Timeout: 5 * time.Second, + KeepAlive: 5 * time.Second, + } + return dialer.DialContext(ctx, "unix", sockPath) + }, + } + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: transport, + } + + resp, err := client.Get("http://unix/api/v1/metrics/backend") + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var metrics BackendMetrics + if err = json.Unmarshal(body, &metrics); err != nil { + require.NoError(t, err) + } + + return &metrics +} + +func TestChunkDedup(t *testing.T) { + test.Run(t, &ChunkDedupTestSuite{}) +} diff --git a/smoke/tests/texture/layer.go b/smoke/tests/texture/layer.go index c6a4933b3fe..ab64bff2a85 100644 --- a/smoke/tests/texture/layer.go +++ b/smoke/tests/texture/layer.go @@ -10,7 +10,10 @@ import ( "syscall" "testing" + "github.com/containerd/nydus-snapshotter/pkg/converter" "github.com/dragonflyoss/nydus/smoke/tests/tool" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/require" ) type LayerMaker func(t *testing.T, layer *tool.Layer) @@ -135,3 +138,28 @@ func MakeMatrixLayer(t *testing.T, workDir, id string) *tool.Layer { return layer } + +func PrepareLayerWithContext(t *testing.T) (*tool.Context, *tool.Layer) { + ctx := tool.DefaultContext(t) + + // Prepare work directory + ctx.PrepareWorkDir(t) + + lowerLayer := MakeLowerLayer(t, filepath.Join(ctx.Env.WorkDir, "source")) + lowerOCIBlobDigest, lowerRafsBlobDigest := lowerLayer.PackRef(t, *ctx, ctx.Env.BlobDir, ctx.Build.OCIRefGzip) + mergeOption := converter.MergeOption{ + BuilderPath: ctx.Binary.Builder, + ChunkDictPath: "", + OCIRef: true, + } + actualDigests, lowerBootstrap := tool.MergeLayers(t, *ctx, mergeOption, []converter.Layer{ + { + Digest: lowerRafsBlobDigest, + OriginalDigest: &lowerOCIBlobDigest, + }, + }) + require.Equal(t, []digest.Digest{lowerOCIBlobDigest}, actualDigests) + + ctx.Env.BootstrapPath = lowerBootstrap + return ctx, lowerLayer +} diff --git a/smoke/tests/tool/context.go b/smoke/tests/tool/context.go index 04cf6d851cf..b1215f59b7d 100644 --- a/smoke/tests/tool/context.go +++ b/smoke/tests/tool/context.go @@ -39,6 +39,7 @@ type RuntimeContext struct { RafsMode string EnablePrefetch bool AmplifyIO uint64 + ChunkDedupDb string } type EnvContext struct { diff --git a/smoke/tests/tool/nydusd.go b/smoke/tests/tool/nydusd.go index c340cce88b1..344588421c6 100644 --- a/smoke/tests/tool/nydusd.go +++ b/smoke/tests/tool/nydusd.go @@ -74,6 +74,7 @@ type NydusdConfig struct { AccessPattern bool PrefetchFiles []string AmplifyIO uint64 + ChunkDedupDb string // Hot Upgrade config. Upgrade bool SupervisorSockPath string @@ -193,6 +194,9 @@ func newNydusd(conf NydusdConfig) (*Nydusd, error) { if len(conf.BootstrapPath) > 0 { args = append(args, "--bootstrap", conf.BootstrapPath) } + if len(conf.ChunkDedupDb) > 0 { + args = append(args, "--dedup-db", conf.ChunkDedupDb) + } if conf.Upgrade { args = append(args, "--upgrade") } @@ -276,6 +280,7 @@ func NewNydusdWithContext(ctx Context) (*Nydusd, error) { RafsMode: ctx.Runtime.RafsMode, DigestValidate: false, AmplifyIO: ctx.Runtime.AmplifyIO, + ChunkDedupDb: ctx.Runtime.ChunkDedupDb, } if err := makeConfig(NydusdConfigTpl, conf); err != nil { @@ -346,7 +351,6 @@ func (nydusd *Nydusd) MountByAPI(config NydusdConfig) error { ) return err - } func (nydusd *Nydusd) Umount() error { diff --git a/storage/src/cache/cachedfile.rs b/storage/src/cache/cachedfile.rs index a2432cb5fa7..43c65cc3f77 100644 --- a/storage/src/cache/cachedfile.rs +++ b/storage/src/cache/cachedfile.rs @@ -299,7 +299,10 @@ impl FileCacheEntry { Self::_update_chunk_pending_status(&delayed_chunk_map, chunk.as_ref(), res.is_ok()); if let Some(mgr) = cas_mgr { if let Err(e) = mgr.record_chunk(&blob_info, chunk.deref(), file_path.as_ref()) { - warn!("failed to record chunk state for dedup, {}", e); + warn!( + "failed to record chunk state for dedup in delay_persist_chunk_data, {}", + e + ); } } }); @@ -309,6 +312,14 @@ impl FileCacheEntry { let offset = chunk.uncompressed_offset(); let res = Self::persist_cached_data(&self.file, offset, buf); self.update_chunk_pending_status(chunk, res.is_ok()); + if let Some(mgr) = &self.cas_mgr { + if let Err(e) = mgr.record_chunk(&self.blob_info, chunk, self.file_path.as_ref()) { + warn!( + "failed to record chunk state for dedup in persist_chunk_data, {}", + e + ); + } + } } fn persist_cached_data(file: &Arc, offset: u64, buffer: &[u8]) -> Result<()> {