27、升级性能优化:升级速度优化、存储I/O优化、并行升级策略
说实话,做系统升级最怕什么?最怕用户等得不耐烦。我见过不少项目,功能做得再花哨,升级慢个十分钟,用户口碑直接崩了。今天咱们就聊聊怎么把升级速度提上去,让用户感觉「嗯,这系统升级还挺快」。
升级速度优化的核心思路
升级速度的瓶颈,说白了就两个:一个是CPU算力,一个是存储I/O。CPU算力我们很难改,但存储I/O这块,大有文章可做。
我个人习惯把升级过程拆成三个阶段:
- 数据读取阶段:从存储介质读取升级包
- 数据处理阶段:解压、校验、解析脚本
- 数据写入阶段:把新数据写入目标分区
你想想看,这三个阶段里,最慢的是哪个?我做过实测,在eMMC设备上,写入阶段占了总时间的60%以上。所以优化I/O,就是抓住了主要矛盾。
核心原则:升级速度优化的本质,是让CPU和I/O尽量并行工作,减少空闲等待。
存储I/O优化:从块设备到文件系统
我在项目中遇到过一个问题:同样的升级包,在A设备上跑15分钟,在B设备上要跑25分钟。查了半天,发现是文件系统的差异。A设备用的是F2FS,B设备用的是ext4。同样的写入模式,F2FS的碎片化程度更低,I/O性能更好。
具体到优化手段,我总结了几个实用的:
1. 调整I/O调度器
Android默认的I/O调度器是CFQ(完全公平队列),它对普通应用很友好,但对升级这种大块连续写入的场景,反而有性能损失。我建议在升级过程中临时切换到noop调度器:
# 查看当前调度器
cat /sys/block/mmcblk0/queue/scheduler
# 临时切换到noop
echo noop > /sys/block/mmcblk0/queue/scheduler
# 升级完成后恢复
echo cfq > /sys/block/mmcblk0/queue/scheduler
为什么?因为noop调度器不做任何合并和排序,直接下发请求。对于升级这种顺序写入的场景,反而减少了调度开销。
2. 增大I/O请求大小
默认的I/O请求大小通常是4KB,但升级时我们完全可以一次请求128KB甚至更大。这样能减少系统调用次数,提高吞吐量。
// 在升级代码中设置
#define IO_BLOCK_SIZE (128 * 1024) // 128KB
ssize_t ret = write(fd, buffer, IO_BLOCK_SIZE);
if (ret != IO_BLOCK_SIZE) {
// 处理错误
}
小技巧:我习惯在升级前先做一次文件系统碎片整理。对于F2FS,可以用fstrim命令。虽然会多花几秒钟,但后续的写入速度能提升20%以上。
3. 使用O_SYNC的时机
很多开发者习惯在每次写入后调用fsync,觉得这样更安全。其实大可不必。升级过程中,数据是分块写入的,只要保证最后一块写入后做一次sync就够了。中间频繁sync,反而让I/O性能大打折扣。
// 不推荐:每次写入都sync
for (int i = 0; i < total_blocks; i++) {
write(fd, block, size);
fsync(fd); // 这行是性能杀手
}
// 推荐:只在最后sync一次
for (int i = 0; i < total_blocks; i++) {
write(fd, block, size);
}
fsync(fd); // 一次搞定
并行升级策略:让CPU和I/O都忙起来
传统的升级方式是串行的:先读A分区,写A分区;再读B分区,写B分区。这就像一个人既当厨师又当服务员,效率很低。
并行升级的思路是:把升级任务拆成多个独立的小任务,让CPU和I/O同时工作。
1. 分区级并行
如果设备有多个物理分区(比如system、vendor、product),而且它们位于不同的块设备上,那就可以并行升级。我做过一个项目,把system和vendor的升级线程分开,总时间从12分钟降到了8分钟。
// 伪代码:分区级并行
pthread_t thread1, thread2;
// 线程1:升级system分区
pthread_create(&thread1, NULL, upgrade_system, NULL);
// 线程2:升级vendor分区
pthread_create(&thread2, NULL, upgrade_vendor, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
注意:并行升级时一定要处理好依赖关系。比如vendor分区依赖system分区中的某些库,那就不能并行。我建议在升级脚本中显式声明依赖关系,避免踩坑。
2. 流水线并行
如果分区之间有依赖,不能完全并行,那可以用流水线的方式。比如:
- 阶段1:读取升级包数据(I/O密集型)
- 阶段2:解压和校验数据(CPU密集型)
- 阶段3:写入目标分区(I/O密集型)
这三个阶段可以流水线化:阶段1读一块数据,交给阶段2处理,同时阶段1继续读下一块。这样CPU和I/O就能重叠工作。
// 流水线并行:使用双缓冲
#define BUFFER_COUNT 2
char buffers[BUFFER_COUNT][BLOCK_SIZE];
int current_buffer = 0;
// 生产者线程:读取数据
void* reader_thread(void* arg) {
while (has_more_data()) {
read_data(buffers[current_buffer]);
current_buffer = (current_buffer + 1) % BUFFER_COUNT;
}
}
// 消费者线程:处理并写入
void* writer_thread(void* arg) {
while (has_more_data()) {
process_data(buffers[current_buffer]);
write_data(buffers[current_buffer]);
current_buffer = (current_buffer + 1) % BUFFER_COUNT;
}
}
知识体系图:升级性能优化全景
实战中的避坑指南
我曾经在一个项目里踩过一个坑:并行升级时,两个线程同时写同一个块设备的不同分区,结果I/O性能反而下降了。后来发现是块设备的队列深度不够,两个线程互相争抢资源。
解决方案是:如果多个分区在同一个块设备上,就不要用分区级并行,改用流水线并行。如果一定要用分区级并行,那就确保每个分区在不同的块设备上(比如一个在eMMC,一个在UFS)。
我的建议:在项目初期,先做一次I/O性能摸底。用fio工具测一下设备的顺序读写和随机读写性能,然后根据结果选择优化策略。别等到上线了才发现性能瓶颈。
总结
升级性能优化,说白了就是让硬件资源物尽其用。I/O调度器、请求大小、sync策略这些细节,看似不起眼,但加起来就是几分钟的差距。并行策略更是锦上添花,但要注意依赖关系和资源争抢。嗯,这些经验都是我在项目里一点点试出来的,希望对你有帮助。
公众号:蓝海资料掘金营,微信deep3321