ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

银河麒麟服务器IO性能调优:从机制到实战

银河麒麟服务器IO性能调优:从机制到实战

银河麒麟服务器操作系统我接触得不算晚,最早是在某单位的迁移项目里做环境适配,当时最头疼的不是应用起不来,而是IO。数据库导入慢、文件服务延迟高、备份任务把业务都拽死了,翻来覆去查系统日志都正常,最后追踪到IO调度和回写参数上才找到突破口。

这篇文章不打算讲虚的,把银河麒麟服务器操作系统里和IO相关的机制从头梳理一遍:数据从应用层到磁盘到底走了哪些环节,哪些参数真正影响性能,IO卡顿的时候怎么一步步锁因。无论你是在做数据库运维、文件存储,还是把业务容器化部署到这套系统上,读完之后起码遇到IO问题不会抓瞎。

1. IO机制的全貌梳理:先看清数据走了多远

1.1 一次写盘操作背后要经过的7个关卡

处理IO问题最忌讳的就是只看单个指标。很多朋友一遇到性能问题就盯着iostat的%util看,util上去了就断定磁盘满了,实际上IO链路上任何一个环节拖后腿,最终指标都会异常,但根因可能完全在另一个地方。

一次普通的写盘操作,从应用发起到底层落盘,大致要经过这些环节:

  • 应用调用write()系统调用,把数据从用户态缓冲区拷到内核态。
  • 内核进入虚拟文件系统层,根据文件路径找到对应文件系统实例。
  • 具体文件系统(ext4或XFS)根据inode信息,分配逻辑块和物理块映射,决定数据写到哪里。
  • 如果走的是缓冲IO路径,数据先进入页缓存,此时并没有真正落盘。
  • 通用块层负责把多个IO请求合并、排序,形成块设备层请求队列。
  • IO调度器按策略决定请求的执行顺序。
  • 设备驱动把请求拆成硬件指令,通过DMA通道和中断机制与磁盘交互。
  • 最后才是磁盘介质上的实际读写。

这就像快递寄包裹:你下了单,快递员要经过仓库分拣、干线运输、网点派送才能到收件人手里,每个环节都可能有延误。IO问题也一样,应用的请求等到真正落到磁盘,中间每一站都可能卡住。所以排查时不能只盯一个指标,得先对整个路径有数。

1.2 银河麒麟环境里的IO栈有什么特点

银河麒麟系统基于Linux内核,IO机制和Linux社区版本一脉相承,经典的那些参数、工具、排查思路基本都能沿用。但实际落地时,有几个特点需要注意。

一是默认文件系统选型不同。很多官方文档和装机流程默认使用XFS,也有很多存量机器跑的是ext4。XFS在处理大文件、高并发、大容量场景下更有优势,ext4在小文件和兼容性上更稳。这两个文件系统在IO行为上的差异很明显,比如XFS的延迟分配策略、allocsize参数,ext4的delalloc机制,都会直接影响写入性能。

二是服务器硬件平台非常多样。银河麒麟不光跑在标准x86服务器上,还常见于各类国产芯片平台,ARM架构和x86并存,存储设备也五花八门,有NVMe固态盘、SAS盘、SATA盘、老式HDD混用的情况。同样的内核参数,在NVMe固态盘和传统机械盘上的效果截然不同。比如给机械盘设置过高的队列深度,反而会增加延迟和IO抖动。

三是厂商常常做了一些定制化调整。有些机型出厂预装的系统里,内核版本、默认调度器、文件系统挂载参数都改过。我遇到过一台机器,默认调度器被设置成none,跑在NVMe固态盘上没问题,但后来业务扩展挂载了机械盘后,延迟高得离谱,就是默认策略不适配新硬件导致的。

1.3 CPU内存正常业务却卡顿,问题大概率藏在IO链路

