Android性能优化:分析数据库事务性能瓶颈与优化方案
10:59 am
Tuesday, 1 November 2022 (HKT)
Time in Hong Kong
背景
我们的设备经过性能测试,在IO测试阶段、数据库操作部分的跑分显著与对比机拉开差距,且跑分结果怪异:常规的串行读写、随机读写与对比机成绩差距细微,但是数据库操作居然差距巨大。
目标
从表中可以看到,两者常规IO成绩接近,但是数据库事务差距一倍以上。我们需要分析跑分差异的原因,并定位设备和系统的性能瓶颈。
- 分析跑分差异根因;为什么数据库读写会意外地落差巨大?
- 定位性能瓶颈;性能瓶颈是来自存储吗?
环境、工具
环境
设备 | 软件环境 | 硬件环境 | 跑分手法 |
---|---|---|---|
MT8183 | Android 10 + 不加后台负载 | MT8183 + 4GB LPDDR4X + mmc | AndroBench V5.0.1 测试data分区,数据库事务size=1; 启用Index Usage; Journal Mode=WAL; |
对比机 QCOM 660 AIE | Android 9 + 不加后台负载 | QCOM 660 AIE + 4GB LPDDR4 | 同上 |
注意,IO测试与存储芯片有相关性,这里用mmc来表示,忽略具体的存储芯片型号和性能。
具体原因是:1. 没有取得机器们的存储芯片的准确的型号和性能参数;2. 两者在Antutu BenchMark和AndroBench的串行读写、随机读写的成绩几乎一致——因此可以认为两个机器的存储性能接近(起码是跑分场景的性能接近),所以忽略存储芯片差异,不影响分析。
工具
工具 | 目的 | 使用 |
---|---|---|
AndroBench V5.0.1 | 数据库跑分 | |
strace | 统计系统调用耗时 | strace -qc -pPID |
top | 查看线程CPU、Per CPU Usage | busybox top -d1 转H、转1 |
top | 查看线程状态 | top -m15 -H -pPID |
iostat | 看IO信息 | busybox iostat -cdtzm 1 |
TraceView | 看Trace | |
taskset | 对跑分软件绑核 | taskset -ap MASK PID |
drop_caches | 强制系统丢弃cache | echo 1 > /proc/sys/vm/drop_caches |
sync | 写入脏buffer到磁盘 | sync |
CPU定频 | 让CPU运行在最高频 | 链接 |
观测
本节通过一些观测手法来尝试分析和定位性能瓶颈。
1. Trace上看瓶颈是存储
首先再次在机器上跑一次AndroBench数据库测试,并抓一次trace。
可以看到sys_fdatasync
耗时很长,整个数据库事务提交过程绝大部分在fdatasync()
。乍一看似乎瓶颈是存储性能,但由于trace机制粒度较粗,细节较少,其实目前还没有足够的证据证实。Anyway,先继续分析,暂时不做出性能瓶颈是存储的结论。
Trace上有很多层的
sys_fdatasync
,虽然它记录的耗时很长但是不一定能真实反应data sync耗时真的很长。实际上很可能是对fdatasync trace end的调用放到了整个过程的后面,即实际上sync工作完成之后一段时间才结束这个tag的trace,因而data sync的记录很长,且记录的耗时可能远远高于实际耗时。
小结1:Trace目前没有可靠地提示一些信息
小结2: 根据Trace的实现机制,认为该Trace记录可能不准确
1.1 检验fdatasync是否真的耗时
从Linux man可以查阅到,fdatasync()
和fsync()
都是会阻塞直到数据完成写入的。那么我们可以通过检查IO线程的挂起状态的占比即可确认线程是否在等待IO时消耗了过长的时间。
The call blocks until the device reports that the transfer has completed。
从trace可以看到,线程在IO等待方面耗时很小。
- 小结:IO flush操作耗时不是性能瓶颈
2. 数据库测试没有跑满IO极限
从上面的分析来看,并没有严丝合缝的证据表明数据库事务测试碰到了IO瓶颈。本节通过一些方法检验该机器的数据库测试性能瓶颈不是IO,并设计一些实验来进行一些额外的观测。
与顺序/随机读写测试对比而言,数据库CRUD的特点是QPS高,但是数据量低。表现在IO上,前者消耗更多的IO资源,而后者通常需要高并发来取得高IO占用。
顺序/随机读写测试,消耗很低的CPU,触及IO极限:
- 上图结果可见,该机器的IO极限约90附近。CPU消耗低且处于IO wait。下图为数据库跑分阶段:
数据库CRUD测试远远达不到IO极限(仅峰值的10%+),但是需要付出巨大的CPU代价。注意,CPU消耗占比中,USER和SYS占比上升几倍,而IO wait接近可以忽略,表明此时CPU是“真忙”,而不是在等待IO中的“假忙”。
小结:数据库事务测试没有跑到IO极限,且针对该设备,数据库CRUD是计算资源敏感型
通常来说数据库性能除了与硬件相关,还与数据库库表设计、磁盘缓冲、内存操作容量等因素相关。但是在IO性能测试场景,由于和对比机使用的测试工具是一致的,因此需要忽略数据库的设计、操作方法、缓存方案、并发数、连接数,而重点关注其他的具有差异的因素对数据库性能的影响。如下:
- 系统缓存策略
- CPU性能
- 存储性能
由于在数据库事务测试中发现没有跑满IO,因此尝试伸缩IO资源来检验性能瓶颈。
2.1 系统缓冲区对dd性能的影响
Linux系统IO实现是分层的,一些上层的测试方法可能因缓冲区策略、大小不同而出现不同的测试成绩。本小结测试缓冲区对dd的性能影响。
iostat
输出的数据实际上是磁盘真实IO速度,而不受缓冲区策略的影响
每次测试前,清空buffer,丢弃cache:
1 | /data/local/tmp # free -k && sync && echo 1 > /proc/sys/vm/drop_caches && free -k |
- 带缓冲区:
1 | time dd if=/dev/zero of=test bs=1k count=4096000 && time sync |
1 | Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn |
- 不带缓冲区
1 | time dd if=/dev/zero of=test bs=1k count=4096000 conv=fsync && time sync |
1 | Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn |
可见,缓冲区确实有一定的IO性能影响,但是测试过程中dd数据量较大,能够覆盖buffer并触发磁盘flush,因此不论带不带缓冲,两种dd测试都触及了磁盘IO峰值性能。
iostat统计磁盘的实际IO,不受buffer影响
2.2 伸缩IO资源发现对数据库测试成绩影响有限
设计一个实验来降低系统分配给数据库测试工具的IO资源。假设该实验中数据库测试成绩变化有限,可以一定程度地说明数据库CRUD性能测试的瓶颈与IO性能关联不大。
通过后台dd占用不同程度的IO资源的方式实现伸缩:
1 | function command0() { |
这将产生一定的IO压力:
1 | Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn |
在这套脚本运行时,启动数据库测试,得出跑分成绩仅仅只比空负载时降低了16%左右。如果IO性能是数据库跑分瓶颈的话,那么此时跑分成绩应该显著下降。
- 小结:通过IO加压的方式缩减了能够分配给数据库测试工具的IO资源,但是跑分变化远低于瓶颈预期值。表明IO性能目前不是性能瓶颈。
3. CPU资源制约数据库跑分
排除数据库设计、使用方案的影响,也排除掉IO性能的影响,还有一个瓶颈嫌疑是CPU性能。
设计一个实验来伸缩CPU资源。如果跑分对CPU资源的变化很敏感、显著地影响了数据库成绩,那么即可确定瓶颈。
通过削减数据库跑分软件的CPU资源来实现CPU资源的控制,主要采取数据库跑分线程绑定到专用核心,并在该核心上附加一定的压力来实现。
- 首先在默认情况下的跑分,可见此时跑分线程会随机地在任意的CPU上运行
注:top1就是数据库跑分线程,top2/3分别是跑分应用的UI线程和渲染线程,它们用于在界面上显示跑分进度条。从数据上看,它们本身也占有一些CPU资源。
- 将跑分线程绑定到CPU0~3,将其他线程绑定到4~7
可见,跑分线程只会在CPU0~3中调度。最终测试成绩与默认情况一致,没有差异。
- 将跑分线程和其他线程都绑定到CPU7
让跑分线程绑定到CPU7,并因CPU7调度了其他线程而损失一些CPU资源。(本例主要是被跑分工具自己的UI线程和渲染线程分走)
可以看到,三个线程全部在CPU7上运行。跑分结果中,成绩较默认情况下降30%,证明该跑分在该设备上是CPU资源敏感型。
- 将跑分线程和其他所有线程都绑定到CPU3
与“3”相同,但是绑定CPU3。
成绩较默认下降50%左右。
- 各场景梳理:
场景(SQLite Delete) | 成绩(QPS of SQLite Delete) | 说明 |
---|---|---|
默认场景 | 849 | |
后台dd命令制造峰值IO速率30%左右的IO压力 | 790 | 成绩略微下降 |
允许跑分线程在CPU0-3调度,其他为4-7调度 | 833 | CPU资源几乎没有差异;成绩几乎没有差异 |
跑分线程和UI线程+渲染线程都绑定到CPU7 | 532 | 成绩下降~36% |
都绑定到CPU3 | 392 | 成绩下降~52% |
小结:该机器在数据库跑分场景的性能瓶颈为CPU性能。
绑定到不同核心得出的跑分成绩: 大小核性能差异
可以看到绑定到不同CPU会产生比较显著的跑分成绩差异,实际上是CPU的大小核架构中大核和小核之间的性能差异。
cat /proc/cpuinfo
可知该机器有两种架构的CPU核。其中CPU part
字段代表不同的架构:
1 | ... ... ... ... |
查阅Kernel cputype.h
可知CPU3为0xd03即小核(Contex-A53),CPU7为0xd09即大核(Contex-A57)。大小核之间的性能差异使之在绑定核心后跑出了不同的分数。
1 |
定频带来跑分提升: 强制提高CPU频率
经过测试发现CPU资源下降将显著影响跑分成绩,表明性能瓶颈在CPU。由于在默认情况下CPU在数据库跑分场景可能没有以最高性能运行,因此通过将CPU频率固定到最高频率进行跑分测试。
固定到CPU最高频率运行:
1 | find /proc/cpufreq/MT_CPU_DVFS_*/cpufreq_oppidx | xargs cat |
场景(SQLite Delete) | 成绩(QPS of SQLite Delete) | 说明 |
---|---|---|
将大核、小核均固定到最高频率 | 1417.1 | 提升~70% |
将大核固定到最高频率 | 1233.2 | 提升~48% |
将小核固定到最高频率 | 1103.3 | 提升~33% |
将大核关闭、小核仅开两颗核心并固定到最低频率 | 253.4 | 下降~69% |
Info:这里为了测试选取固定到最高频率,对于复杂的实际场景,不能笼统地认为最高频率=最佳性能/最好的用户体验。
- 小结:可以看到,在该数据库测试方法测试情况下,该平台的性能瓶颈为CPU。
结论
对于该设备,AndroBench V5.0.1
的数据库性能测试方法中,跑分瓶颈是CPU性能,跑分成绩是CPU敏感的。
优化方案:提升CPU单核性能。