4、I/O性能调优:I/O栈分析、文件系统选择、I/O调度器调优、存储性能测试工具
说到Android系统的性能调优,I/O这块儿往往是大家最容易忽视的。你想想看,CPU和内存优化大家都很重视,但存储一慢,整个系统就像被掐住了脖子。我这些年排查过不少卡顿问题,最后发现根因都在I/O上——嗯,今天我们就来好好聊聊这个。
4.1 I/O栈分析:从应用到闪存的全链路
先搞清楚I/O请求是怎么走的。Android的I/O栈,说白了就是一条从上层应用到下层闪存的完整链路。我个人习惯把它分成四层来看:
- 应用层:你写的Java/Kotlin代码,调用FileOutputStream、SQLite、SharedPreferences等API
- 框架层:Android的ContentProvider、MediaStore等系统服务,以及Vold(Volume Daemon)
- 内核层:VFS(虚拟文件系统)、具体文件系统(如ext4/f2fs)、Block Layer、I/O调度器
- 硬件层:eMMC、UFS闪存控制器、FTL(闪存转换层)
我在项目中遇到过一个问题:某款手机在安装大量应用后,启动速度越来越慢。用systrace一抓,发现I/O等待时间占了启动时间的60%以上。顺着栈一层层往下查,最后发现是文件系统碎片化严重,加上I/O调度器配置不当导致的。
核心观点:I/O性能瓶颈往往不在硬件,而在软件栈的某一层。你调优时一定要有全局视角,别一上来就怪闪存慢。
分析I/O栈,我常用的工具有这些:
| 工具 | 作用 | 使用场景 |
|---|---|---|
| systrace / Perfetto | 抓取I/O事件时间线 | 定位I/O等待、线程阻塞 |
| iostat / vmstat | 查看磁盘读写速率、IOPS | 实时监控I/O负载 |
| block_dump | 记录每个块设备的I/O请求 | 分析I/O请求大小、队列深度 |
| fio | 基准测试工具 | 评估存储极限性能 |
4.2 文件系统选择:ext4 vs f2fs
Android从4.4开始引入f2fs(Flash Friendly File System),专门为闪存设计。ext4是传统文件系统,成熟稳定。那到底选哪个?
我直接说结论:新设备优先用f2fs,老设备或兼容性要求高的场景用ext4。
为什么?f2fs有几个关键优势:
- 减少写放大:f2fs的日志结构设计,天然适合闪存的写入特性。ext4的日志(journal)会额外写两倍数据。
- 碎片化控制:f2fs有专门的碎片整理机制,长期使用后性能衰减比ext4慢得多。
- 小文件性能:f2fs对小文件(比如应用缓存、数据库WAL日志)的读写更快。
我的经验:在某个平板项目上,我们把/data分区从ext4换成f2fs后,随机写入性能提升了30%以上,应用安装速度几乎翻倍。但要注意,f2fs在极端掉电场景下的数据一致性不如ext4——嗯,这就要看你的产品定位了。
文件系统挂载参数也很关键。我建议这样配:
# ext4优化参数
/dev/block/mmcblk0p25 /data ext4 noatime,nodiratime,data=ordered,barrier=0 0 0
# f2fs优化参数
/dev/block/mmcblk0p25 /data f2fs noatime,nodiratime,background_gc=on,inline_xattr,inline_data 0 0
解释一下:noatime和nodiratime能减少大量无意义的元数据写入。barrier=0对ext4来说能提升性能,但牺牲了部分掉电保护——这个取舍你要想清楚。
4.3 I/O调度器调优
I/O调度器负责决定哪个I/O请求先执行。Android内核常用的调度器有:
- CFQ(Completely Fair Queuing):公平调度,适合机械硬盘,对闪存来说太复杂
- Deadline:按截止时间调度,减少I/O延迟,适合实时性要求高的场景
- Noop:先入先出,简单高效,适合闪存这种没有寻道时间的设备
- BFQ(Budget Fair Queuing):基于预算的公平调度,适合多任务场景
我个人建议:对eMMC用Noop,对UFS用Deadline或BFQ。
为什么?eMMC的队列深度浅(通常只有1-2),Noop的简单策略反而最有效。UFS的队列深度深(32甚至更多),Deadline能更好地利用并行能力。
曾经踩过的坑:我在一个项目里用了BFQ,结果发现游戏加载时偶尔卡顿。排查后发现BFQ的公平调度机制让前台应用的I/O请求被后台任务抢了时间片。后来换成Deadline,并调整了fifo_batch参数,问题就解决了。
调度器参数调优示例:
# 查看当前调度器
cat /sys/block/mmcblk0/queue/scheduler
# 切换调度器
echo "deadline" > /sys/block/mmcblk0/queue/scheduler
# 调整Deadline参数
echo 16 > /sys/block/mmcblk0/queue/iosched/fifo_batch
echo 4 > /sys/block/mmcblk0/queue/iosched/writes_starved
fifo_batch控制每次批量处理的请求数,writes_starved控制写请求被饿死的阈值。这些参数需要根据实际负载反复调——没有银弹。
4.4 存储性能测试工具
调优不能靠猜,得用数据说话。我常用的测试工具和场景如下:
| 工具 | 测试类型 | 关键指标 |
|---|---|---|
| fio | 顺序/随机读写、混合读写 | IOPS、带宽、延迟 |
| AndroBench | Android应用级性能 | SQLite插入速度、文件拷贝速度 |
| dd + hdparm | 快速验证 | 读写速率 |
| Perfetto | I/O事件追踪 | I/O等待时间、队列深度 |
fio的典型用法:
# 随机4K读取测试
fio --name=randread --ioengine=libaio --iodepth=64 --rw=randread \
--bs=4k --direct=1 --size=1G --numjobs=4 --runtime=60 \
--group_reporting
# 顺序写入测试
fio --name=seqwrite --ioengine=libaio --iodepth=32 --rw=write \
--bs=1M --direct=1 --size=2G --numjobs=2 --runtime=60 \
--group_reporting
注意--direct=1这个参数,它绕过页缓存,直接测试硬件性能。如果你要测真实应用场景,可以去掉它。
一个小技巧:测试时一定要用--size指定足够大的文件(至少1GB),否则数据会被缓存影响,结果不准确。我见过有人用100MB文件测,结果读带宽飙到2GB/s——嗯,那全是缓存命中。
最后说一句:I/O调优是个持续的过程。你调完参数、换完文件系统,一定要跑一轮完整的测试,包括长时间压力测试和低电量场景测试。我曾经在某个项目上,f2fs在低电量下性能骤降,查了半天发现是内核的电源管理策略把闪存频率降了——你看,又是跨层的问题。
好了,这一章的内容就到这里。下一章我们会深入讲内存调优,包括LMK、ZRAM、内存碎片化这些实战内容。到时候见。