我踩过最典型的一个坑,是某天业务突然变慢,登录服务器一看,CPU不到20%,内存还剩一大半,负载却到了两位数。当时第一反应是网络或者应用层问题,排查了半天没结论,后来用iotop一看,jbd2进程(文件系统日志提交线程)占着大量IO资源,脏页回写堵住了整个存储路径。

这种案例其实很常见。CPU高、内存高通常一眼就能看到瓶颈,但IO卡顿的表现往往隐蔽得多,因为每个单独指标的“数值”看起来都正常,问题出在它们之间的关联关系上。这也是为什么我觉得先把IO机制嚼透,比单纯记住一堆调优参数更有价值——遇到问题至少知道往哪个方向查。

2. 核心IO机制的技术拆解:从缓冲到异步IO

2.1 缓冲IO、直接IO、内存映射到底怎么选

IO模式的选择,直接决定了数据在内核里怎么流转,也就决定了延迟、吞吐和CPU开销的水平。我通常把这三种模式称为“IO三兄弟”,各有脾气,用错场景就会翻车。

**缓冲IO(Buffered IO)**是最通用的方式,数据先写到页缓存,由内核后续异步回写到磁盘。好处是读数据时命中缓存就能直接返回,写数据时应用不用等磁盘落盘,吞吐高,延迟低。坏处是数据实际到达磁盘的时间不确定,掉电可能丢数据,而且脏页积压到一定程度会导致回写风暴。

**直接IO(Direct IO)**绕过页缓存,数据直接从用户态缓冲区搬进磁盘,应用自己要承担缓存管理。数据库这类应用最喜欢直接IO,因为自己已经做了内存缓冲,不希望内核再缓存一份导致双倍内存开销,也更可控,写入即落盘。缺点是每次IO都需要等待真正的磁盘操作,随机小IO场景下性能可能很惨。

**内存映射(MMAP)**把文件直接映射到进程地址空间,读写数据就像操作内存一样,不需要显式调read/write。适合大文件共享、消息队列这种场景。但mmap有个坑:文件被截断或磁盘空间不足时,进程可能收到SIGBUS信号,处理不好直接崩溃。

IO类型缓存机制延迟特点适合场景典型风险
缓冲IO经过页缓存低延迟高吞吐文件服务、Web服务、日志收集掉电丢数据、回写风暴
直接IO绕过页缓存高可控、落盘即返回数据库数据文件、高可靠写入小IO性能差、需自制缓存
内存映射映射到用户空间读写像内存大文件共享、消息队列SIGBUS、脏页回收负担

实际选择时,我习惯先问一个问题:这个数据丢了能接受吗?业务自己能管缓存吗?比如在银河麒麟上跑数据库,数据文件我一定用直接IO,日志盘也是直接IO,但归档日志或备份文件就走缓冲IO,吞吐大,稍许延迟无所谓。

2.2 IO调度器:选错调度器,磁盘直接“假死”

IO调度器处于通用块层和设备驱动之间,负责把上层的IO请求排序、合并、控制延迟。传统内核里有CFQ,多队列时代主要看mq-deadline、kyber、none这几种。

mq-deadline可以理解为每把请求按“截止时间”排序,兼顾公平和延迟,是目前最稳的通用选择。kyber走的是延迟控制路线,它根据设备不同队列的延迟目标动态调整并发量,适合延迟敏感的NVMe场景,但对拥塞状态的估算偶尔会过激,导致吞吐上不去。none其实就是不调度,直接把请求扔给硬件队列,最大化吞吐,最适合NVMe固态盘这种本身就带智能队列的设备。

查看当前调度器很简单:

cat /sys/block/sda/queue/scheduler

临时切换也容易:

echo mq-deadline > /sys/block/sda/queue/scheduler

但要注意两个坑。第一个是银河麒麟系统重启后,调度器设置会回到默认值。要持久化,一般写成udev规则或开机执行脚本,比如在/etc/rc.d/rc.local里加一行。第二个坑更隐蔽:有些硬件RAID卡自带IO队列和固件调度,内核层再排一次队等于双重排队,反而降低性能。判断方法是看/sys/block/设备名/queue/下面是否有硬件队列信息,如果硬件侧已经做了大量调度工作,优先考虑none或mq-deadline。

