开始学习

Stage 02 · 进阶 · 第 7 / 7

I/O 调优:调度器、队列与内核参数

I/O 调度器、队列深度、预读、脏页参数与文件系统挂载选项的调优。

40 分钟|全程第 17 / 38

网上搜"Linux 磁盘优化",你会看到一大堆"十个让 I/O 性能翻倍的参数":调度器改成 noop、read_ahead_kb 调到 16 MB、vm.dirty_ratio 改成 80、swappiness 改成 0……照着改完,机器好像快了,又好像没快,出了问题谁也说不清是哪一条惹的祸。这正是性能分析方法论里的随机调参反方法。

先泼一盆冷水:《Systems Performance》第 9 章 9.9 节说得很清楚,这些参数的默认值通常是合理的,很少需要大改。调参的收益往往比不上消除不必要的工作,也比不上换一种架构。但默认值是按"一般情况"选的,在你的负载下未必最优——大内存机器的脏页阈值、SATA 盘上的混合读写、跨 NUMA 访问 NVMe,这些都是真实存在、调了就能测出差别的地方。这一课讲:调之前测什么;如何选择 I/O 调度器;nr_requestsread_ahead_kbmax_sectors_kbrq_affinitynomerges 这几个队列参数各管什么;vm.dirty_*swappiness;挂载选项和 XFS/ext4 的格式化参数;NVMe 中断与 NUMA 亲和;最后用 udev 规则和 tuned 把配置持久化,并且给每一项都配上验证方法。本课主要取材于《Systems Performance》第 8 章和第 9 章。

调优之前:先测量

调优是一个实验,不是一个动作。每改一项都走同一个闭环:

text
基线测量 ──▶ 提出假设 ──▶ 只改一个参数 ──▶ 同样负载重测 ──▶ 对比 ──▶ 保留或回滚
(数据)     (为什么会变好)  (记录旧值)     (重复 3 次)    (均值±标准差)  (写进变更记录)

"提出假设"这一步最常被跳过。"把 read_ahead_kb 调大"不是假设;"这个负载是单线程的大文件顺序读,设备延迟 1 ms,128 KiB 的预读窗口让吞吐被限制在约 128 MB/s,调到 4 MiB 后应该能接近设备带宽上限"才是假设——它告诉你该用什么负载验证、预期看到什么。

动手前先给当前配置拍个快照。grep . 文件... 会以"文件名:内容"的格式打印,非常适合批量查看 sysfs:

io-snapshot.sh
#!/usr/bin/env bash
# 保存当前的 I/O 相关配置,调优前后各跑一次,用 diff 对比
out=io-snapshot-$(hostname)-$(date +%F-%H%M).txt
{
  uname -r
  for q in /sys/block/{sd*,nvme*n*,vd*}/queue; do
    [ -d "$q" ] || continue
    grep -H . "$q"/{scheduler,nr_requests,read_ahead_kb,max_sectors_kb,max_hw_sectors_kb,rq_affinity,nomerges,rotational,wbt_lat_usec} 2>/dev/null
  done
  sysctl vm.dirty_ratio vm.dirty_background_ratio vm.dirty_bytes vm.dirty_background_bytes \
         vm.dirty_expire_centisecs vm.dirty_writeback_centisecs vm.swappiness vm.vfs_cache_pressure
  findmnt -rno TARGET,SOURCE,FSTYPE,OPTIONS -t xfs,ext4
  command -v tuned-adm >/dev/null && tuned-adm active
} > "$out"
echo "saved to $out"

I/O 调度器

四个选项

截至本文写作时,Ubuntu 24.04 的 6.8 内核提供四个多队列(blk-mq)调度器:

