volume: open volume files with O_NOATIME (#11055)

* volume server: open volume files with O_NOATIME

Nothing reads the atime of .dat, .idx, .sdx, or EC files, but every
needle read still dirtied the inode: even relatime writes atime on the
first read after each write, so an actively written volume paid a
metadata write per read/write cycle, and strictatime mounts paid one
per read. Open the serving handles with O_NOATIME, falling back to a
plain open when the file belongs to another owner (EPERM).

Claude-Session: https://claude.ai/code/session_015uVY4diBgEn3VYQoc2eMuD

* seaweed-volume: mirror the O_NOATIME volume file opens

Same change as the Go volume server: serving handles for .dat, .idx,
.sdx, .ecx, .ecj, and shard files open with O_NOATIME on Linux, with a
plain-open fallback on EPERM.

Claude-Session: https://claude.ai/code/session_015uVY4diBgEn3VYQoc2eMuD

* route the tier-down and recreate .dat opens through the no-atime helper

Review caught the Rust tier-down swap opening the local .dat directly.
The Go swapToLocalDatBackend and the zero-length read-only .dat
recreate in maybeWriteSuperBlock had the same gap: all three install
long-lived serving handles.

Claude-Session: https://claude.ai/code/session_015uVY4diBgEn3VYQoc2eMuD
This commit is contained in:
Chris Lu
2026-08-31 21:41:50 -07:00
committed by GitHub
parent 0c59c0fb05
commit 1996c6aec6
17 changed files with 132 additions and 59 deletions
@@ -4,6 +4,7 @@ use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use crate::storage::types::*;
use crate::storage::volume_open::open_volume_file;
pub const DATA_SHARDS_COUNT: usize = 10;
pub const PARITY_SHARDS_COUNT: usize = 4;
@@ -50,7 +51,7 @@ impl EcVolumeShard {
/// Open the shard file for reading.
pub fn open(&mut self) -> io::Result<()> {
let path = self.file_name();
let file = File::open(&path)?;
let file = open_volume_file(OpenOptions::new().read(true), &path)?;
self.ecd_file_size = file.metadata()?.len() as i64;
self.ecd_file = Some(file);
Ok(())
@@ -59,12 +60,14 @@ impl EcVolumeShard {
/// Create the shard file for writing.
pub fn create(&mut self) -> io::Result<()> {
let path = self.file_name();
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)?;
let file = open_volume_file(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true),
&path,
)?;
self.ecd_file = Some(file);
self.ecd_file_size = 0;
Ok(())
@@ -14,6 +14,7 @@ use crate::storage::erasure_coding::ec_locate;
use crate::storage::erasure_coding::ec_shard::*;
use crate::storage::needle::needle::{get_actual_size, Needle, NeedleError};
use crate::storage::types::*;
use crate::storage::volume_open::open_volume_file;
/// An erasure-coded volume managing its local shards and index.
pub struct EcVolume {
@@ -435,7 +436,7 @@ impl EcVolume {
// Matches Go which opens ecx for writing via MarkNeedleDeleted.
let ecx_path = vol.ecx_file_name();
if std::path::Path::new(&ecx_path).exists() {
let file = OpenOptions::new().read(true).write(true).open(&ecx_path)?;
let file = open_volume_file(OpenOptions::new().read(true).write(true), &ecx_path)?;
vol.ecx_file_size = file.metadata()?.len() as i64;
vol.ecx_file = Some(file);
} else if dir_idx != dir {
@@ -447,7 +448,8 @@ impl EcVolume {
volume_id = volume_id.0,
"ecx file not found in idx dir, falling back to data dir"
);
let file = OpenOptions::new().read(true).write(true).open(&fallback_ecx)?;
let file =
open_volume_file(OpenOptions::new().read(true).write(true), &fallback_ecx)?;
vol.ecx_file_size = file.metadata()?.len() as i64;
vol.ecx_file = Some(file);
vol.ecx_actual_dir = dir.to_string();
@@ -463,12 +465,14 @@ impl EcVolume {
let ecj_base =
crate::storage::volume::volume_file_name(&vol.ecx_actual_dir, collection, volume_id);
let ecj_path = format!("{}.ecj", ecj_base);
let ecj_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.append(true)
.open(&ecj_path)?;
let ecj_file = open_volume_file(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.append(true),
&ecj_path,
)?;
vol.ecj_file_size = ecj_file.metadata()?.len() as i64;
vol.ecj_file = Some(ecj_file);
@@ -1559,12 +1563,14 @@ impl EcVolume {
// in-memory deleted set (all of its contents are now materialized
// in .ecx), and reset the cached size.
fs::remove_file(&ecj_path)?;
let ecj_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.append(true)
.open(&ecj_path)?;
let ecj_file = open_volume_file(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.append(true),
&ecj_path,
)?;
self.ecj_file = Some(ecj_file);
self.ecj_file_size = 0;
if let Ok(mut set) = self.deleted_needles.write() {
+1
View File
@@ -10,5 +10,6 @@ pub mod super_block;
pub mod types;
pub mod volume;
pub mod volume_idx_repair;
pub mod volume_open;
pub mod volume_report;
pub mod volume_report_hash;
@@ -18,6 +18,8 @@ use std::fs::{File, OpenOptions};
use std::io;
use std::sync::{Arc, Mutex, OnceLock};
use crate::storage::volume_open::open_volume_file;
/// Descriptors the pool keeps open. Matches Go's `maxPooledIndexFiles`.
pub const MAX_POOLED_INDEX_FILES: usize = 1024;
@@ -68,7 +70,10 @@ impl IndexFilePool {
// Opened outside the lock: a cold open blocks on disk, and holding a
// process-wide mutex across it would serialize every volume's lookups.
let file = Arc::new(OpenOptions::new().read(true).write(writable).open(path)?);
let file = Arc::new(open_volume_file(
OpenOptions::new().read(true).write(writable),
path,
)?);
Ok(self.insert(key, file))
}
+18 -23
View File
@@ -26,6 +26,7 @@ use crate::storage::needle_map::sorted_file::SortedFileNeedleMap;
use crate::storage::needle_map::{CompactNeedleMap, NeedleMap, NeedleMapKind, RedbNeedleMap};
use crate::storage::super_block::{ReplicaPlacement, SuperBlock, SUPER_BLOCK_SIZE};
use crate::storage::types::*;
use crate::storage::volume_open::open_volume_file;
// ============================================================================
// Errors
@@ -736,12 +737,12 @@ impl Volume {
let metadata = fs::metadata(&dat_path)?;
// Try to open read-write; fall back to read-only
match OpenOptions::new().read(true).write(true).open(&dat_path) {
match open_volume_file(OpenOptions::new().read(true).write(true), &dat_path) {
Ok(file) => {
self.dat_file = Some(file);
}
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
self.dat_file = Some(File::open(&dat_path)?);
self.dat_file = Some(open_volume_file(OpenOptions::new().read(true), &dat_path)?);
self.no_write_or_delete = true;
}
Err(e) => return Err(e.into()),
@@ -762,11 +763,10 @@ impl Volume {
if let Some(parent) = Path::new(&dat_path).parent() {
fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&dat_path)?;
let file = open_volume_file(
OpenOptions::new().read(true).write(true).create(true),
&dat_path,
)?;
if preallocate > 0 {
preallocate_file(&file, preallocate);
}
@@ -982,7 +982,7 @@ impl Volume {
if self.no_write_or_delete {
// Open read-only
if Path::new(&idx_path).exists() {
let mut idx_file = File::open(&idx_path)?;
let mut idx_file = open_volume_file(OpenOptions::new().read(true), idx_path)?;
let nm = CompactNeedleMap::load_from_idx(&mut idx_file, self.version())?;
self.nm = Some(NeedleMap::InMemory(nm));
} else {
@@ -1001,11 +1001,10 @@ impl Volume {
}
} else {
// Open read-write (create if missing)
let idx_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&idx_path)?;
let idx_file = open_volume_file(
OpenOptions::new().read(true).write(true).create(true),
idx_path,
)?;
let idx_size = trim_torn_idx_tail(&idx_file, idx_path)?;
let mut idx_reader = io::BufReader::new(&idx_file);
@@ -1031,7 +1030,7 @@ impl Volume {
if self.no_write_or_delete {
// Open read-only
if Path::new(&idx_path).exists() {
let mut idx_file = File::open(&idx_path)?;
let mut idx_file = open_volume_file(OpenOptions::new().read(true), idx_path)?;
let nm = RedbNeedleMap::load_from_idx(&rdb_path, &mut idx_file, self.version())?;
self.nm = Some(NeedleMap::Redb(nm));
} else {
@@ -1050,11 +1049,10 @@ impl Volume {
}
} else {
// Open read-write (create if missing)
let idx_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&idx_path)?;
let idx_file = open_volume_file(
OpenOptions::new().read(true).write(true).create(true),
idx_path,
)?;
let idx_size = trim_torn_idx_tail(&idx_file, idx_path)?;
let mut idx_reader = io::BufReader::new(&idx_file);
@@ -2696,10 +2694,7 @@ impl Volume {
/// Open the local .dat as the data backend, dropping any remote backend, so reads
/// are served from local disk. Mirrors Go's swapToLocalDatBackend after a tier-down.
pub(crate) fn open_local_dat_backend(&mut self) -> Result<(), VolumeError> {
let file = OpenOptions::new()
.read(true)
.write(true)
.open(self.dat_path())?;
let file = open_volume_file(OpenOptions::new().read(true).write(true), self.dat_path())?;
self.remote_dat_file = None;
self.dat_file = Some(file);
Ok(())
+27
View File
@@ -0,0 +1,27 @@
//! Opening volume data and index files without access-time updates.
use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
/// Open a volume data or index file with `O_NOATIME`. Nothing reads these
/// files' atime, but without the flag every needle read dirties the inode —
/// even relatime writes atime on the first read after each write, so an
/// actively written volume pays a metadata write per read/write cycle.
/// Matches Go's `backend.OpenVolumeFile`.
pub fn open_volume_file(opts: &OpenOptions, path: impl AsRef<Path>) -> io::Result<File> {
let path = path.as_ref();
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::OpenOptionsExt;
let mut noatime = opts.clone();
noatime.custom_flags(libc::O_NOATIME);
match noatime.open(path) {
// O_NOATIME is refused unless we own the file or hold CAP_FOWNER.
Err(e) if e.raw_os_error() == Some(libc::EPERM) => opts.open(path),
result => result,
}
}
#[cfg(not(target_os = "linux"))]
opts.open(path)
}
+1 -1
View File
@@ -125,7 +125,7 @@ func (vs *VolumeServer) VolumeTierMoveDatFromRemote(req *volume_server_pb.Volume
// swapToLocalDatBackend closes the remote data backend and opens the downloaded
// local .dat as a DiskFile so reads are served from local disk.
func swapToLocalDatBackend(v *storage.Volume, datFileName string) error {
dataFile, err := os.OpenFile(datFileName, os.O_RDWR, 0644)
dataFile, err := backend.OpenVolumeFile(datFileName, os.O_RDWR)
if err != nil {
return err
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
)
func CreateVolumeFile(fileName string, preallocate int64, memoryMapSizeMB uint32) (BackendStorageFile, error) {
file, e := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
file, e := OpenVolumeFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
if e != nil {
return nil, e
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
func CreateVolumeFile(fileName string, preallocate int64, memoryMapSizeMB uint32) (BackendStorageFile, error) {
file, e := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
file, e := OpenVolumeFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
if e != nil {
return nil, e
}
+11
View File
@@ -0,0 +1,11 @@
//go:build !linux
package backend
import "os"
// OpenVolumeFile opens a volume data or index file. Only Linux can suppress
// atime updates per file descriptor; elsewhere this is a plain open.
func OpenVolumeFile(fileName string, flag int) (*os.File, error) {
return os.OpenFile(fileName, flag, 0644)
}
+22
View File
@@ -0,0 +1,22 @@
//go:build linux
package backend
import (
"errors"
"os"
"syscall"
)
// OpenVolumeFile opens a volume data or index file with O_NOATIME. Nothing
// reads these files' atime, but without the flag every needle read dirties
// the inode — even relatime writes atime on the first read after each write,
// so an actively written volume pays a metadata write per read/write cycle.
func OpenVolumeFile(fileName string, flag int) (*os.File, error) {
file, err := os.OpenFile(fileName, flag|syscall.O_NOATIME, 0644)
if err != nil && errors.Is(err, syscall.EPERM) {
// O_NOATIME is refused unless we own the file or hold CAP_FOWNER.
return os.OpenFile(fileName, flag, 0644)
}
return file, err
}
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
@@ -77,7 +78,7 @@ func NewEcVolumeShard(diskType types.DiskType, dirname string, collection string
baseFileName := v.FileName()
// open ecd file
if v.ecdFile, e = os.OpenFile(baseFileName+ToExt(int(shardId)), os.O_RDONLY, 0644); e != nil {
if v.ecdFile, e = backend.OpenVolumeFile(baseFileName+ToExt(int(shardId)), os.O_RDONLY); e != nil {
if e == os.ErrNotExist || strings.Contains(e.Error(), "no such file or directory") {
return nil, os.ErrNotExist
}
+3 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/idx"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
@@ -132,7 +133,7 @@ func NewEcVolume(diskType types.DiskType, dir string, dirIdx string, collection
default:
return nil, fmt.Errorf("cannot open ec volume index %s.ecx (or %s.ecx): %w", localBaseFileName, sharedBaseFileName, os.ErrNotExist)
}
if ev.ecxFile, err = os.OpenFile(indexBaseFileName+".ecx", os.O_RDWR, 0644); err != nil {
if ev.ecxFile, err = backend.OpenVolumeFile(indexBaseFileName+".ecx", os.O_RDWR); err != nil {
return nil, fmt.Errorf("cannot open ec volume index %s.ecx: %w", indexBaseFileName, err)
}
ecxFi, statErr := ev.ecxFile.Stat()
@@ -144,7 +145,7 @@ func NewEcVolume(diskType types.DiskType, dir string, dirIdx string, collection
ev.ecxCreatedAt = ecxFi.ModTime()
// open ecj file and seed the in-memory deleted set from it.
if ev.ecjFile, err = os.OpenFile(indexBaseFileName+".ecj", os.O_RDWR|os.O_CREATE, 0644); err != nil {
if ev.ecjFile, err = backend.OpenVolumeFile(indexBaseFileName+".ecj", os.O_RDWR|os.O_CREATE); err != nil {
return nil, fmt.Errorf("cannot open ec volume journal %s.ecj: %v", indexBaseFileName, err)
}
if ecjFi, statErr := ev.ecjFile.Stat(); statErr == nil {
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"sync"
"github.com/hashicorp/golang-lru/v2/simplelru"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
)
// Read-only volumes — cloud-tiered ones above all — outnumber writable ones by
@@ -78,7 +79,7 @@ func (p *indexFilePool) borrow(name string, writable bool) (*pooledFile, error)
}
// Opened outside the lock: a cold open blocks on disk, and holding a
// process-wide mutex across it would serialize every volume's lookups.
file, err := os.OpenFile(name, flag, 0644)
file, err := backend.OpenVolumeFile(name, flag)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -17,7 +17,7 @@ import (
// openIndex returns a file descriptor for the volume's index, and the index size in bytes.
func (v *Volume) openIndex() (*os.File, int64, error) {
idxFileName := v.FileName(".idx")
idxFile, err := os.OpenFile(idxFileName, os.O_RDONLY, 0644)
idxFile, err := backend.OpenVolumeFile(idxFileName, os.O_RDONLY)
if err != nil {
return nil, 0, fmt.Errorf("failed to open IDX file %s for volume %v: %v", idxFileName, v.Id, err)
}
+5 -5
View File
@@ -71,7 +71,7 @@ func (v *Volume) reopenIdxForWrite() error {
return nil
}
indexFile, err := os.OpenFile(v.FileName(".idx"), os.O_RDWR|os.O_CREATE, 0644)
indexFile, err := backend.OpenVolumeFile(v.FileName(".idx"), os.O_RDWR|os.O_CREATE)
if err != nil {
return fmt.Errorf("reopen %s read-write: %v", v.FileName(".idx"), err)
}
@@ -192,10 +192,10 @@ func (v *Volume) load(alsoLoadIndex bool, createDatIfMissing bool, needleMapKind
}
var dataFile *os.File
if canWrite {
dataFile, err = os.OpenFile(v.FileName(".dat"), os.O_RDWR|os.O_CREATE, 0644)
dataFile, err = backend.OpenVolumeFile(v.FileName(".dat"), os.O_RDWR|os.O_CREATE)
} else {
glog.V(0).Infof("opening %s in READONLY mode", v.FileName(".dat"))
dataFile, err = os.Open(v.FileName(".dat"))
dataFile, err = backend.OpenVolumeFile(v.FileName(".dat"), os.O_RDONLY)
v.noWriteOrDelete = true
}
v.lastModifiedTsSeconds = uint64(modifiedTime.Unix())
@@ -267,12 +267,12 @@ func (v *Volume) load(alsoLoadIndex bool, createDatIfMissing bool, needleMapKind
var indexFile *os.File
if v.noWriteOrDelete {
glog.V(0).Infoln("open to read file", v.FileName(".idx"))
if indexFile, err = os.OpenFile(v.FileName(".idx"), os.O_RDONLY, 0644); err != nil {
if indexFile, err = backend.OpenVolumeFile(v.FileName(".idx"), os.O_RDONLY); err != nil {
return fmt.Errorf("cannot read Volume Index %s: %v", v.FileName(".idx"), err)
}
} else {
glog.V(1).Infoln("open to write file", v.FileName(".idx"))
if indexFile, err = os.OpenFile(v.FileName(".idx"), os.O_RDWR|os.O_CREATE, 0644); err != nil {
if indexFile, err = backend.OpenVolumeFile(v.FileName(".idx"), os.O_RDWR|os.O_CREATE); err != nil {
return fmt.Errorf("cannot write Volume Index %s: %v", v.FileName(".idx"), err)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ func (v *Volume) maybeWriteSuperBlock(ver needle.Version) error {
if e != nil && os.IsPermission(e) {
//read-only, but zero length - recreate it!
var dataFile *os.File
if dataFile, e = os.Create(v.DataBackend.Name()); e == nil {
if dataFile, e = backend.OpenVolumeFile(v.DataBackend.Name(), os.O_RDWR|os.O_CREATE|os.O_TRUNC); e == nil {
v.DataBackend = backend.NewDiskFile(dataFile)
if _, e = v.DataBackend.WriteAt(v.SuperBlock.Bytes(), 0); e == nil {
v.noWriteLock.Lock()