我自己的经验是:机械盘和普通SATA SSD用mq-deadline,NVMe固态盘直接用none,低延迟业务追求更稳定的P99延迟时再试kyber,测完再看数据决定。调度器对整体吞吐的影响不像硬件那么大,但对延迟抖动的影响非常明显,这部分值得花时间调。

2.3 页缓存与回写:脏页积压的隐藏成本

缓冲IO模式下,所有写操作其实都是先写页缓存,把页面标记为脏页,再等内核后台把这些脏页刷到磁盘。这就是“回写”机制。

驱动回写的主要条件有几个:脏页数量达到一定比例、脏页存在超过一定时间、应用主动调用sync或fsync。内核提供了一组关键参数:

  • vm.dirty_background_ratio:脏页占内存比例达到该值,后台内核线程开始异步回写,不阻塞应用。
  • vm.dirty_ratio:脏页占比达到该值,应用自己的写操作会被阻塞,直到脏页降到阈值以下。
  • vm.dirty_expire_centisecs:脏页的“过期时间”,时间到了即使比例不高也会被刷出去。

默认情况下,dirty_background_ratio一般是10,dirty_ratio是20。这个配置在内存大的机器上可能积累巨量脏页,比如一台256GB内存的服务器,20%就是50多GB的脏数据。一旦触发应用阻塞,所有写操作瞬间“冻住”,表现为业务写入卡死,但CPU和IO都显示很闲,因为瓶颈在回写同步逻辑上。

数据库类应用通常希望减少这种抖动,会主动调低阈值:

sysctl -w vm.dirty_background_ratio=5 sysctl -w vm.dirty_ratio=10

但也不是越低越好。调太低,内核会频繁启动回写线程,导致写吞吐明显下降。我的一般做法是:普通文件服务器保持默认或略低,比如background降到8、ratio降到16;数据库服务器可以激进一点,background=2、ratio=5,同时搭配直接IO,让业务数据不走页缓存,脏页压力自然就小了。

2.4 io_uring与传统AIO,异步IO的正确打开方式

说到异步IO,传统Linux AIO接口(io_setup、io_submit那一套)一直存在不少限制,最典型的是只支持O_DIRECT模式,不支持缓冲IO;而且异步通知机制要靠事件循环轮询,搞起来很繁琐。很多跑在银河麒麟上的老应用,所谓“异步IO”其实是用线程池模拟的,线程一多,上下文切换成本就上来了。

io_uring是新一代异步IO框架,核心思路是用共享内存环形队列在用户态和内核态之间传递IO请求和完成事件,避免了频繁系统调用的开销,还支持轮询模式、注册文件、固定缓冲区等高级特性。实际测试中,在NVMe设备上跑高并发随机读写,io_uring配合直接IO能明显降低CPU占用和时延抖动。

对运维人员来说,即便应用代码没有用io_uring接口,也可以用引擎辅助测试。比如fio指定ioengine=io_uring:

fio --name=test --rw=randread --bs=4k --size=2G --iodepth=64 --ioengine=io_uring --direct=1 --runtime=60

不过多数现有应用不会因为系统支持io_uring就自动受益,这需要应用层改造,比如数据库、存储软件升级到支持io_uring的版本。我们在选型时,会优先看应用的IO引擎是否支持io_uring,对新部署的存储类服务尽量选新版。老应用保持原有IO路径不强行迁移,毕竟稳定优先。

3. 性能调优的实操路径:从摸底到落地

3.1 调优之前先拿fio跑个基线

没有基线数据,调参就是在碰运气。我每一次调优前都会用fio把当前环境的真实性能摸一遍,记录顺序读写、随机读写、混合读写几类场景的吞吐和延迟,然后再动参数。

fio是IO基准测试的事实标准,银河麒麟上可以直接安装。测顺序读,适合评估大文件拷贝、备份场景:

