09 | I/O优化(上):开发工程师必备的I/O优化知识
下载APP
关闭
渠道合作
推荐作者
09 | I/O优化(上):开发工程师必备的I/O优化知识
2018-12-27 张绍文 来自北京
《Android开发高手课》
课程介绍
讲述:冯永吉
时长16:10大小14.77M
250GB 容量,512MB DDR4 缓存,连续读取不超过 550MB/s,连续写入不超过 520MB/s。
“双十一”在天猫看到一款固态硬盘有上面的这些介绍,这些数字分别代表了什么意思?
在专栏前面卡顿和启动优化里,我也经常提到 I/O 优化。可能很多同学觉得 I/O 优化不就是不在主线程读写大文件吗,真的只有这么简单吗?那你是否考虑过,从应用程序调用 read() 方法,内核和硬件会做什么样的处理,整个流程可能会出现什么问题?今天请你带着这些疑问,我们一起来看看 I/O 优化需要的知识。
I/O 的基本知识
在工作中,我发现很多工程师对 I/O 的认识其实比较模糊,认为 I/O 就是应用程序执行 read()、write() 这样的一些操作,并不清楚这些操作背后的整个流程是怎样的。
我画了一张简图,你可以看到整个文件 I/O 操作由应用程序、文件系统和磁盘共同完成。首先应用程序将 I/O 命令发送给文件系统,然后文件系统会在合适的时机把 I/O 操作发给磁盘。
这就好比 CPU、内存、磁盘三个小伙伴一起完成接力跑,最终跑完的时间很大程度上取决于最慢的小伙伴。我们知道,CPU 和内存相比磁盘是高速设备,整个流程的瓶颈在于磁盘 I/O 的性能。所以很多时候,文件系统性能比磁盘性能更加重要,为了降低磁盘对应用程序的影响,文件系统需要通过各种各样的手段进行优化。那么接下来,我们首先来看文件系统。
1. 文件系统
文件系统,简单来说就是存储和组织数据的方式。比如在 iOS 10.3 系统以后,苹果使用 APFS(Apple File System)替代之前旧的文件系统 HFS+。对于 Android 来说,现在普遍使用的是 Linux 常用的 ext4 文件系统。
关于文件系统还需要多说两句,华为在 EMUI 5.0 以后就使用 F2FS 取代 ext4,Google 也在最新的旗舰手机 Pixel 3 使用了 F2FS 文件系统。Flash-Friendly File System 是三星是专门为 NAND 闪存芯片开发的文件系统,也做了大量针对闪存的优化。根据华为的测试数据,F2FS 文件系统在小文件的随机读写方面比 ext4 更快,例如随机写可以优化 60%,不足之处在于可靠性方面出现过一些问题。我想说的是,随着 Google、华为的投入和规模化使用,F2FS 系统应该是未来 Android 的主流文件系统。
还是回到文件系统的 I/O。应用程序调用 read() 方法,系统会通过中断从用户空间进入内核处理流程,然后经过 VFS(Virtual File System,虚拟文件系统)、具体文件系统、页缓存 Page Cache。下面是 Linux 一个通用的 I/O 架构模型。
虚拟文件系统(VFS)。它主要用于实现屏蔽具体的文件系统,为应用程序的操作提供一个统一的接口。这样保证就算厂商把文件系统从 ext4 切换到 F2FS,应用程序也不用做任何修改。
文件系统(File System)。ext4、F2FS 都是具体文件系统实现,文件元数据如何组织、目录和索引结构如何设计、怎么分配和清理数据,这些都是设计一个文件系统必须要考虑的。每个文件系统都有适合自己的应用场景,我们不能说 F2FS 就一定比 ext4 要好。F2FS 在连续读取大文件上并没有优势,而且会占用更大的空间。只是对一般应用程序来说,随机 I/O 会更加频繁,特别是在启动的场景。你可以在 /proc/filesystems 看到系统可以识别的所有文件系统的列表。
页缓存(Page Cache)。在启动优化中我已经讲过 Page Cache 这个概念了,在读文件的时候会,先看它是不是已经在 Page Cache 中,如果命中就不会去读取磁盘。在 Linux 2.4.10 之前还有一个单独的 Buffer Cache,后来它也合并到 Page Cache 中的 Buffer Page 了。
具体来说,Page Cache 就像是我们经常使用的数据缓存,是文件系统对数据的缓存,目的是提升内存命中率。Buffer Cache 就像我们经常使用的 BufferInputStream,是磁盘对数据的缓存,目的是合并部分文件系统的 I/O 请求、降低磁盘 I/O 的次数。需要注意的是,它们既会用在读请求中,也会用到写请求中。
通过 /proc/meminfo 文件可以查看缓存的内存占用情况,当手机内存不足的时候,系统会回收它们的内存,这样整体 I/O 的性能就会有所降低。
2. 磁盘
磁盘指的是系统的存储设备,就像小时候我们常听的 CD 或者电脑使用的机械硬盘,当然还有现在比较流行的 SSD 固态硬盘。
正如我上面所说,如果发现应用程序要 read() 的数据没有在页缓存中,这时候就需要真正向磁盘发起 I/O 请求。这个过程要先经过内核的通用块层、I/O 调度层、设备驱动层,最后才会交给具体的硬件设备处理。
通用块层。系统中能够随机访问固定大小数据块(block)的设备称为块设备,CD、硬盘和 SSD 这些都属于块设备。通用块层主要作用是接收上层发出的磁盘请求,并最终发出 I/O 请求。它跟 VFS 的作用类似,让上层不需要关心底层硬件设备的具体实现。
I/O 调度层。磁盘 I/O 那么慢,为了降低真正的磁盘 I/O,我们不能接收到磁盘请求就立刻交给驱动层处理。所以我们增加了 I/O 调度层,它会根据设置的调度算法对请求合并和排序。这里比较关键的参数有两个,一个是队列长度,一个是具体的调度算法。我们可以通过下面的文件可以查看对应块设备的队列长度和使用的调度算法。
块设备驱动层。块设备驱动层根据具体的物理设备,选择对应的驱动程序通过操控硬件设备完成最终的 I/O 请求。例如光盘是靠激光在表面烧录存储、闪存是靠电子擦写存储数据。
Android I/O
前面讲了 Linux I/O 相关的一些知识,现在我们再来讲讲 Android I/O 相关的一些知识。
1. Android 闪存
我们先来简单讲讲手机使用的存储设备,手机使用闪存作为存储设备,也就是我们常说的 ROM。
考虑到体积和功耗,我们肯定不能直接把 PC 的 SSD 方案用在手机上面。Android 手机前几年通常使用 eMMC 标准,近年来通常会采用性能更好的 UFS 2.0/2.1 标准,之前沸沸扬扬的某厂商“闪存门”事件就是因为使用 eMMC 闪存替换了宣传中的 UFS 闪存。而苹果依然坚持独立自主的道路,在 2015 年就在 iPhone 6s 上就引入了 MacBook 上备受好评的 NVMe 协议。
最近几年移动硬件的发展非常妖孽,手机存储也朝着体积更小、功耗更低、速度更快、容量更大的方向狂奔。iPhone XS 的容量已经达到 512GB,连续读取速度可以超过 1GB/s,已经比很多的 SSD 固态硬盘还要快,同时也大大缩小了和内存的速度差距。不过这些都是厂商提供的一些测试数据,特别是对于随机读写的性能相比内存还是差了很多。
上面的数字好像有点抽象,直白地说闪存的性能会影响我们打开微信、游戏加载以及连续自拍的速度。当然闪存性能不仅仅由硬件决定,它跟采用的标准、文件系统的实现也有很大的关系。
2. 两个疑问
看到这里可能有些同学会问,知道文件读写的流程、文件系统和磁盘这些基础知识,对我们实际开发有什么作用呢?下面我举两个简单的例子,可能你平时也思考过,不过如果不熟悉 I/O 的内部机制,你肯定是一知半解。
疑问一:文件为什么会损坏?
先说两个客观数据,微信聊天记录使用的 SQLite 数据库大概有几万分之一的损坏率,系统 SharedPreference 如果频繁跨进程读写也会有万分之一的损坏率。
在回答文件为什么会损坏前,首先需要先明确一下什么是文件损坏。一个文件的格式或者内容,如果没有按照应用程序写入时的结果都属于文件损坏。它不只是文件格式错误,文件内容丢失可能才是最常出现的,SharedPreference 跨进程读写就非常容易出现数据丢失的情况。
再来探讨文件为什么会损坏,我们可以从应用程序、文件系统和磁盘三个角度来审视这个问题。
应用程序。大部分的 I/O 方法都不是原子操作,文件的跨进程或者多线程写入、使用一个已经关闭的文件描述符 fd 来操作文件,它们都有可能导致数据被覆盖或者删除。事实上,大部分的文件损坏都是因为应用程序代码设计考虑不当导致的,并不是文件系统或者磁盘的问题。
文件系统。虽说内核崩溃或者系统突然断电都有可能导致文件系统损坏,不过文件系统也做了很多的保护措施。例如 system 分区保证只读不可写,增加异常检查和恢复机制,ext4 的 fsck、f2fs 的 fsck.f2fs 和 checkpoint 机制等。
在文件系统这一层,更多是因为断电而导致的写入丢失。为了提升 I/O 性能,文件系统把数据写入到 Page Cache 中,然后等待合适的时机才会真正的写入磁盘。当然我们也可以通过 fsync、msync 这些接口强制写入磁盘,在下一其我会详细介绍直接 I/O 和缓存 I/O。
磁盘。手机上使用的闪存是电子式的存储设备,所以在资料传输过程可能会发生电子遗失等现象导致数据错误。不过闪存也会使用 ECC、多级编码等多种方式增加数据的可靠性,一般来说出现这种情况的可能性也比较小。
闪存寿命也可能会导致数据错误,由于闪存的内部结构和特征,导致它写过的地址必须擦除才能再次写入,而每个块擦除又有次数限制,次数限制是根据采用的存储颗粒,从十万次到几千都有(SLC>MLC>TLC)。
下图是闪存(Flash Memory)的结构图,其中比较重要的是 FTL(Flash Translation Layer),它负责物理地址的分配和管理。它需要考虑到每个块的擦除寿命,将擦除次数均衡到所有块上去。当某个块空间不够的时候,它还要通过垃圾回收算法将数据迁移。FTL 决定了闪存的使用寿命、性能和可靠性,是闪存技术中最为重要的核心技术之一。
对于手机来说,假设我们的存储大小是 128GB,即使闪存的最大擦除次数只有 1000 次,那也可以写入 128TB,但一般来说比较难达到。
疑问二:I/O 有时候为什么会突然很慢?
手机厂商的数据通常都是出厂数据,我们在使用 Android 手机的时候也会发现,刚买的时候“如丝般顺滑”的手机,在使用一年之后就会变得卡顿无比。
这是为什么呢?在一些低端机上面,我发现大量跟 I/O 相关的卡顿。I/O 有时候为什么会突然变慢,可能有下面几个原因。
内存不足。当手机内存不足的时候,系统会回收 Page Cache 和 Buffer Cache 的内存,大部分的写操作会直接落盘,导致性能低下。
写入放大。上面我说到闪存重复写入需要先进行擦除操作,但这个擦除操作的基本单元是 block 块,一个 page 页的写入操作将会引起整个块数据的迁移,这就是典型的写入放大现象。低端机或者使用比较久的设备,由于磁盘碎片多、剩余空间少,非常容易出现写入放大的现象。具体来说,闪存读操作最快,在 20us 左右。写操作慢于读操作,在 200us 左右。而擦除操作非常耗时,在 1ms 左右的数量级。当出现写入放大时,因为涉及移动数据,这个时间会更长。
由于低端机的 CPU 和闪存的性能相对也较差,在高负载的情况下容易出现瓶颈。例如 eMMC 闪存不支持读写并发,当出现写入放大现象时,读操作也会受影响。
系统为了缓解磁盘碎片问题,可以引入 fstrim/TRIM 机制,在锁屏、充电等一些时机会触发磁盘碎片整理。
I/O 的性能评估
正如下图你所看到的,整个 I/O 的流程涉及的链路非常长。我们在应用程序中通过打点,发现一个文件读取需要 300ms。但是下面每一层可能都有自己的策略和调度算法,因此很难真正的得到每一层的耗时。
在前面的启动优化内容中,我讲过 Facebook 和支付宝采用编译单独 ROM 的方法来评估 I/O 性能。这是一个比较复杂但是有效的做法,我们可以通过定制源码,选择打开感兴趣的日志来追踪 I/O 的性能。
1. I/O 性能指标
I/O 性能评估中最为核心的指标是吞吐量和 IOPS。今天文章开头所说的,“连续读取不超过 550MB/s,连续写入不超过 520MB/s”,就指的是 I/O 吞吐量。
还有一个比较重要的指标是 IOPS,它指的是每秒可以读写的次数。对于随机读写频繁的应用,例如大量的小文件存储,IOPS 是关键的衡量指标。
2. I/O 测量
如果不采用定制源码的方式,还有哪些方法可以用来测量 I/O 的性能呢?
第一种方法:使用 proc。
总的来说,I/O 性能会跟很多因素有关,是读还是写、是否是连续、I/O 大小等。另外一个对 I/O 性能影响比较大的因素是负载,I/O 性能会随着负载的增加而降低,我们可以通过 I/O 的等待时间和次数来衡量。
如果是 root 的机器,我们可以开启内核的 I/O 监控,将所有 block 读写 dump 到日志文件中,这样可以通过 dmesg 命令来查看。
第二种方法:使用 strace。
Linux 提供了 iostat、iotop 等一些相关的命令,不过大部分 Anroid 设备都不支持。我们可以通过 strace 来跟踪 I/O 相关的系统调用次数和耗时。
通过上面的日志,你可以看到应用程序在读取文件操作符为 53 的文件,每次读取 1024 个字节。第一次读取花了 447us,后面两次都使用了 100us 不到。这跟启动优化提到的“数据重排”是一个原因,文件系统每次读取以 block 为单位,而 block 的大小一般是 4KB,后面两次的读取是从页缓存得到。
我们也可以通过 strace 统计一段时间内所有系统调用的耗时概况。不过 strace 本身也会消耗不少资源,对执行时间也会产生影响。
从上面的信息你可以看到,读占了 97.56% 的时间,一共调用了 1987 次,耗时 0.04s,平均每次系统调用 21us。同样的道理,我们也可以计算应用程序某个任务 I/O 耗时的百分比。假设一个任务执行了 10s,I/O 花了 9s,那么 I/O 耗时百分比就是 90%。这种情况下,I/O 就是我们任务很大的瓶颈,需要去做进一步的优化。
第三种方法:使用 vmstat。
vmstat 的各个字段说明可以参考《vmstat 监视内存使用情况》,其中 Memory 中的 buff 和 cache,I/O 中的 bi 和 bo,System 中的 cs,以及 CPU 中的 sy 和 wa,这些字段的数值都与 I/O 行为有关。
总结
在性能优化的过程中,我们关注最多的是 CPU 和内存,I/O 也是性能优化中比较重要的一部分。
今天我们学习 I/O 处理的整个流程,它包括应用程序、文件系统和磁盘三个部分。不过 I/O 这个话题真的很大,在课后需要花更多时间学习课后练习中的一些参考资料。
LPDDR5、UFS 3.0 很快就要在 2019 年面世,有些同学会想,随着硬件越来越牛,我们根本就不需要去做优化了。但是一方面考虑到成本的问题,在嵌入式、IoT 等一些场景的设备硬件不会太好;另一方面,我们对应用体验的要求也越来越高,沉浸体验(VR)、人工智能(AI)等新功能对硬件的要求也越来越高。所以,应用优化是永恒的,只是在不同的场景下有不同的要求。
课后练习
学习完今天的内容,可能大部分同学会感觉有点陌生、有点茫然。但是没有关系,我们可以在课后补充更多的基础知识,下面的链接是我推荐给你的参考资料。今天的课后作业是,通过今天的学习,在留言区写写你对 I/O 的理解,以及你都遇到过哪些 I/O 方面的问题。
“实践出真知”,你也可以尝试使用 strace 和 block_dump 来观察自己应用的 I/O 情况,不过有些实验会要求有 root 的机器。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 11
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
08 | 启动优化(下):优化启动速度的进阶方法
下一篇
10 | I/O优化(中):不同I/O方式的使用场景是什么?
精选留言(16)
- 东方2018-12-28看到很多朋友提到SP的问题,SP造成ANR的原因大家也知道,我来说说我的经验。 1.把sdk中SP的实现剥离出来,重写Application的getSharedPreferencen方法。这里需要注意几点,a.低版本multidex,b.Activity Service中getSP的context必须是ApplicationContext。 2.上述的方案依然是普通File操作,解析xml,线上依然有fsync导致ANR。 3.自己使用mmap实现sp的映射,或者使用微信的MMKV,但是getAll方法需要自己兼容。 4.增加大kv的监控。 大家伙还有更好的方案可以继续交流展开19
- menty2019-04-01请问,微信debug模式下,运行会很卡吗。目前我司的app在debug模式下就超卡,非得在debug=false下才运行顺畅,不知是何原因导致
作者回复: 是的,主要是因为 1.debug包代码没有经过proguard这些优化 2.debug模式下虚拟机一些运行策略 3.debug包大量的logcat
共 2 条评论7 - itmagic_jack2019-07-17Java应用层的I/O优化,可以考虑使用square公司出品的Okio框架(Okhttp底层IO实现),在Java平台上使用,简化了api调用,支持IO超时检测,优化了资源缓存等7
- Kenny2018-12-27张老师,你好,mmap技术是首先通过业缓存去拿数据,如果没有就发生缺页中断,然后发生物理io从磁盘拿数据到业缓存,然后再从业缓存拿数据,这样相对普通文件io就少了一次逻辑io(即与文件系统的io),是可以这样理解么?
作者回复: 对,少一次数据拷贝跟系统调用的时间
4 - 下雨天2018-12-27大佬,好!SharedPreference 跨进程读写就非常容易出现数据丢失的情况。从io性能方面来看,应用层有什么更好的方案和设计来提SharedPreference跨进程访问数据的可靠性么?3
- 程序员小跃2019-03-13真的是高手课,如果之前没看过内存分析的教程,有些东西我都不大懂
作者回复: 需要结合参考资料进一步学习
1 - Lll2019-02-17"我们知道,CPU 和内存相比磁盘是高速设备,整个流程的瓶颈在于磁盘 I/O 的性能。"这句话好像有歧义,请问是笔误了吧?
作者回复: 这个意思就是cpu和内存都是高速设备,磁盘是低速设备
1 - 东方2018-12-28对于io,如果使用glide作为默认的图片加载库,启动glide后,会把上一次符合条件的图片都一次给加载到内存中,这个是disklrucache做的,想问一下张老师,有什么经验或者办法,只load感兴趣的图片。再者微信的图片加载框架底层是mmap实现吗?1
- 欢乐de羊2018-12-28请问下在文件遍历方面,(尤其是层级比较多的文件,例如微信的image2),有什么好的方案提高遍历速度吗?
作者回复: 小文件系统,后面会讲到
1 - 扈丽霞2022-01-07文件读取不是以page为单位,擦除以block为单位吗?
- Geek_a246642019-09-07你好老师,我遇到过在向文件中写入字符串时,最终发现文件中内容和写入顺序不太一致,能不能指点一下思路,现在毫无头绪
作者回复: 这个一般不太可能,可以hook写入的内容看看,可能是多线程的问题?
- Joyce2019-06-20IO优化,应用层可以做什么
作者回复: 合理使用和监控IO操作,选择合适的存储方案
- 蚂蚁内推+v2019-01-07连续写入限制指的是一秒最多写入多大小吗?
- 09282018-12-28说到io,我有个问题想咨询一下: 场景大概是,有26亿的数据需要读取从HBase中读取出来分别写到12个文件中取,有什么方式能高效且数据不错乱的写到文件中去呢? 想法: 1.读取出来的数据需要并发写到文件中 2.每次读取一定的缓存大小批量写入,不能一条一条写入 3.文件并发写入会出现错位等问题,所以需要一个写队列来处理 问题: 目前1和2两种方式没什么问题,但是在写队列的设计上有一定问题,希望给点意见。 如果最终写文件还是单线程的话性能瓶颈都在这里,这里应该怎么设计呢?展开
- 有生丶之莲2018-12-27张老师好,之前遇到过一些anr,定位到firebase中的tokenRefresh这块方法,但是自己重写这个方法也就只是用SP把token保存下来,请问SP读写会导致这个吗?SP的apply和commit都试过,apply说是异步的,但主线程会等着保存完成的回调,是这样吗?
作者回复: 即使是异步,在一些时机也会强制等待
1 - Kenny2018-12-27hi,张老师,刚你说业缓存在内存紧张的时候会被回收,那么发生GC一定会回收吗?还是说跟GC无关联?是有另外一套内存检测回收业缓存的机制?那么启动优化,除了资源重排,降低总内存也能提升io次数?
作者回复: 下一篇文章会讲到