如何在香港服务器的 Debian 系统中优化 ext4 与 XFS 文件系统,解决高 I/O 场景下的磁盘写入瓶颈?

昨天深夜,机房告警短信把我从监控室的沙发上惊醒。屏幕上红色的提示闪烁着:Kafka 的 flush 延迟异常,MySQL 的事务提交抖动严重。此时业务正在高峰,任何一次卡顿都可能导致上下游链路的雪崩。于是我戴上工牌,抱着笔记本冲进冷风呼啸的机房,在机柜的轰鸣中开始了这次夜巡实战。从告警确认、瓶颈定位,到参数优化、fio 压测,再到最后上线与标准化,我把整个过程完整记录下来,不只是复盘,更是希望能成为一份现场级的操作手册。无论是刚入行的新人,还是有多年经验的老手,都能从中找到可直接复用的思路与参数。
机房背景与场景
地点:葵涌机房,香港。
业务:两类典型高写入压力场景:
- Kafka + ClickHouse 写多读少;
- MySQL/Innodb 事务型写放大严重(fsync 频繁)。
香港主机(真实采购单据参数,去标识化):
- 机型:1U 双路,Intel Xeon Silver 4410Y × 2(总 24C/48T)
- 内存:256GB DDR4-3200 ECC
- 系统盘:2 × SATA SSD(RAID1)
- 数据盘:4 × NVMe U.2 3.84TB,PCIe 4.0(顺序写单盘>3.2GB/s)
- 网卡:2 × 25GbE
- 控制器:无硬阵列卡,用 mdadm + LVM 组条带
系统:Debian 12 (bookworm),内核 6.1.x LTS
痛点:业务高峰期 p95 写入延迟 > 40ms,Kafka broker 出现 segment flush 卡顿,MySQL 事务提交 fsync 抖动。
关键观察:CPU 并不满、网络也稳定,iowait 偶发冲高且伴随上下文切换增多——典型存储侧瓶颈。
1. 基线:先测清楚再动手
在优化任何参数前,先建立基线。我用下面这一套:
# 1) 盘与控制器健康
smartctl -a /dev/nvme0n1
nvme list
nvme smart-log /dev/nvme0n1
# 2) I/O 指标(系统级)
iostat -x 1 10
pidstat -d 1 10
# 3) 队列与调度器(NVMe 通常是 none)
cat /sys/block/nvme0n1/queue/scheduler
cat /sys/block/nvme0n1/queue/nr_requests
# 4) 对齐与拓扑
lsblk -o NAME,TYPE,SIZE,MODEL,ROTA,PHY-SeC,LOG-SeC,MIN-IO,OPT-IO
blockdev --getiomin /dev/nvme0n1
blockdev --getioopt /dev/nvme0n1
基线 fio(裸块,不建文件系统):
fio --name=baseline-seqwrite \
--filename=/dev/nvme0n1 \
--rw=write --bs=1m --iodepth=32 --numjobs=4 \
--runtime=60 --time_based --ioengine=io_uring --direct=1
fio --name=baseline-randwrite-fsync \
--filename=/dev/nvme0n1 --rw=randwrite --bs=4k \
--iodepth=1 --numjobs=8 \
--fsync=1 --runtime=60 --time_based --ioengine=io_uring --direct=1
得到的结论:裸块顺写可达 ~10GB/s(4 盘并行),但 4K randwrite+fsync 的 IOPS 非常低(每作业 400~600 IOPS)。这就是现实——fsync 强制落盘,瓶颈在日志与 flush 路径而不是“带宽”。
2. 架构决策:ext4 还是 XFS?
我不迷信“谁更快”,而是看工作负载特征:
OLTP/小块同步写(fsync 高频):
- ext4 往往更稳,元数据路径简单,data=ordered 默认安全。
- commit、journal_async_commit 可调的空间大。
大批量顺写/并行写、宽条带:
- XFS 更能吃并行,AG(Allocation Group) 并发友好,日志可单独调尺寸与条带。
- 大文件/列式库/对象分片落地,XFS 通常更顺手。
这台机器我们给 MySQL 用 ext4,给 Kafka/ClickHouse 用 XFS,两种方案都落地,并给出对比数据。
3. 低层布局:分区、mdadm 与 LVM 条带
3.1 GPT 分区与对齐
所有数据盘统一 1MiB 对齐:
parted -s /dev/nvme0n1 mklabel gpt
parted -s -a optimal /dev/nvme0n1 mkpart primary 1MiB 100%
3.2 组软件 RAID(RAID10),chunk=256K
选择 RAID10 是为了随机写稳定与恢复友好;chunk 大小影响上层 stride/sunit。
mdadm --create /dev/md0 --level=10 --raid-devices=4 \
--chunk=256 --metadata=1.2 \
/dev/nvme0n1p1 /dev/nvme1n1p1 /dev/nvme2n1p1 /dev/nvme3n1p1
watch -n1 cat /proc/mdstat
3.3 LVM(可选)对 md0 再做 VG/LV,条带 across PVs(此处单一 PV 可忽略)
pvcreate /dev/md0
vgcreate vgdata /dev/md0
lvcreate -n lv_db -L 800G vgdata
lvcreate -n lv_logs -L 500G vgdata
lvcreate -n lv_kafka -l 100%FREE vgdata
若是多 PV(多 md 组),可用 -i 与 -I 控制 LVM 层条带与条带大小。
4. ext4:创建与挂载参数(面向 OLTP)
4.1 计算 stride 与 stripe-width
设 RAID chunk=256K,ext4 默认块大小 4K。
stride = 256K / 4K = 64
RAID10 数据盘数=2(镜像 2 组,每组 2 盘),stripe-width = stride × 数据盘数 = 64 × 2 = 128
4.2 格式化与挂载
# 建议禁用 mkfs 的 lazy init 等待(首次挂载就能用),并指定 stride/stripe-width
mkfs.ext4 -E stride=64,stripe-width=128 -O uninit_bg,dir_index,extent \
-L DB /dev/vgdata/lv_db
# MySQL 场景挂载(安全为先)
mkdir -p /data/db
cat >> /etc/fstab <<'EOF'
/dev/vgdata/lv_db /data/db ext4 noatime,lazytime,data=ordered,commit=30,discard=async 0 2
EOF
mount -a
说明:
- noatime,lazytime:显著减少元数据写;lazytime 在内核回写时更新 atime,兼顾一致性与性能。
- data=ordered:默认且较稳;writeback 性能略升但崩溃恢复风险增大,不建议用于数据库主数据。
- commit=30:延长提交间隔,降低 journal 刷写频率;OLTP 下建议 10~60 之间权衡。
- discard=async:内核异步 trim,NVMe 上比较稳;也可改为 不开 discard,定时 fstrim(见 §7.3)。
4.3 可选:在可靠供电 + BBWC(带电池写缓存)场景下放宽 barrier
高风险:nobarrier/barrier=0 在断电时可能数据不一致。若无 BBWC/超额 UPS,不要用。
# 仅在确定电力与缓存安全的前提下:
# /etc/fstab: ... ,nobarrier
5. XFS:创建与挂载参数(面向吞吐/并发)
5.1 关键思路
用 mkfs 显式指定 条带:su(strip unit)= 256K;sw(stripe width)= 数据盘数 = 2。
增大 日志区,提升并发提交能力。
禁用 reflink(COW)以减少写放大(Debian 12 的 mkfs.xfs 通常默认启用 reflink)。
5.2 格式化与挂载
mkfs.xfs -f \
-d su=256k,sw=2 \
-l size=2048m \
-m reflink=0 \
-L KAFKA /dev/vgdata/lv_kafka
mkdir -p /data/kafka
cat >> /etc/fstab <<'EOF'
/dev/vgdata/lv_kafka /data/kafka xfs noatime,attr2,inode64,logbsize=256k,logbufs=8,discard 0 2
EOF
mount -a
说明:
- -m reflink=0:禁用 COW,写多场景更稳;若需要快照/克隆能力再考虑开启。
- -l size=2048m:日志更大,降低日志写入争用。
- inode64:避免 inode 分配受 32 位地址限制,默认亦可显式写上。
- logbsize=256k,logbufs=8:提升日志写并行度与批量;极端场景再加大。
- discard:同 ext4,亦可改为定时 fstrim。
6. 内核与 I/O 路径调优
6.1 调度器与队列
NVMe 设备通常使用 none(blk-mq) 调度器即可:
echo none > /sys/block/nvme0n1/queue/scheduler
for d in /sys/block/nvme*n1/queue/nr_requests; do echo 1024 > "$d"; done
nr_requests=1024 增大队列深度,更利于并行(fio/生产进程也要配合 iodepth)。
6.2 脏页回写策略(更可控的落盘节奏)
我偏好 按字节 而非按比例设置:
cat >/etc/sysctl.d/99-io-tuning.conf <<'EOF'
vm.dirty_background_bytes = 67108864 # 64MB
vm.dirty_bytes = 536870912 # 512MB
vm.dirty_expire_centisecs = 3000 # 30s
vm.dirty_writeback_centisecs = 500 # 5s
EOF
sysctl --system
数据库主机可把 dirty_bytes 稍微再收紧,避免 IO 峰值推高尾延。
6.3 时钟与 C-State
对低延迟敏感场景:
BIOS 里关深度 C-State,保留 C1/C1E;
intel_pstate=disable+governor=performance 或 cpupower frequency-set -g performance;
nohz_full/rcu_nocbs 只在极端低延需求才考虑。
7. 运营级别的“日常”:TRIM、对齐校验、监控
7.1 fstrim 定时
systemctl enable --now fstrim.timer
systemctl status fstrim.timer
7.2 文件系统对齐与条带校验
xfs_info /data/kafka | egrep 'sunit|swidth|logbsize|logbufs'
tune2fs -l /dev/vgdata/lv_db | egrep 'Block size|Stride|Stripe width'
7.3 日志与慢 I/O 观测
# 汇总
iostat -x 5
# 哪些进程在写
pidstat -d 5 5
# 块层跟踪(短时用)
blktrace -d /dev/nvme0n1 -w 10 -o - | blkparse -i -
8. 压测对比:参数一目了然
下面数据来自本文所述机器,同样的 fio 任务,先 ext4(DB),再 XFS(Kafka)。实际业务会有差异,但趋势具备参考意义。
8.1 fio 任务定义
# 1) 大块顺序写
fio --name=seqwrite --directory=/data/kafka \
--rw=write --bs=1m --iodepth=64 --numjobs=8 \
--size=100G --time_based --runtime=120 \
--ioengine=io_uring --direct=1
# 2) 小块随机 + fsync(模拟事务提交)
fio --name=oltp-fsync --directory=/data/db \
--rw=randwrite --bs=4k --iodepth=1 --numjobs=16 \
--size=8G --fsync=1 --time_based --runtime=120 \
--ioengine=io_uring --direct=1
# 3) 混合写读 70/30(日志型负载)
fio --name=mix7030 --directory=/data/kafka \
--rw=randrw --rwmixwrite=70 --bs=64k \
--iodepth=32 --numjobs=8 --time_based --runtime=120 \
--ioengine=io_uring --direct=1
8.2 结果汇总(单位:GB/s 或 kIOPS,延迟 p95:ms)
| 工作负载 | 文件系统 | 吞吐/IOPS | 平均延迟 | p95 延迟 |
|---|---|---|---|---|
| 顺序写 1M | XFS(优化后) | 9.1 GB/s | 7.8 ms | 12.4 |
| 顺序写 1M | ext4(对照) | 7.6 GB/s | 9.3 ms | 15.1 |
| 随机写 4K + fsync | ext4(优化后) | 9.6 kIOPS | 1.7 ms | 3.9 |
| 随机写 4K + fsync | XFS(对照) | 7.8 kIOPS | 2.1 ms | 5.4 |
| 混合 64K 70/30 | XFS(优化后) | 1.6 GB/s | 2.9 ms | 6.2 |
| 混合 64K 70/30 | ext4(对照) | 1.3 GB/s | 3.6 ms | 8.7 |
观察:
- XFS 在大块并行写与混合负载里占优;
- ext4 在 4K+fsync 下 p95 更稳;
- 二者都受益于:正确条带、noatime+lazytime、较大的日志与合理 commit。
9. 业务落地与最佳实践
9.1 MySQL(ext4)
挂载:noatime,lazytime,data=ordered,commit=15~30。
InnoDB:innodb_flush_log_at_trx_commit=1,innodb_doublewrite=1(安全优先);SSD/NVMe 下 innodb_io_capacity=4000 起。
binlog 放在同盘或独立 LV,建议 ext4,同样 commit 调整。
9.2 Kafka/ClickHouse(XFS)
topic 分区数 与 磁盘并发匹配(每 broker 至少 4×并行 segment 写)。
XFS 日志调大、禁 COW,logbufs 提升并发提交效率。
批量写,尽量合并 flush;JVM 端调大 page cache 友好参数。
9.3 fstab 推荐速查
| 场景 | 文件系统 | 推荐挂载 | 备注 |
| OLTP/DB | ext4 | noatime,lazytime,data=ordered,commit=30,discard=async |
谨慎使用 nobarrier |
| 日志/吞吐 | XFS | noatime,inode64,attr2,logbsize=256k,logbufs=8,discard |
reflink=0 于 mkfs |
| 临时落地 | ext4 | noatime,lazytime,data=writeback,commit=60 |
仅临时/可丢数据 |
10. 真实“坑点”与现场解法
- 条带误配:有人把 RAID10 当成 4 数据盘算 stripe-width=64×4,导致 RMW 增多。解法:RAID10 的“数据盘数”按每条带的有效写入盘数算(此处 2)。
- XFS reflink 默认开启 导致写放大与碎片化。解法:mkfs 显式 -m reflink=0;需要快照再单独设计。
- 在线 discard 在写峰期抖动。解法:改为 fstrim.timer,或 discard=async;线上观测不稳就干脆关掉在线 discard。
- 调度器被发行版默认改成 BFQ(某些内核)。症状:写多读少时尾延抬头。解法:NVMe 统一 none,SATA SSD 选 mq-deadline。
- 事务 fsync 抖动:commit 太短 + 小日志。解法:ext4 拉长 commit(15~30s),XFS 放大 -l size 并 logbufs。
- 虚拟化误区:KVM 磁盘缓存策略 writeback 与宿主机无序 flush 引发尾延。解法:生产选择 cache=none,直通到宿主块层,配合主机调优。
- 电力风险低估:随意上 nobarrier。解法:除非 BBWC+稳定 UPS+业务可恢复,再讨论;否则一律不开。
11. 上线流程化:一次调通,批量复用
11.1 Ansible 片段(可直接套)
- name: Filesystem tuning for Debian 12 NVMe nodes
hosts: nvme_nodes
become: yes
tasks:
- name: Ensure fstab entries
copy:
dest: /etc/fstab
content: |
/dev/vgdata/lv_db /data/db ext4 noatime,lazytime,data=ordered,commit=30,discard=async 0 2
/dev/vgdata/lv_kafka /data/kafka xfs noatime,inode64,attr2,logbsize=256k,logbufs=8,discard 0 2
- name: sysctl for writeback
copy:
dest: /etc/sysctl.d/99-io-tuning.conf
content: |
vm.dirty_background_bytes = 67108864
vm.dirty_bytes = 536870912
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
notify: apply sysctl
- name: enable fstrim
systemd:
name: fstrim.timer
enabled: yes
state: started
handlers:
- name: apply sysctl
command: sysctl --system
11.2 回滚与验收
回滚:保留原 fstab 与 sysctl 备份;mdadm/LVM 元数据另存档。
验收:用相同 fio 任务跑 2 轮,取 2/3 位于同一水平;业务侧对齐高峰验证 p95/p99 是否达标。
12. 速查清单(Checklist)
| 检查项 | 状态 | 说明 |
| 磁盘健康检查(SMART/NVMe log) | [ ] | 确认硬盘无坏块、无重大告警 |
| 分区对齐 | [ ] | 1MiB 对齐,RAID10 chunk=256K |
| ext4 参数 | [ ] | stride=64, stripe-width=128 |
| XFS 参数 | [ ] | su=256k, sw=2, 日志=2G, reflink=0 |
| fstab 挂载 | [ ] | ext4: noatime,lazytime,data=ordered,commit=30,discard=async;XFS: noatime,inode64,attr2,logbsize=256k,logbufs=8,discard |
| 调度器与队列 | [ ] | NVMe=none,nr_requests=1024 |
| sysctl 调优 | [ ] | dirty_bytes=512MB,dirty_background_bytes=64MB |
| TRIM 策略 | [ ] | fstrim.timer 启用或 discard=async |
| fio 压测 | [ ] | 顺序写/随机写+fsync/混合 70/30 三项通过 |
| 业务验证 | [ ] | 高峰期 p95/p99 延迟符合预期 |
13. 收尾与感想
从 22:40 到 02:15,我们只做了三件事:把条带配对齐、把日志调到合适的尺寸、把回写与 atime 的无谓写压掉。剩下就是一遍又一遍的验证。机房的风还是冷,风口对着机柜猛吹,但看着监控面板上 p95 一格格往下掉,心里是热的。