fio --name=seqread --rw=read --bs=1M --size=4G --iodepth=64 --ioengine=libaio --direct=1 --numjobs=1

测随机写,模拟数据库日志写入:

fio --name=randwrite --rw=randwrite --bs=4k --size=2G --iodepth=32 --ioengine=libaio --direct=1 --runtime=60 --time_based

测试过程中要注意:必须配合direct=1,否则测试数据被页缓存吞掉,测出来的全是内存速度。另外测试时长至少60秒以上,短测很难暴露稳态延迟问题。yd其他一个容易忽略的点是,fio测试会对生产环境产生一定压力,最好在业务低峰期跑,或者用空闲备机测。

测完的结果要记录几个核心指标:IOPS、带宽、平均延迟(lat avg)、P99延迟。这些数据是后面判断“调优是否有效”的唯一依据。

3.2 内核参数与块设备队列参数怎么调

IO调优的另一个大头是内核虚拟内存参数和块设备队列参数,这里挑几个实操频率最高的。

先看脏页回写相关的两个,前面已经提过。实际修改时用sysctl:

sysctl -w vm.dirty_background_ratio=5 sysctl -w vm.dirty_ratio=15

再看块设备队列的几个参数。/sys/block/sda/queue/nr_requests控制内核为设备维护的IO请求数,也就是队列深度。对NVMe固态盘,可以适当增大,比如从默认的128调到512或1024,提升高并发场景下的吞吐。机械盘就别动这个,队列加深只会增加排队延迟。

/sys/block/sda/queue/max_sectors_kb控制单次IO的最大大小,默认128KB或512KB。如果有大块顺序读写需求,比如备份、数据迁移,可以调到1024KB:

echo 1024 > /sys/block/sda/queue/max_sectors_kb

/sys/block/sda/queue/read_ahead_kb控制预读大小,适合顺序读场景,对随机读没什么帮助,还可以关掉避免浪费IO:

echo 2048 > /sys/block/sda/queue/read_ahead_kb

还有一个经常被忽略但影响巨大的硬件层面设置:RAID卡缓存策略。带电池或闪存保护的RAID卡,把写缓存策略调到writeback,写入性能能提升好几倍;没有掉电保护的卡,别省这个心,老老实实用writethrough,否则掉电就是数据丢失事故。这个参数在RAID卡管理工具里改,不同品牌命令不一样,但重要性一定要知道。

3.3 文件系统挂载参数和格式化规划不能偷懒

文件系统层面的配置,决定IO请求在内核里怎么被处理。银河麒麟默认装XFS的机器不少,装ext4的也不少,二者的调优思路不完全一样。

XFS推荐打开noatime,避免每次访问文件都更新访问时间戳,减少不必要的写IO。挂载参数示例:

mount -t xfs -o noatime,allocsize=64k /dev/sdb1 /data

allocsize是XFS的预分配大小,对大文件顺序写很有帮助,可以按实际业务调整,一般64K到1M之间。对大目录、多文件场景,还有inode64这个参数,让inode分配不局限于前1TB空间,避免老文件系统上inode耗尽的问题。

ext4这边,首先考虑barrier参数。默认是开启的,保证日志提交时数据顺序正确,但对性能有一定影响。如果RAID卡有电池保护,确保掉电时缓存能正确落盘,这时可以关掉屏障:

mount -t ext4 -o noatime,barrier=0 /dev/sdb1 /data

另外ext4默认开启了延迟分配(delalloc),对小文件写入有优化效果,通常不用改。

格式化规划往往被忽视,但实际上收益很大。比如在RAID阵列上格式化XFS时,stripe width和stripe unit设置不匹配,文件系统就不知道底层阵列的条带布局,写入时可能产生大量读改写操作。格式化时按阵列信息加参数:

mkfs.xfs -d su=64k,sw=4 -f /dev/sdb

数据库场景更要把数据盘和日志盘分离到不同物理设备上,避免日志写和随机数据写互相抢IO。

