Learn to optimize Linux filesystem performance by configuring I/O schedulers, tuning ext4 mount options, and monitoring disk performance with iostat and iotop for high-throughput workloads.
Prerequisites
- Root or sudo access
- Basic understanding of Linux filesystems
- Storage devices to optimize
What this solves
Filesystem performance is critical for applications with heavy disk I/O requirements like databases, web servers, and analytics workloads. Default filesystem settings often prioritize data safety over performance, leaving significant optimization opportunities on the table. This tutorial shows you how to optimize I/O schedulers, configure filesystem-specific mount options, and monitor performance metrics to achieve better throughput and lower latency.
Step-by-step configuration
Install required monitoring tools
Install iostat, iotop, and other performance monitoring utilities to baseline and measure filesystem performance improvements.
sudo apt update
sudo apt install -y sysstat iotop hdparm fio
Check current I/O scheduler
View the current I/O scheduler for each block device to understand your baseline configuration.
cat /sys/block/sda/queue/scheduler
ls /sys/block/ | grep -v loop | xargs -I {} sh -c 'echo "Device {}: $(cat /sys/block/{}/queue/scheduler)"'
Configure optimal I/O scheduler
Set the I/O scheduler based on your storage type and workload. Use mq-deadline for SSDs with general workloads, kyber for high IOPS requirements, or none for NVMe devices with very fast storage.
# For SSDs and general workloads (recommended default)
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
For high-IOPS workloads requiring low latency
echo kyber | sudo tee /sys/block/sda/queue/scheduler
For NVMe with very fast storage (bypasses scheduler)
echo none | sudo tee /sys/block/nvme0n1/queue/scheduler
Make I/O scheduler changes persistent
Configure udev rules to automatically apply the optimal I/O scheduler on boot based on device type.
# Set mq-deadline for SATA SSDs
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
Set kyber for high-performance SSDs (adjust by model if needed)
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{size}==">1000000000", ATTR{queue/scheduler}="kyber"
Set none for NVMe devices
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
Optimize ext4 mount options
Configure performance-oriented mount options for ext4 filesystems. The noatime option eliminates access time updates, and data=writeback improves write performance by not forcing data writes before metadata.
# Backup current fstab
sudo cp /etc/fstab /etc/fstab.backup
Check current mount options
mount | grep ext4
# Example optimized ext4 mount options
Replace /dev/sda1 and / with your actual device and mount point
/dev/sda1 / ext4 defaults,noatime,data=writeback,barrier=0,commit=60 0 1
For data partitions where performance is critical
/dev/sda2 /var/lib/mysql ext4 defaults,noatime,data=writeback,barrier=0,commit=60,nodelalloc 0 2
Configure XFS performance options
For XFS filesystems, optimize allocation and logging options for better performance with large files and high concurrency.
# Optimized XFS mount options
/dev/sdb1 /data xfs defaults,noatime,largeio,inode64,swalloc,logbsize=256k 0 2
Apply new mount options
Remount filesystems with new options or reboot to apply all changes. Test one filesystem at a time in production environments.
# Test fstab syntax
sudo findmnt --verify
Remount specific filesystem (if possible)
sudo mount -o remount /
Or reboot to apply all changes
sudo reboot
Tune filesystem readahead
Increase readahead values for sequential workloads like large file processing or streaming. This prefetches more data, reducing seeks for sequential reads.
# Check current readahead
sudo blockdev --getra /dev/sda
Increase readahead for sequential workloads (adjust based on workload)
sudo blockdev --setra 4096 /dev/sda
Make persistent by adding to rc.local or systemd service
echo 'blockdev --setra 4096 /dev/sda' | sudo tee -a /etc/rc.local
Configure Btrfs optimization
For Btrfs filesystems, disable copy-on-write for database files and configure optimal mount options for performance-critical applications.
# Optimized Btrfs mount options
/dev/sdc1 /data btrfs defaults,noatime,compress=lzo,space_cache=v2,commit=60 0 0
# Disable COW for database directories
sudo chattr +C /var/lib/mysql
sudo chattr +C /var/lib/postgresql
Optimize kernel parameters
Tune kernel parameters that affect filesystem and I/O performance. These settings optimize page cache behavior and I/O scheduling.
# Reduce swappiness to keep filesystem cache in memory
vm.swappiness = 1
Allow more dirty pages in memory before forcing writes
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
Reduce dirty page cache timeout
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 500
Increase inode and dentry cache
fs.file-max = 2097152
Optimize for high-throughput workloads
kernel.sched_min_granularity_ns = 10000000
kernel.sched_wakeup_granularity_ns = 15000000
Apply kernel parameter changes
Load the new kernel parameters and verify they are applied correctly.
sudo sysctl --system
sudo sysctl vm.dirty_ratio vm.swappiness
Monitor filesystem performance
Use iostat for I/O monitoring
Monitor disk I/O statistics to identify bottlenecks and measure performance improvements. The key metrics are IOPS, throughput, and wait times.
# Monitor I/O every 2 seconds, 5 iterations
iostat -x 2 5
Monitor specific device
iostat -x /dev/sda 2 5
Extended statistics with detailed metrics
iostat -dx 1
Use iotop for process-level I/O monitoring
Identify which processes are generating the most I/O to optimize application-specific performance.
# Monitor I/O by process (requires root)
sudo iotop -o
Show accumulated I/O instead of bandwidth
sudo iotop -a
Monitor specific processes
sudo iotop -p $(pidof mysql)
Benchmark filesystem performance
Use fio to benchmark your filesystem performance before and after optimizations to measure improvements.
# Random read/write benchmark
sudo fio --name=randrw --ioengine=libaio --iodepth=16 --rw=randrw --bs=4k --direct=1 --size=1G --numjobs=4 --runtime=60 --group_reporting --filename=/tmp/fio-test
Sequential read benchmark
sudo fio --name=seqread --ioengine=libaio --iodepth=32 --rw=read --bs=1M --direct=1 --size=4G --numjobs=1 --runtime=60 --group_reporting --filename=/tmp/fio-seq-test
Clean up test files
sudo rm -f /tmp/fio-*-test
Verify your setup
# Verify I/O scheduler
cat /sys/block/sda/queue/scheduler
Check mount options
mount | grep -E 'ext4|xfs|btrfs'
Verify kernel parameters
sysctl vm.dirty_ratio vm.swappiness vm.dirty_background_ratio
Check filesystem performance
iostat -x 1 3
Test readahead setting
sudo blockdev --getra /dev/sda
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| High iowait times | Wrong I/O scheduler or mount options | Switch to mq-deadline scheduler and add noatime mount option |
| Filesystem corruption after optimization | Aggressive options like barrier=0 | Remove barrier=0, use data=ordered instead of writeback |
| Poor database performance | Default mount options | Use noatime, increase commit intervals, disable COW for Btrfs |
| System becomes unresponsive under I/O load | vm.dirty_ratio too high | Reduce vm.dirty_ratio to 10-15 and vm.dirty_background_ratio to 5 |
| Mount fails after fstab changes | Invalid mount options | Boot from rescue mode, restore /etc/fstab.backup |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Usage
usage() {
echo "Usage: $0 [--device DEVICE] [--scheduler SCHEDULER] [--mount-point PATH] [--fs-type TYPE]"
echo ""
echo "Options:"
echo " --device Block device to optimize (default: auto-detect)"
echo " --scheduler I/O scheduler (auto|mq-deadline|kyber|none, default: auto)"
echo " --mount-point Mount point to optimize (default: /)"
echo " --fs-type Filesystem type (auto|ext4|xfs, default: auto)"
echo ""
echo "Example: $0 --device /dev/sda --scheduler mq-deadline"
exit 1
}
# Cleanup function
cleanup() {
log_error "Script failed. Check logs above for details."
if [[ -f /etc/fstab.backup ]]; then
log_warn "Original fstab backed up to /etc/fstab.backup"
fi
exit 1
}
trap cleanup ERR
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Parse arguments
DEVICE=""
SCHEDULER="auto"
MOUNT_POINT="/"
FS_TYPE="auto"
while [[ $# -gt 0 ]]; do
case $1 in
--device)
DEVICE="$2"
shift 2
;;
--scheduler)
SCHEDULER="$2"
shift 2
;;
--mount-point)
MOUNT_POINT="$2"
shift 2
;;
--fs-type)
FS_TYPE="$2"
shift 2
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
# Detect distribution
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "Detected distribution: $ID"
# Auto-detect device if not specified
if [[ -z "$DEVICE" ]]; then
DEVICE=$(findmnt -n -o SOURCE "$MOUNT_POINT" | sed 's/[0-9]*$//')
if [[ -z "$DEVICE" ]]; then
log_error "Could not auto-detect device for mount point $MOUNT_POINT"
exit 1
fi
fi
# Auto-detect filesystem type if not specified
if [[ "$FS_TYPE" == "auto" ]]; then
FS_TYPE=$(findmnt -n -o FSTYPE "$MOUNT_POINT")
if [[ -z "$FS_TYPE" ]]; then
log_error "Could not auto-detect filesystem type for $MOUNT_POINT"
exit 1
fi
fi
log_info "Target device: $DEVICE, Filesystem: $FS_TYPE, Mount point: $MOUNT_POINT"
echo "[1/8] Installing performance monitoring tools..."
$PKG_UPDATE
$PKG_INSTALL sysstat iotop hdparm fio
log_info "Performance tools installed"
echo "[2/8] Checking current I/O scheduler configuration..."
BLOCK_DEV=$(basename "$DEVICE")
if [[ -f "/sys/block/$BLOCK_DEV/queue/scheduler" ]]; then
CURRENT_SCHEDULER=$(cat "/sys/block/$BLOCK_DEV/queue/scheduler" | grep -o '\[.*\]' | tr -d '[]')
log_info "Current scheduler for $BLOCK_DEV: $CURRENT_SCHEDULER"
else
log_warn "Cannot access scheduler for $BLOCK_DEV"
fi
echo "[3/8] Determining optimal I/O scheduler..."
if [[ "$SCHEDULER" == "auto" ]]; then
if [[ "$BLOCK_DEV" =~ ^nvme ]]; then
SCHEDULER="none"
log_info "Detected NVMe device, using 'none' scheduler"
elif [[ -f "/sys/block/$BLOCK_DEV/queue/rotational" ]] && [[ $(cat "/sys/block/$BLOCK_DEV/queue/rotational") == "0" ]]; then
SCHEDULER="mq-deadline"
log_info "Detected SSD, using 'mq-deadline' scheduler"
else
SCHEDULER="mq-deadline"
log_info "Using default 'mq-deadline' scheduler"
fi
fi
echo "[4/8] Configuring I/O scheduler..."
if [[ -f "/sys/block/$BLOCK_DEV/queue/scheduler" ]]; then
echo "$SCHEDULER" > "/sys/block/$BLOCK_DEV/queue/scheduler"
log_info "Set scheduler to $SCHEDULER for $BLOCK_DEV"
else
log_warn "Cannot set scheduler for $BLOCK_DEV"
fi
echo "[5/8] Making I/O scheduler changes persistent..."
UDEV_RULE_FILE="/etc/udev/rules.d/60-io-scheduler.rules"
cat > "$UDEV_RULE_FILE" << 'EOF'
# Optimize I/O schedulers for different device types
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
EOF
chmod 644 "$UDEV_RULE_FILE"
log_info "Created udev rules for persistent scheduler configuration"
echo "[6/8] Backing up fstab and preparing mount options..."
cp /etc/fstab /etc/fstab.backup
log_info "Backed up /etc/fstab to /etc/fstab.backup"
# Get current mount entry
CURRENT_ENTRY=$(grep " $MOUNT_POINT " /etc/fstab | head -n1)
if [[ -z "$CURRENT_ENTRY" ]]; then
log_error "Could not find mount entry for $MOUNT_POINT in /etc/fstab"
exit 1
fi
echo "[7/8] Optimizing filesystem mount options..."
case "$FS_TYPE" in
ext4)
log_warn "Using performance-oriented ext4 options. These reduce data safety!"
NEW_OPTIONS="defaults,noatime,data=writeback,barrier=0,commit=60"
;;
xfs)
NEW_OPTIONS="defaults,noatime,largeio,inode64,swalloc,logbsize=256k"
;;
*)
NEW_OPTIONS="defaults,noatime"
log_warn "Using basic optimization for filesystem type: $FS_TYPE"
;;
esac
# Update fstab with new options
DEVICE_PATH=$(echo "$CURRENT_ENTRY" | awk '{print $1}')
DUMP=$(echo "$CURRENT_ENTRY" | awk '{print $5}')
PASS=$(echo "$CURRENT_ENTRY" | awk '{print $6}')
# Remove old entry and add new one
sed -i "\|$MOUNT_POINT|d" /etc/fstab
echo "$DEVICE_PATH $MOUNT_POINT $FS_TYPE $NEW_OPTIONS $DUMP $PASS" >> /etc/fstab
log_info "Updated fstab with optimized mount options: $NEW_OPTIONS"
# Validate fstab syntax
if command -v findmnt >/dev/null; then
findmnt --verify
log_info "fstab syntax validated successfully"
fi
echo "[8/8] Configuring readahead optimization..."
CURRENT_RA=$(blockdev --getra "$DEVICE" 2>/dev/null || echo "unknown")
log_info "Current readahead for $DEVICE: $CURRENT_RA"
# Set readahead to 2MB (4096 sectors) for better sequential performance
blockdev --setra 4096 "$DEVICE"
log_info "Set readahead to 4096 sectors (2MB) for $DEVICE"
# Make readahead persistent via udev
cat >> "$UDEV_RULE_FILE" << EOF
# Optimize readahead for performance
ACTION=="add|change", KERNEL=="$BLOCK_DEV", ATTR{queue/read_ahead_kb}="2048"
EOF
log_info "Made readahead optimization persistent"
# Reload udev rules
udevadm control --reload-rules
udevadm trigger
echo ""
log_info "Filesystem performance optimization completed!"
echo ""
echo "Summary of changes:"
echo "- I/O scheduler: $SCHEDULER"
echo "- Mount options: $NEW_OPTIONS"
echo "- Readahead: 2MB"
echo ""
log_warn "A reboot is recommended to apply all optimizations"
echo ""
echo "To verify changes after reboot:"
echo " cat /sys/block/$BLOCK_DEV/queue/scheduler"
echo " mount | grep $MOUNT_POINT"
echo " blockdev --getra $DEVICE"
echo ""
echo "To monitor performance:"
echo " iostat -x 1"
echo " iotop"
Review the script before running. Execute with: bash install.sh