调度器原理适合代价
none不排序、不调度,请求直接下发到硬件队列NVMe、高速 SSD、虚拟机里的虚拟盘、下层还有调度的设备没有公平性和读写优先级
mq-deadline按扇区排序合并,读写各有截止时间,读优先,防止饿死SATA/SAS 的 HDD 和 SSD、RAID 卷一个全局锁,极高 IOPS 下 CPU 开销上升
bfq按进程/cgroup 分配"预算",追求公平与交互响应桌面、需要按 cgroup 按比例分配带宽的场景CPU 开销最大,不适合高 IOPS 服务器
kyber按读写延迟目标动态限制派发深度快速设备上需要"读不被写拖垮"的场景参数少、用得少

老教程里的 noopdeadlinecfq 是单队列时代的调度器,5.0 内核之后已经被移除,看到它们说明文档过时了。

查看和临时修改:

console
$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline
$ cat /sys/block/sda/queue/scheduler
none [mq-deadline]
$ echo bfq | sudo tee /sys/block/sda/queue/scheduler
bfq
$ cat /sys/block/sda/queue/scheduler
none mq-deadline [bfq]

方括号里是当前生效的调度器。列表里没有 bfqkyber 时,它们可能是没加载的内核模块,sudo modprobe bfqsudo modprobe kyber-iosched 之后再看。Ubuntu 的默认规则是:NVMe 用 none,SATA/SCSI 设备用 mq-deadline

怎么选

我的建议很简单:

  • NVMe 保持 none 设备自己有几十上百个并行单元和多个硬件队列,内核再排序只会增加 CPU 开销和锁争用;
  • HDD 和 SATA 设备用 mq-deadline 它能把随机请求按扇区排序、合并相邻请求,并且保证读请求在 500 ms 内一定被派发,避免被大量写淹没;
  • 虚拟机里的 vda/sda 通常用 none 真正的调度发生在宿主机或存储后端,客户机里再调度一次意义不大;
  • 只有在确实需要按进程或 cgroup 做公平分配时才考虑 bfq,并且要实测它的 CPU 开销。

mq-deadline 的参数在 /sys/block/<dev>/queue/iosched/ 下:read_expire(默认 500 ms)、write_expire(默认 5000 ms)、fifo_batch(默认 16)、writes_starved(默认 2,读可以连续饿死写几轮)。绝大多数情况不需要动。

验证方法:调度器影响的主要是混合负载下的公平性和尾延迟,而不是单一负载的峰值。所以验证要用混合负载——比如在一块 HDD 上,一个 fio job 做大块顺序写、另一个做 4 KiB 随机读,比较不同调度器下随机读的 p99:

bash
sudo fio --filename=/dev/sdb --direct=1 --ioengine=libaio --runtime=60 --time_based \
  --name=writer --rw=write --bs=1m --iodepth=16 \
  --name=reader --rw=randread --bs=4k --iodepth=1 --percentile_list=50:99

同时用 sudo biolatency-bpfcc -Q -D 10 1 看包含队列时间的延迟分布。只看 writer 的吞吐会得出错误的结论——none 下写吞吐可能更高,恰恰是因为读被饿着了。

队列参数

每个块设备在 /sys/block/<dev>/queue/ 下有一组参数,先整体看一眼:

console
$ grep -H . /sys/block/nvme0n1/queue/{nr_requests,read_ahead_kb,max_sectors_kb,max_hw_sectors_kb,rq_affinity,nomerges,wbt_lat_usec}
/sys/block/nvme0n1/queue/nr_requests:1023
/sys/block/nvme0n1/queue/read_ahead_kb:128
/sys/block/nvme0n1/queue/max_sectors_kb:1280
/sys/block/nvme0n1/queue/max_hw_sectors_kb:2048
/sys/block/nvme0n1/queue/rq_affinity:1
/sys/block/nvme0n1/queue/nomerges:0
/sys/block/nvme0n1/queue/wbt_lat_usec:2000

(具体数值取决于设备、驱动和内核版本。)