3.4 应用层IO优化:改代码之外的可行路线

很多时候我们没有应用源码,没法做代码级改造,但这不代表只能干瞪眼。有几个应用侧的IO优化思路,效果往往比调内核参数还明显。

减少小IO次数是大头。一个典型的场景是日志写入,应用大量小文件或小数据块频繁fsync,IOPS压力巨大。解决办法是引入缓冲层,比如用日志采集代理批量刷盘,把几百个小IO合并成几个大IO,吞吐直接翻倍。合并IO的本质是利用磁盘的顺序带宽优势,和小时候攒硬币去银行换整钞一个道理。

控制应用并发数。无脑堆线程数反而会让IO队列拥塞,导致延迟上升。数据库连接池、应用线程池都要设置合理上限,别让同一个存储设备上排队几千个请求。一个粗略的经验值是:机械盘并发保持在几十以内,SSD可以到几百,NVMe可以更高,具体用fio测出最优并行度再倒推。

引入中间缓存层。面对热点数据重复读的场景,Redis这类缓存能让大部分读请求不碰磁盘。但这要区分场景,不要因为缓存能抗住读就忽视写路径,写多读少的业务加了缓存也白搭。

延迟敏感的业务还有一个思路:把低优先级任务错峰。备份、数据清洗、批量计算这些大IO任务尽量安排到业务低谷,避免和核心链路抢同一块磁盘资源。

4. 常见IO故障与排查实录

4.1 排查工具链怎么组合最顺手

IO故障排查,工具永远是辅助,思路才是核心。我常用的组合是这样的:

  • iostat -x 1看设备级指标的长期趋势,重点观察r/s、w/s、await、%util、svctm(虽然svctm在多队列时代表现不太靠谱,但仍有参考意义)。
  • vmstat 1看系统级IO交换状态,重点是bi、bo两列,以及等待IO的进程数r列是否异常。
  • pidstat -d 1定位哪个进程在产生IO压力,避免被整体指标误导。
  • iotop实时看进程IO速率,适合快速找出“谁在搞事”。
  • strace -f -e trace=read,write,fsync跟踪应用系统调用,确认是否发生大量同步写。
  • blktrace配合blkparse做块层请求级的跟踪,能精细到每个IO请求的位置、大小、延迟,但生产环境慎用,开销不小。

排查顺序一般是这样:先用vmstat判断是不是IO问题,再用iostat看设备健康度,用pidstat和iotop缩小到进程,最后用strace或blkparse深挖原因。这个顺序能避免一上来就被工具带偏。

4.2 await高但%util不高,别急着判磁盘死刑

iostat里,await表示IO请求从进入到完成所用的时间,%util表示设备忙于处理请求的时间比例。按照传统经验,二者应该同步:设备忙,await就应该高。但实际中经常出现await飙到几百毫秒,util却只有百分之三四十的诡异情况。

这种反常现象背后可能有几种原因。

**软RAID重组开销。**用mdadm做的软阵列,写操作需要计算校验或做镜像,CPU要把数据先做处理后才能下发。这时候设备本身空闲,但请求在中间层被拖住了。

**块设备限流。**系统配置了cgroup的io限制,比如blkio.throttle.io_serviced,超过限额的IO请求会排队等待令牌,await就涨上去了。排查时可以看cgroup配置,或者直接关掉限流测试。

**设备正在执行内部操作。**SSD做垃圾回收、磨损均衡、坏块重映射时,会暂时“神秘地”拉高延迟。这种情况NAND内部操作的优先级会抢占正常IO,外部的util看不出忙,但单个请求延迟飙升。

**多队列设备util计算陷阱。**blk-mq架构普及以后,%util对多硬件队列设备的计算方式已经和传统单队列不同,设备util为100%的判断标准不再绝对可靠。方向是结合iostat的r_await、w_await两个分项,它们比总await更能区分读慢还是写慢。

