【全网首发】手机Android存储性能优化架构分析原创
作者简介
李晓辉(微信名:小辉),一线码农,有多年丰富嵌入式操作系统开发工作经验。先后在南大富士通,某手机大厂和初创小公司任软件工程师,系统架构师等职务。对嵌入式系统开发和os性能优化工作很熟悉。
引子
怎么能够找到手机存储性能优化方面的技术特性,怎么去凸显手机存储里面碰到的独特性能优化问题,有了这些需求,所以我需要写一点东西了。
因为工作繁忙,只好写成了类似bbs和笔记的风格,不过虽然这样,我保证下面文字是我对手机存储性能架构方面的原创心得,网上根本搜不到下面这样的文字。
手机安卓系统IO特性
业务方面特性
手机存储对应的都是单机存储架构。出存储性能问题时,往往直观的表现是桌面运行卡顿,需要分析用户上传的bugreport,然后自己根据用户场景做问题复现,然后通过分析内核ftrace log, 还有借助一些性能排查工具去解决该性能问题。
技术方面特性
1: buffer IO很多,异步IO,direct IO很少
手机中异步IO,direct IO操作应该很少,基本上都是buffer IO(经过page cache的IO),所以出存储性能问题时,寻找根本原因时,
往往是内存原因和IO原因交织混杂在一块,表现出来的原因,并不是孤立的IO模块的原因。
内存原因有内存回收,内存slow path分配,IO原因有具体的文件系统,还有块设备层IO调度器。
2: sqlite数据库引发的IO操作很多
这个是手机系统的一个很重要的IO特色,详见三星公司出的那篇经典Android IO特性分析论文。
Android 手机里面大部分写都是数据库sqlite的单笔小量数据的频繁同步随机写(就是write+fsync),而不是服务器场景中出现的文件写。
3:手机里面大部分都是中低速存储芯片,比如emmc, ufs。所以内核层的over head不高,需要利用到内核的文件系统,还有IO调度器
这个涉及到内核存储架构问题了。
1)现有的存储系统,在分布式和大数据应用场景下,通常会用些高速存储芯片(比如nvme接口的),内核现有的存储架构不能有效的发挥作用。
具体表现在每一笔的IO下发,在内核存储栈产生的over head太重了,并且还不能充分地利用存储芯片的IO带宽。
尤其是块设备层那些传统的single queue设计,IO 请求排序和合并,cfq调度器,传统文件系统自身局限性等这些内核的设计思想,都不符合高速存储的需求。
所以现有的分布式和大数据存储场景,都是会bypass 内核,直接做成用户态driver去直接读写存储芯片的,比如现在的dpdk, spdk架构。
所以现在内核也被迫的调整存储架构,io_urging, 块设备层的blk-mq多队列设计,bfq调度器等这些存储架构的调整都是为了跟上当前硬件上,存储芯片性能不断暴涨的节奏。
2)但是手机场景是比较特殊的,因为用的还是emmc,ufs这种存储芯片速度不是特别快的场景,再加上每天的实际存储数据写入耗时占每天总时长的比例是比较小的(sandisk有存储芯片打点数据分析的),
再加上上面的2点IO特性,所以手机场景下,还是会用到内核传统的文件系统,块设备层设计思想,会用到这些传统的内核存储架构。
4:会频繁异常掉电,所以对文件系统稳定性要求高
服务器场景会有ups电源,可能很少会有系统频繁掉电的场景出现。
但是手机场景,据我们分析,就算用户选择的是正常关机流程,也会很多时候出现因为文件系统卸载超时导致的实际上走的也是类似于这种异常掉电流程
(就是虽然不是异常掉电,但是走的流程,还有从对存储数据稳定性影响方面评估,跟异常掉电场景区别不大)。
所以就更不用提很多时候,用户关不了机时(系统按键无响应时),往往采取的强制掉电关机或者掉电重启。
5:会有前后台任务管理方面的工作需求
因为安卓手机通常后台跑的做IO工作的进程数量是很多的,同时用户在前台操作手机时,前台的任务不希望被后台任务所干扰。
随着内存越来越大,还有安卓本来就是多任务系统,所以安卓后台跑的进程数量是比较多的。但是用户在前台操作app或者图形界面时,是不希望受到后台进程的干扰影响的。
6:手机会出现存储碎片化厉害的场景,这样对IO性能会影响比较大
手机里面大部分都是存储的小文件,但是手机存储容量往往由于成本和具体应用原因,不会容量像大数据或者分布式的存储容量那么大。
而且随着手机应用场景越来越丰富,比如视频,聊天,当做电脑一样在应用手机,所以手机本来存储容量就不大,再加上使用时间长和强度高后,存储碎片化现象肯定越来越严重(具体表现在文件系统碎片化)。
这样对IO性能会有大的影响的。
7:会有ext4文件系统的自身局限性带来的一些问题
设计ext4的时候,当时都是机械硬盘为主,内存资源不充足的时代,现在的时代是以flash为主,手机已经稍微迈入了大内存时代。
ext4的局限性:
1)原地更新数据特性,会加重存储芯片里面的IO写放大问题。
2)ext4+jbd2的journal架构设计。
这个设计虽然是为了实现文件系统日志功能,它采用了类似c语言式的简单直观的设计思想(ext4社区ted有相关邮件说明),同时采取physical logging,也是为了追求复用内存,节省内存。
但是这种类c语言的设计思想,虽然当初设计时,可以做到简单高效。但是不好的地方在于:
1> 随着多核处理器架构的普及,这种设计思想带来的性能瓶颈问题越来越厉害。
2> 这种设计思想容易造成内核的IO优先级倒置问题(具体见下面的fsync慢原因分析)。
3)是针对机械硬盘进行设计的,没有突出flash - aware.
综合这些ext4自身局限性,造成了对手机IO性能的不良影响,所以 f2fs出现了。
8:会结合emmc/ufs存储芯片里面的ftl固件实现思想去定位性能问题
1)因为解决的是单机存储性能问题,所以想要精确评估性能优化方案会对手机带来多大的性能提升,需要对存储芯片里面的固件工作原理,工作思想有所了解。
2)手机换成了f2fs文件系统后,因为f2fs文件系统的设计初衷就是flash - aware,所以更需要了解存储芯片里面的固件后,才能准确把握和ext4相比,f2fs带来的性能优化效果。
手机存在的存储性能问题
针对上面罗列的Android系统的一些IO特性,手机存在的存储性能问题如下
1:针对特性1,2,3和5,会有ext4的fsync问题
原因在于下面几点:
1)Android系统里面sqlite写数据时,sqlite自身的日志架构设计,会导致频繁下发fsync。这样后台fsync下发多了,前台或者system server里面下发fsync就会受到影响(fsync耗时会增加很多),
2)同时手机安卓系统还有一些原生特性,就是jbd2工作在order模式下的, 同时又开启了ext4 延迟分配功能,这样也会加重fsync耗时。
1> order模式加重耗时原因:
order模式下,jbd2不得不去在commit thread里面多了个等待脏iNode的数据全部刷到盘上的工作。所以jbd2 commit thread耗时增加,会带来fsync的耗时也会增加不少,
因为fsync最后一步是要等待自身的事务被commit thread处理完成。
2> 内核存储IO优先级倒置加重耗时原因:
CFQ I/O调度器在调度io请求时,会优先去调度同步的io请求,这样会导致异步io请求的处理时间变长。应用调用fsync的时候,fsync自身下发的IO请求都是同步的,这是从存储设计角度,为了追求提升fsync性能。
但是系统如果刚好正在进行内存脏页回写,这个时候fsync需要等待脏页回写对应的异步io请求完成后,fsync的工作才能算做完。这样就会导致fsync耗时。
3> 延迟分配加重耗时原因:
延迟分配会加重单次fsync的负担,因为系统flush thread是每隔30秒触发的(Android系统里面),
然后flush thread 在前,fsync在后的话,容易fsync卡在order模式中的等待file data flush到disk中 这个地方,等很久
(因为之前的flush thread里面已经为脏的iNode分配了很多物理blocks,并且把这些脏iNode(每隔30s去触发flush thread的话,是会产生很多脏iNode的)注册到了jbd2的transaction里面,
jbd2里面多了个额外等待这些脏iNode里面脏数据被刷到磁盘上的操作)。
flush thread 在后,fsync在前的话,没有这个问题。
3)ext4的实时discard机制也会造成fsync慢的问题。
这样在手机Android系统里面,由于上面种种原因,造成fsync响应慢,然后就会表现出手机前台操作卡顿或者卡死直接安卓层重启的现象。
2:针对特性6,会有文件系统碎片化导致存储IO性能下降的问题
手机data分区碎片化厉害后,会导致:
1)手机的写性能(顺序写,随机写性能,direct写)会下降的(之前我详细分析过原因的,并挖掘过性能下降的具体场景)。
2)手机的sqlite数据库IO性能也会下降很多的(之前跟存储芯片厂商共同定位过该下降的原因)。
3:针对特性1,3和5,会有前后台IO管理的问题
1)特殊场景出现的问题:后台U盘拷贝时,前台安卓桌面操作卡顿问题出现。
原因:
i:U盘拷贝数据量大时,导致内存里面脏页变多,然后可用free内存变少。
ii:然后前台手机操作时,会alloca_pages,这个时候会触发内存回收,去回收脏页,这样io又变多,阻塞了申请内存的前台用户进程。
2)通用场景出现的问题:后台做的IO任务异常活跃,干扰了前台任务的运行,需要前后台任务进行协作。
原因:
i: Android的IO调度器为cfq,并未区分前后台。这样后台app或者进程正在进行IO工作时,会抢占前台app的IO带宽和内存资源。
现在的核心优化需求是怎么保证用户在前台操作app,或者图形界面时,一直都是流畅的,即使后台跑再多的任务,对前台也没有影响。
4:针对特性4,会有文件系统稳定性问题
存储性能优化会对文件系统做一些深入修改,以前的测试强度还不够。
对文件系统深入修改后,以前的测试case还不能深入进行文件系统测试。
5:针对特性7,存在的问题上面已经做分析了
6:针对特性8,会存在需要存储芯片厂商的支援和配合问题
ftl固件是存储芯片厂商的专利,给我们暴露的存储芯片使用接口太少。他们虽然很配合我们的性能优化工作,但是通常涉及到他们芯片固件实现的一些敏感问题,会回避我们。
需要我们自己去理解ftl的一些通用设计思想,另外联合采购人员,一起去跟存储芯片厂商进行性能问题的沟通和解决。
具体问题场景如下:
1)部署无感垃圾回收优化方案时,需要准确的评估该优化会带来哪些IO性能提升。
2)上面的碎片整理对Android sqlite数据库IO性能的提升的根本原因调研。
现有的性能优化措施
针对上面罗列的手机存在的若干存储性能问题,具体优化工作如下:
问题1的优化:
1)无感垃圾回收优化,是针对上面的原因3
2)ext4 fsync专项优化,是针对上面的原因2
3)sqlite io优化,是针对上面的原因1
上面是结合手机业务场景,做的简单有效的优化方案,最根本最彻底的优化方案是下面的:
4)社区的fast commit方案 (正在开发中,有这一个方案就可以彻底解决问题,不需要上面3个了)。
问题2的优化:
就是目前的ext4碎片整理方案。具体有两个优化措施:
1)compact整理,解决的是上面说的写性能下降的问题。
2)单文件碎片整理,解决的是上面说的sqlite数据IO性能下降的问题。
问题3的优化:
1)针对上面的U盘拷贝导致的性能问题,社区有相关优化方案,具体是对内存回收方面做点优化。
2)针对这个前后台的性能问题,结合手机业务场景方面的优化有IO限速方案。
3)彻底的最根本的优化是前后台分组,即运用cgroup v2分组方案,前台给予比较多的IO和内存资源请求,后台给予少点,目前我有所调研,做过一些优化方案设计。
(cgroup v2对于buffer io会有些优先级倒置问题,部分原因跟那个mmap_sem锁整个进程地址空间的缺陷是相关的,目前尽量在高版本内核上(大于4.14的)做cgroup v2方案)
问题4的优化:
就是需要导入目前的xfstest,以便对稳定性测试方面加大测试强度。
问题5的优化:
针对上面说的ext4文件系统的局限性,需要用f2fs来代替ext4了。
大内存时代,读性能没有写性能那么需要得到工程师的迫切优化,因为内存大,很多文件第一次读完后,就缓存在内存里面了。
往往是写性能会导致出现存储IO性能问题。
所以f2fs针对手机写场景,做的优化如下:
1)对数据库写专门做了优化(sqlite原子写).
2)提出了将随机写转换为顺序写,还有copy on write思想.
3)针对手机存储芯片的一些物理特性,做了冷热数据分离功能.
这样优化措施,可以减少了flash的写放大问题(写放大,会导致用户长期使用手机后,会出现flash坏块增多,会导致存储芯片自身固件做GC时间增多,这样IO性能就会下降了),
可以提升Android的随机写性能,同时又是logical logging,所以没有jbd2那个导致的fsync问题出现。
所有的这些f2fs优化,都是针对手机写场景做的针对性优化,ext4是没有的。
所以为了突破现有的ext4架构上导致的存储性能问题缺陷,所以f2fs在手机场景应该取代ext4。
上f2fs后,有两个性能优化点需要关注:
1)f2fs的gc效率(重点是后台的)
2)f2fs的冷热数据分离效果。
这两点的优化好坏能关系到f2fs的dirty segments数目降低多少。只有dirty segments数目降低了,free segments数目增加的多了,系统的IO性能才能好起来。
问题6的优化:
1)自己可以看这本书:深入浅出ssd,仔细理解该书后,会对ftl的设计思想有所理解。
2)已经解决了上面的问题场景2,问题场景1正在解决中。
2)已经进行了几次存储芯片原厂举办的关于存储芯片内部软硬件实现的培训会议,通过这些会议,已经对ftl固件,还有存储芯片的一些物理特性有所理解。