参数含义什么时候调怎么验证
nr_requests每个硬件队列可分配的请求数(软件队列深度)使用调度器时默认较小(如 SATA + mq-deadline 常见 64);应用并发很高、blktrace 频繁出现 S(睡眠等待 request)时可适当调大blktraceS 事件是否减少;biolatency -Q 与不带 -Q 的差距
read_ahead_kb顺序读时预读的最大量,默认 128 KiB大文件顺序读、高延迟设备(云盘、网络块设备、HDD 阵列)可调大到 1~4 MiB;随机读为主、内存紧张时调大反而会污染缓存清缓存后 buffered 顺序读的吞吐;iostatrareq-sz
max_sectors_kb单个请求的最大尺寸,不能超过 max_hw_sectors_kbRAID 条带很宽时可以调大到接近条带宽度;对延迟敏感、需要把大 I/O 拆小时调小iostatareq-szbitesize-bpfcc 的分布
rq_affinity完成处理在哪个 CPU 上执行:1 同组 CPU(默认)、2 强制回到提交请求的那个 CPU、0 不做迁移中断集中在少数核、完成处理成为瓶颈时试 2mpstat -P ALL 1 看软中断分布;fio 的 IOPS 与 cpu
nomerges0 允许所有合并(默认)、1 只做简单的一次性合并、2 完全禁止合并NVMe 上纯随机小 I/O 负载可以设 2,省去合并查找的 CPU;有顺序成分的负载不要动fio 的 IOPS 与 cpu 行;iostatrrqm/s wrqm/s
wbt_lat_usec回写节流(Writeback Throttling)的目标延迟,SSD 默认 2 ms,HDD 75 ms;0 关闭后台回写拖慢前台读时,它已经在帮你;除非测出它限制了纯写吞吐,否则别关回写期间前台读的 p99

一个实际的验证例子——在云盘上验证预读假设:

bash
for ra in 128 1024 4096; do
  echo $ra | sudo tee /sys/block/vdb/queue/read_ahead_kb >/dev/null
  sync; echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null
  sudo fio --name=ra-$ra --filename=/mnt/test/bigfile --rw=read --bs=64k \
    --direct=0 --ioengine=psync --size=8g | grep -E 'READ:'
done

注意这里必须用 direct=0:预读是页缓存的行为,O_DIRECT 绕过了它,用 direct I/O 测预读永远测不出差别。分布式文件系统(NFS、CephFS)的预读不走块设备,而是通过 /sys/class/bdi/<设备号>/read_ahead_kb 或挂载选项(如 CephFS 的 rasize)控制。

脏页与回写:vm.dirty_*

页缓存一课讲过:写入先进页缓存成为脏页,由后台回写线程异步落盘。控制这个过程的是四个参数:

