4、IO性能监控:IO瓶颈识别、iostat与/proc/diskstats、文件系统与fstrim、mmap与传统IO对比

IO性能,说白了就是手机读写数据的速度。

你想想看,App启动要读代码,加载图片要读文件,数据库要写日志。如果IO慢了,整个系统就像被卡住脖子。我见过太多案例,CPU和内存都正常,但手机就是卡,一查,IO瓶颈。

4.1 IO瓶颈识别:从现象到根因

怎么判断是不是IO的问题?我个人习惯先看三个现象:

  • App启动慢:冷启动时,如果卡在「加载资源」阶段,大概率是读文件慢。
  • 滑动掉帧:滑动时突然卡一下,往往是主线程在等IO。
  • 安装/更新慢:这不用说了,纯写文件操作。

我在项目中遇到过一个问题:某款手机在安装大型游戏时,进度条走到80%就卡住。查了一圈,发现是文件系统碎片化严重,写入速度从50MB/s掉到了5MB/s。嗯,这就是典型的IO瓶颈。

核心指标:IO瓶颈的黄金三指标

  • IO等待时间(iowait):CPU空闲等待IO完成的时间占比。超过10%就要警惕。
  • IO吞吐量(IOPS):每秒读写次数。随机读写场景下,IOPS比带宽更重要。
  • IO延迟(Latency):单次IO请求的完成时间。超过10ms就算慢。

4.2 iostat与/proc/diskstats:实战工具

说到IO监控工具,iostat是首选。但Android上默认没有,需要自己编译或者用busybox。我建议直接用/proc/diskstats,它更底层,也更直接。

4.2.1 /proc/diskstats 详解

这个文件记录了每个磁盘设备的IO统计信息。格式如下:

major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq
259    0     dm-0 12345 6789 98765 123456 54321 9876 45678 654321 0 78901 123456

关键字段解释:

字段 含义 实战用法
rio / wio 读写请求次数 计算IOPS = (rio + wio) / 时间间隔
rsect / wsect 读写扇区数(1扇区=512字节) 计算吞吐量 = (rsect + wsect) * 512 / 时间间隔
ruse / wuse 读写花费的总时间(毫秒) 计算平均延迟 = (ruse + wuse) / (rio + wio)
running 当前正在处理的IO请求数 如果持续大于0,说明IO队列有积压

我的小技巧:写一个shell脚本,每秒读取一次/proc/diskstats,计算差值。这样就能实时看到IOPS和延迟的变化。我曾经用这个脚本抓到一个第三方App疯狂写日志的问题,每秒写2000次,直接把闪存寿命写短了。

4.2.2 iostat 实战

如果设备上有iostat,用法很简单:

# 每1秒输出一次,共输出5次
iostat -x 1 5

# 只看某个设备
iostat -x dm-0 1 5

输出中重点关注%util字段。这个值表示设备忙的时间占比。如果接近100%,说明IO已经饱和了。但要注意,%util高不一定就是瓶颈,也可能是设备本身性能差。

避坑指南:我曾经被%util骗过一次。某个设备%util一直100%,但IOPS和延迟都很正常。后来发现是设备驱动实现的问题,它把等待队列的时间也算进去了。所以,一定要结合IOPS和延迟一起看。

4.3 文件系统与fstrim:闪存的保养秘笈

Android用的是ext4或f2fs文件系统。闪存(eMMC/UFS)有个特性:写入前必须先擦除。如果文件系统碎片化严重,写入速度会急剧下降。

fstrim就是干这个的。它告诉闪存哪些块是空闲的,让闪存提前擦除。这样下次写入时就能直接写,不用先擦除。

# 手动触发fstrim
adb shell fstrim -v /data

# 查看上次fstrim时间
adb shell cat /proc/fs/f2fs/sda/trim_speed

我建议在系统空闲时定期执行fstrim。比如每天凌晨一次。如果设备长期不trim,写入速度可能从100MB/s掉到10MB/s。嗯,这就是为什么有些手机用久了会变慢的原因之一。

关键点:fstrim不是万能的。它只对文件系统层面的碎片有效。如果闪存本身磨损严重,trim也救不了。所以,监控闪存寿命也很重要。

4.4 mmap与传统IO对比:谁更快?

这个问题我经常被问到。先说结论:没有绝对的快慢,取决于场景

4.4.1 传统IO(read/write)

传统IO的流程:

  1. 用户态调用read()
  2. 陷入内核态,从磁盘读到内核缓冲区
  3. 从内核缓冲区拷贝到用户态缓冲区
  4. 返回用户态

这里有一次数据拷贝(内核→用户),还有两次上下文切换。对于小文件,开销不大。但对于大文件,拷贝成本就高了。

4.4.2 mmap

mmap直接把文件映射到进程地址空间。读写就像操作内存一样。流程:

  1. mmap建立映射,不实际读数据
  2. 访问映射区域时,触发缺页中断
  3. 内核从磁盘读数据到页缓存
  4. 用户态直接访问页缓存,无需拷贝

少了数据拷贝,理论上更快。但有个坑:缺页中断。如果访问模式是随机的,每次访问都可能触发缺页,反而比传统IO慢。

我的经验:在Android上,我推荐用mmap的场景:

  • 频繁读取大文件的固定区域(比如数据库的索引)
  • 需要共享内存的场景(多个进程读同一个文件)
  • 对延迟敏感,但能接受偶尔的缺页开销

不推荐用mmap的场景:

  • 小文件频繁读写(mmap建立映射的开销比read大)
  • 随机访问模式(缺页中断太多)
  • 文件大小会动态变化(mmap需要重新映射)

4.4.3 性能对比数据

我做过一个测试:读取一个100MB的文件,顺序访问:

方法 耗时(ms) CPU占用 内存占用
read() 每次4KB 850 12% 4KB
read() 每次1MB 320 8% 1MB
mmap 顺序访问 280 5% 100MB(虚拟内存)
mmap 随机访问 1200 20% 100MB(虚拟内存)

看到了吧?顺序访问时mmap确实快,但随机访问反而更慢。所以,选哪个,得看你的访问模式。

避坑指南:我曾经在一个图片加载库中用了mmap,想着能加速。结果用户反馈滑动时更卡了。一查,图片是随机访问的(用户滑动到哪里就加载哪里),mmap的缺页中断把IO延迟拉高了。后来改回传统read,配合预读,反而更流畅。

4.5 总结:IO监控的实战要点

  • 监控工具:/proc/diskstats 比 iostat 更底层,适合写脚本自动化监控。
  • 关键指标:IOPS、延迟、iowait,三个一起看,别只看一个。
  • 文件系统:定期fstrim,别让碎片化拖慢写入速度。
  • IO模型:顺序访问用mmap,随机访问用传统read+预读。

嗯,IO优化是个细活。没有银弹,只有对症下药。希望这些经验能帮你少走弯路。