遇到这种情况,我的排查动作是:先看软件RAID状态和cgroup配置,排除人为限流;再用smartctl查磁盘健康信息,看是否记录了大量pending sectors或CRC错误;最后用blktrace看延迟发生在下发前还是下发后。多试几次,根因总能浮出来。

4.3 进程卡在D状态,如何定位是IO hang还是驱动异常

Linux进程状态里有个D状态(不可中断睡眠),进程到D状态一般是在等待IO完成,正常情况下很快出来。但如果大量进程长期停在D状态,基本就是某个IO请求永远得不到响应的信号。

定位方法不算复杂。先看系统里D状态的进程:

ps -eo pid,stat,wchan:30,cmd | grep '^.*D' | grep -v grep

wchan列会显示进程在内核里等待的内核函数,结合/proc/PID/stack看内核栈,能判断卡在哪里。比如卡在wait_on_page_bit一般是页缓存等待回写,卡在scsi_wait通常是SCSI层等设备响应。

再查dmesg和/var/log/messages,如果伴随reset、abort、target failure之类的日志,大概率是硬盘或控制器层面出了故障,比如磁盘固件bug、SAS链路抖动、控制器半死状态。如果是NFS挂载的目录导致IO hang,反而是网络问题,但表现也是D状态进程堆积。

处理上要分情况。硬件故障驱动的IO hang,调参数救不了,只能更换设备、重启控制器。NFS导致的问题,可以先强制卸载或切换挂载点。某些内核bug导致的挂死,就只能结合内核版本排查了。这里有个经验:重启之前一定要先保留现场,把/proc/进程/stack、dmesg、iostat记录都保存下来,否则重启后想查什么线索都没了。

4.4 回写风暴来了,整机像冻住一样,怎么止血

回写风暴是最有迷惑性的一种IO故障。现象是整机突然卡顿,CPU空闲,内存也够,但所有写请求都慢得离谱,负载直线飙升。原因就是脏页积压到dirty_ratio阈值,内核强制应用同步回写,所有写进程一起被堵在门口。

临时止血的办法很直接,调高后台回写的积极性,让脏页尽早开始回写,别攒到阻塞点:

sysctl -w vm.dirty_background_ratio=3 sysctl -w vm.dirty_ratio=6

同时可以手动触发一次回写,让脏页尽快落盘:

sync

但sync命令会同步等待所有脏页落盘,如果脏页量已经很大,执行sync的过程本身也会卡住,要有点耐心。更稳的操作是等系统自己慢慢消化,先把应用写入停止,让后台线程跑完。

长期方案才是重点。要避免回写风暴,一是给写操作错峰,大任务(备份、批量导入)放到业务低谷;二是监控要能提前告警,比如通过监控系统跟踪dirty页占比,超过阈值就触发预警,而不是等业务卡死才发现;三是如果是数据库这类高写入应用,优先走直接IO,让大量写请求不经过页缓存,从源头上减少脏页压力。

我在一次实际运维中还发现,定时任务里的find、grep全盘扫描会在不经意间把文件系统的访问时间戳更新打开,产生大量回写IO。后来把所有挂载点都加上noatime,这类问题明显减少。这提醒我:IO优化是细水长流,不是调一两个参数就完事。

说来也怪,IO调优这事儿,很多时候不是知道多少高级参数,而是能不能一眼看出瓶颈在哪个环节。我个人习惯是每次碰到IO问题,先画一遍数据链路图,然后按环节排除,这个方法帮我省了大量时间。那次回写风暴最后解决靠的也不是一个参数,而是把备份任务、文件系统挂载选项、脏页阈值几个点一起改掉,才算真正脱困。

银河麒麟系统的IO机制说到底是Linux那套东西,但落地到具体机型、具体业务上,真正考验的是你对自己环境的熟悉程度。新机器到手我通常先花一晚跑一遍fio基线,把每种存储设备的最佳队列深度、文件系统挂载选项都记下来,后面再出问题就有据可查。比网上随便搜一堆参数乱试要靠谱得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进