参数默认值含义
vm.dirty_background_ratio10脏页超过可用内存的这个百分比,后台回写线程开始工作
vm.dirty_ratio20脏页超过这个百分比,写入进程自己被限流(阻塞在 balance_dirty_pages
vm.dirty_expire_centisecs3000脏页存在超过 30 秒就必须回写
vm.dirty_writeback_centisecs500回写线程每 5 秒醒来一次

*_ratio 各有一个对应的 *_bytes 版本(vm.dirty_bytesvm.dirty_background_bytes),用绝对字节数设定阈值。两者互斥:设置了 _bytes,对应的 _ratio 就会变成 0,反之亦然。

问题出在大内存机器上。一台 512 GB 内存的存储节点,dirty_ratio=20 意味着可以积累将近 100 GB 的脏页。设备每秒写 1 GB 的话,一次刷完要 100 秒:这期间任何 fsync 都要排在这 100 GB 后面,数据库提交从毫秒级跳到几十秒。这就是用 BPF 看清 I/O里后台回写和数据库提交抢盘的放大版。

大内存机器上我的建议是改用字节数,让回写早开始、小批量、持续进行

/etc/sysctl.d/90-storage.conf
# 大内存存储节点:脏页阈值用绝对值,避免一次积累几十 GB
vm.dirty_background_bytes = 536870912    # 512 MiB 开始后台回写
vm.dirty_bytes = 4294967296              # 4 GiB 开始对写入进程限流
vm.swappiness = 10
bash
sudo sysctl --system            # 加载 /etc/sysctl.d/ 下所有文件
sysctl vm.dirty_bytes vm.dirty_ratio

具体数值要按设备的写带宽定:一个经验值是让 dirty_bytes 大约对应设备几秒钟的写入量。

验证方法:一边用 dd if=/dev/zero of=/mnt/test/big bs=1M count=20000 持续写,一边运行:

bash
watch -n1 "grep -E '^(Dirty|Writeback):' /proc/meminfo"     # 脏页是否被控制在阈值附近
sudo fio --name=commit --directory=/mnt/test --rw=write --bs=4k --size=64m \
  --fdatasync=1 --runtime=60 --time_based                    # 同时测同步写的 p99

对比调整前后:Dirty 的峰值、同步写的 clat p99、iostat 里写入是"一阵一阵的突发"还是"平稳的一条线"。

swappiness

vm.swappiness(默认 60,5.8 内核起取值范围 0~200)控制内存回收时换出匿名内存与回收页缓存的倾向:值越低,内核越倾向于丢弃页缓存而保留进程的匿名内存。对自己管理缓存的数据库、对延迟敏感的存储服务,常见做法是设为 1~10;它不是"禁用交换"的开关,设为 0 也不等于没有 swap。验证看 vmstatsi/so/proc/vmstat 中的 pswpin/pswpout,以及 sar -Bmajflt/s

元数据密集的负载(海量小文件)还可以关注 vm.vfs_cache_pressure(默认 100):调低它会让内核更倾向于保留 dentry 和 inode 缓存,用 slabtop 观察 dentryxfs_inode/ext4_inode_cache 的变化来验证。

文件系统:挂载选项与格式化参数

挂载选项

《Systems Performance》第 8 章 8.8 节提到,noatime 曾经是最常见的文件系统调优:避免每次读都要更新访问时间,从而产生额外的元数据写。自 2.6.30 起内核默认使用 relatime(只在访问时间早于修改时间、或超过一天时才更新),这个问题已经基本解决了。对于读密集且确实不需要 atime 的场景,noatime 仍能省掉一点元数据写:

bash
findmnt -no OPTIONS /data              # 查看当前挂载选项
sudo mount -o remount,noatime /data    # 临时生效
# 永久生效:修改 /etc/fstab 对应行的选项,例如
# UUID=...  /data  xfs  defaults,noatime  0 0

验证:跑一个纯读负载(比如 tar cf /dev/null /data/many-small-files),对比调整前后 iostatw/s——读负载下的写请求基本就是元数据更新。

其他值得了解的选项:

选项作用建议
lazytime时间戳只在内存中更新,稍后批量写回relatime 配合,减少元数据写
discard删除文件时实时下发 TRIM部分 SSD 上会引起延迟抖动;优先用定期的 fstrim,Ubuntu 默认启用了每周运行的 fstrim.timer
ext4 commit=N日志提交间隔,默认 5 秒调大减少日志写,但崩溃时可能丢失更多数据
XFS logbsize=内存日志缓冲区大小,最大 256k元数据密集的负载可以设 logbsize=256k

格式化参数:与 RAID 条带对齐

格式化时最重要的参数是条带对齐:让文件系统的分配单元和 RAID 的条带对齐,避免一次写跨两个条带、触发额外的读-改-写(参见RAID)。

以一个 RAID 6 为例:6 块盘(4 块数据盘 + 2 块校验盘),条带单元(chunk)64 KiB。

bash
# XFS:su = 条带单元,sw = 数据盘数
sudo mkfs.xfs -d su=64k,sw=4 /dev/sdb
xfs_info /data | grep -E 'sunit|swidth'       # 验证:sunit=16 swidth=64 blks(单位 4 KiB 块)

# ext4:stride = chunk / 块大小 = 64K / 4K = 16;stripe_width = stride × 数据盘数 = 64
sudo mkfs.ext4 -E stride=16,stripe_width=64 /dev/sdb
sudo dumpe2fs -h /dev/sdb | grep -iE 'stride|stripe'

在 Linux 软 RAID(md)和 LVM 上,mkfs.xfs 通常能自动探测几何信息;硬件 RAID 卡往往不会把条带信息报告给操作系统,必须手动指定。格式化参数改错了只能重新格式化,这是少数必须在上线前做对的调优。

NVMe 中断与 NUMA 亲和

在双路服务器上,每块 NVMe 都挂在某一颗 CPU 的 PCIe 根端口下。从另一颗 CPU 上的进程访问它,每次 DMA 和中断都要跨 CPU 互联,既增加延迟,也消耗跨路带宽。先找出设备属于哪个 NUMA 节点:

console
$ cat /sys/class/nvme/nvme0/device/numa_node
1
$ lscpu | grep NUMA
NUMA node(s):                         2
NUMA node0 CPU(s):                    0-31,64-95
NUMA node1 CPU(s):                    32-63,96-127

NVMe 驱动为每个 CPU(或每组 CPU)建一个硬件队列,每个队列有自己的中断:

console
$ grep -H . /sys/block/nvme0n1/mq/{0,1,2}/cpu_list
/sys/block/nvme0n1/mq/0/cpu_list:0, 64
/sys/block/nvme0n1/mq/1/cpu_list:1, 65
/sys/block/nvme0n1/mq/2/cpu_list:2, 66
$ grep -E 'nvme0q[0-3]$' /proc/interrupts      # 截取前 4 个 CPU 列
 134:          0          0          0          0  IR-PCI-MSIX-0000:c1:00.0    0-edge      nvme0q0
 135:    1820345          0          0          0  IR-PCI-MSIX-0000:c1:00.0    1-edge      nvme0q1
 136:          0    1790221          0          0  IR-PCI-MSIX-0000:c1:00.0    2-edge      nvme0q2
 137:          0          0    1802210          0  IR-PCI-MSIX-0000:c1:00.0    3-edge      nvme0q3

nvme0q0 是管理队列,其余是 I/O 队列,每个队列的中断只落在对应的 CPU 上。这些是内核管理的中断(Managed IRQ),亲和性由内核按 CPU 拓扑自动分配,irqbalance 不会移动它们,手动写 /proc/irq/<N>/smp_affinity 也会被拒绝(返回 I/O 错误)。

所以 NVMe 的 NUMA 调优不是去改中断,而是把使用这块盘的进程放到正确的节点上

bash
# 用 numactl 把进程绑到 NVMe 所在的 node 1(CPU 和内存都绑)
sudo numactl --cpunodebind=1 --membind=1 fio --name=local --filename=/dev/nvme0n1 \
  --readonly --direct=1 --ioengine=io_uring --rw=randread --bs=4k --iodepth=1 --runtime=30 --time_based

# fio 自身也支持 NUMA 绑定
sudo fio --name=remote --numa_cpu_nodes=0 --numa_mem_policy=bind:0 --filename=/dev/nvme0n1 \
  --readonly --direct=1 --ioengine=io_uring --rw=randread --bs=4k --iodepth=1 --runtime=30 --time_based

验证:比较两次测试 QD1 下的 clat 均值和 p99,以及高 QD 下的最大 IOPS 和 cpu 行。跨节点的差距在 QD1 延迟上通常只有几微秒,但在打满多块 NVMe 的高吞吐场景下,跨路互联可能成为整机的瓶颈。对服务进程,用 systemd 的 NUMAPolicy=CPUAffinity= 或容器编排的拓扑管理做持久化绑定。同样的道理也适用于网络存储的网卡——网卡、NVMe 和处理进程最好在同一个 NUMA 节点上,这在 RDMA 一课里还会遇到。

持久化:udev、sysctl 与 tuned

echo 到 sysfs 的改动重启就没了。三种持久化手段各有分工。

udev 规则:块设备参数

设备出现(包括热插拔和重启)时由 udev 设置参数,这是持久化 sysfs 队列参数最标准的方式:

/etc/udev/rules.d/60-io-tuning.rules
# NVMe:不使用调度器;纯随机负载的节点可以关闭合并
ACTION=="add|change", KERNEL=="nvme[0-9]*n[0-9]*", ENV{DEVTYPE}=="disk", ATTR{queue/scheduler}="none"

# 机械盘:mq-deadline + 较大的预读
ACTION=="add|change", KERNEL=="sd[a-z]*", ENV{DEVTYPE}=="disk", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline", ATTR{queue/read_ahead_kb}="4096"

# SATA/SAS SSD:mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]*", ENV{DEVTYPE}=="disk", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"

ENV{DEVTYPE}=="disk" 用来排除分区(分区没有 queue/ 目录)。不重启就让规则生效,并验证:

bash
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=block --action=change
grep -H . /sys/block/*/queue/scheduler
udevadm test /sys/block/sda 2>&1 | grep -i scheduler     # 调试:看规则是否匹配

sysctl.d:内核参数

前面的 /etc/sysctl.d/90-storage.conf 就是持久化的方式,文件按名字顺序加载,数字大的后加载、会覆盖前面的设置。用 sudo sysctl --system 立即加载,用 sysctl -a | grep dirty 确认。

tuned:成套的配置档

tuned 把磁盘、sysctl、CPU 等设置打包成一个"配置档(profile)",可以一条命令切换,并能校验当前系统是否符合配置。Ubuntu 24.04 上需要单独安装:

bash
sudo apt install -y tuned
tuned-adm list                         # 内置的 throughput-performance、latency-performance 等
sudo mkdir -p /etc/tuned/storage-node
/etc/tuned/storage-node/tuned.conf
[main]
summary=Storage node: based on throughput-performance
include=throughput-performance

[disk]
# 对所有磁盘生效;readahead 前的 > 表示只在当前值更小时才调大
elevator=none
readahead=>4096

[sysctl]
# throughput-performance 会把 vm.dirty_ratio 调到 40,这里覆盖掉
vm.dirty_background_bytes=536870912
vm.dirty_bytes=4294967296
vm.swappiness=10
bash
sudo tuned-adm profile storage-node
tuned-adm active                       # Current active profile: storage-node
sudo tuned-adm verify                  # 检查当前系统设置是否与配置档一致

每一项怎么验证

把本课的内容汇总成一张表,这是调优时的核对清单:

调整项验证负载看什么
I/O 调度器混合负载:大块顺序写 + 4K 随机读读的 p99(fio)、biolatency -Q;以及调度器的 CPU 开销
nr_requests高并发随机读写blktraceS 事件、biolatency -Q 与设备延迟的差距
read_ahead_kb清缓存后 buffered 顺序读(direct=0吞吐、iostatrareq-szcachestat 命中率
max_sectors_kb大块顺序读写iostatareq-szbitesize 分布、吞吐
nomerges / rq_affinity高 IOPS 随机小 I/Ofio 的 IOPS 与 cpu 行、mpstat 各核软中断
vm.dirty_*持续大写入 + 同步写探针Dirty 峰值、同步写 p99、iostat 写入是否平稳
swappiness内存压力下的真实业务si/somajflt/s、业务延迟
noatime纯读负载iostatw/s
条带对齐小块随机写RAID 卡/md 的读-改-写次数、写 IOPS
NUMA 绑定QD1 延迟与高 QD 峰值clat 均值与 p99、IOPS、跨路带宽

动手练习

  1. 拍快照。 运行 io-snapshot.sh,找出你的实验机上每块盘的调度器、nr_requestsread_ahead_kbmax_sectors_kb,并解释为什么 NVMe 和 SATA 盘的默认值不同。
  2. 调度器对比。 在一块 HDD 或 SATA SSD 测试盘上,用本课的混合负载命令分别测试 nonemq-deadlinebfq 三种调度器,记录读 job 的 p99 和写 job 的吞吐,并用 mpstat 观察 CPU 开销的差异。
  3. 验证预读假设。 在测试文件系统上建一个 8 GiB 的文件,按本课的循环脚本测试 read_ahead_kb 为 128、1024、4096 时的 buffered 顺序读吞吐;再把 --direct=0 改成 --direct=1 重做一次,解释为什么差别消失了。
  4. 观察脏页阈值。 先记录默认设置下 dd 持续写入时 Dirty 的峰值和同步写 p99,再把 vm.dirty_background_bytes 设为 256 MiB、vm.dirty_bytes 设为 1 GiB 重做,比较两次的结果。做完用 sudo sysctl --system 恢复。
  5. 写一条 udev 规则。 为你的测试盘写一条规则,把调度器设为 mq-deadline、预读设为 1024 KiB,执行 udevadm trigger 使其生效并验证;然后重启机器,确认设置依然存在。

自测

为什么 NVMe 通常使用 none 调度器,而 HDD 通常使用 mq-deadline

NVMe 内部有大量并行单元和多个硬件队列,请求的顺序对它影响很小,内核排序只会增加 CPU 开销和锁争用,所以直接下发(none)最好。HDD 的随机访问要付出寻道和旋转的代价,mq-deadline 能按扇区排序、合并相邻请求,并用截止时间保证读请求不会被大量写饿死,改善混合负载下的延迟。

read_ahead_kb 调大后用 fio --direct=1 测试,发现顺序读吞吐毫无变化,说明预读调整无效吗?

不说明。预读是页缓存的机制,O_DIRECT 绕过了页缓存,根本不会触发预读,所以 direct I/O 测不出预读的效果。应该用 --direct=0 的 buffered 顺序读,并在每次测试前清空页缓存。另外预读对高延迟设备(云盘、网络块设备、HDD 阵列)效果明显,在低延迟的 NVMe 上差别本来就很小。

一台 1 TB 内存的节点上,数据库每隔几十秒出现一次长达十几秒的提交卡顿,同时 /proc/meminfoDirty 周期性涨到上百 GB。原因可能是什么?怎么调?

默认 vm.dirty_ratio=20dirty_background_ratio=10 按可用内存的百分比计算,在 1 TB 内存上允许积累上百 GB 脏页,回写时大量写请求占满设备,数据库的 fsync 只能排队等待,造成提交卡顿。应改用 vm.dirty_background_bytesvm.dirty_bytes 设定较小的绝对阈值(例如几百 MiB 和几 GiB),让回写更早开始、更平稳;然后用 Dirty 峰值、同步写 p99 和 iostat 的写入曲线验证效果。

为什么写 /proc/irq/<N>/smp_affinity 无法修改 NVMe I/O 队列中断的亲和性?NVMe 的 NUMA 优化应该怎么做?

NVMe 驱动使用内核管理的中断(Managed IRQ),每个 I/O 队列与一组 CPU 绑定,亲和性由内核按 CPU 拓扑自动分配,不允许用户修改,irqbalance 也不会移动它们。优化方法是反过来:查出设备所在的 NUMA 节点(/sys/class/nvme/nvmeX/device/numa_node),用 numactl、systemd 的 CPU/NUMA 设置或 fio 的 numa_cpu_nodes 把使用该设备的进程和内存绑到同一节点上,并用延迟和 IOPS 对比验证。

在自定义 tuned 配置档中 include=throughput-performance,却发现 vm.dirty_ratio 变成了 40,这是为什么?如何避免这类问题?

throughput-performance 配置档本身会修改一些 sysctl,包括把 vm.dirty_ratio 调大到 40,include 会继承这些设置。应在自己的配置档 [sysctl] 中显式覆盖需要的值(例如设置 vm.dirty_bytes),用 tuned-adm verify 和配置快照核对实际生效的值,并且对同一类参数只用一种机制(tuned 或 udev/sysctl.d)管理,避免互相覆盖。

参考资料

学完了吗?

进度保存在本机浏览器中,可在学习路线页查看。