上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-16 10:30 阅读量:831


昨天深夜,机房告警短信把我从监控室的沙发上惊醒。屏幕上红色的提示闪烁着: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. 真实“坑点”与现场解法

  1. 条带误配:有人把 RAID10 当成 4 数据盘算 stripe-width=64×4,导致 RMW 增多。解法:RAID10 的“数据盘数”按每条带的有效写入盘数算(此处 2)。
  2. XFS reflink 默认开启 导致写放大与碎片化。解法:mkfs 显式 -m reflink=0;需要快照再单独设计。
  3. 在线 discard 在写峰期抖动。解法:改为 fstrim.timer,或 discard=async;线上观测不稳就干脆关掉在线 discard。
  4. 调度器被发行版默认改成 BFQ(某些内核)。症状:写多读少时尾延抬头。解法:NVMe 统一 none,SATA SSD 选 mq-deadline。
  5. 事务 fsync 抖动:commit 太短 + 小日志。解法:ext4 拉长 commit(15~30s),XFS 放大 -l size 并 logbufs。
  6. 虚拟化误区:KVM 磁盘缓存策略 writeback 与宿主机无序 flush 引发尾延。解法:生产选择 cache=none,直通到宿主块层,配合主机调优。
  7. 电力风险低估:随意上 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 一格格往下掉,心里是热的。

目录结构
全文