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优化 并行升级策略 数据读取 数据处理 数据写入 I/O调度器 请求大小 sync策略 分区级并行 流水线并行 依赖管理 目标:减少用户等待时间,提升升级体验 注:三种优化方向可组合使用,效果更佳 速度优化 I/O优化 并行策略

实战中的避坑指南

我曾经在一个项目里踩过一个坑:并行升级时,两个线程同时写同一个块设备的不同分区,结果I/O性能反而下降了。后来发现是块设备的队列深度不够,两个线程互相争抢资源。

解决方案是:如果多个分区在同一个块设备上,就不要用分区级并行,改用流水线并行。如果一定要用分区级并行,那就确保每个分区在不同的块设备上(比如一个在eMMC,一个在UFS)。

我的建议:在项目初期,先做一次I/O性能摸底。用fio工具测一下设备的顺序读写和随机读写性能,然后根据结果选择优化策略。别等到上线了才发现性能瓶颈。

总结

升级性能优化,说白了就是让硬件资源物尽其用。I/O调度器、请求大小、sync策略这些细节,看似不起眼,但加起来就是几分钟的差距。并行策略更是锦上添花,但要注意依赖关系和资源争抢。嗯,这些经验都是我在项目里一点点试出来的,希望对你有帮助。


公众号:蓝海资料掘金营,微信deep3321