关闭

磁盘:最容易被忽略的性能洼地

标签: Android磁盘
2705人阅读 评论(1) 收藏 举报
分类:

引言:从整个软件的性能来说,资源类性能就像是撑起冰山一角的下面的冰层。构成这部分的,是传统部分的磁盘、CPU、内存和网络以及因为移动网络而显得特别重要的电池(耗电)。本文我们将向您着重介绍磁盘部分。
本文选自《Android移动性能实战》。

1 原理

  在没有SSD硬盘之前,大家都会觉得我们的HDD硬盘很好用,什么5400转、7200转,广告都是棒棒的。直到有一天,SSD出现了,发现启动Windows的时候,居然可以秒开,这才幡然醒悟。因此,对于外行来说,磁盘I/O性能总是最容易被忽略的,精力会更集中在CPU上。但是对于内行人来说,大家都懂得,性能无非是CPU密集型和I/O密集型。磁盘I/O就是其中之一。那么到了移动时代,我们的存储芯片性能究竟怎样呢?在讨论这个问题之前,我们来看一个测试数据。
              图片描述
  如上图,我们的顺序读/写的性能进步得非常快,很多新的机型,顺序读/写比起以前的性能,那是大幅度提升,跟SSD的差距已经缩小了很多。但是这里有个坏消息,随机读/写的性能依旧很差,见MOTO X、S7、iPhone 6S Plus。到这里,必须给大家介绍第一个概念:随机读/写。

随机读/写

  随机写无处不在,举两个简单例子吧。第一个例子最简单,数据库的journal文件会导致随机写。当写操作在数据库的db文件和journal文件中来回发生时,则会引发随机写。如下表,将一条数据简单地插入到test.db,监控pwrite64的接口,可以看到表中有底纹的地方都是随机写。第二个例子,如果向设置了AUTOINCREMENT(自动创建主键字段的值)的数据库表中插入多条数据,那么每插入一条数据,都需要操作两张数据库表,这就意味着存在随机写。
           图片描述
           图片描述
  从上面的例子可知,随机读/ 写是相对顺序读/ 写而言的, 在读取或者写入的时候随机地产生offset。但为什么随机读/ 写会如此之慢呢? 
  1. 随机读会失去预读(read-ahead)的优化效果。
  2. 随机写相对于顺序写除了产生大量的失效页面之外,更重要的是增加了触发“写入放大”效应的概率。
  那么“写入放大”又是什么呢?下面我们来介绍第二个概念:“写入放大”效应。

“写入放大”效应

  当数据第一次写入时,由于所有的颗粒都为已擦除状态,所以数据能够以页为最小单位直接写入进去。当有新的数据写入需要替换旧的数据时,主控制器将把新的数据写入到另外的空白闪存空间上(已擦除状态),然后更新逻辑LBA 地址来指向到新的物理FTL 地址。此时,旧的地址内容就变成了无效的数据,但主控制器并没执行擦除操作而是会标记对应的“页”为无效。当磁盘需要在上述无效区域进行再次写入的话,为了得到空闲空间,闪存必须先复制该“块”中所有的有效“页”到新的“块”里,并擦除旧“块”后,才能写入。(进一步学习,可参见:http://bbs.pceva.com.cn/forum.php?mod=viewthread&action=print able&tid=8277 。) 
  比如,现在写入一个4KB 的数据,最坏的情况就是,一个块里已经没有干净空间了, 但是恰好有一个“页”的无效数据可以擦除,所以主控就把所有的数据读出来,擦除块, 再加上这个4KB 新数据写回去。回顾整个过程,其实只想写4KB 的数据,结果造成了整个块(512KB)的写入操作。同时带来了原本只需要简单地写4KB 的操作变成了“闪存读取 (512KB)-> 缓存改(4KB)-> 闪存擦除(512KB)-> 闪存写入(512KB)”,这造成了延迟大大增加,速度慢是自然的。这就是所谓的“写入放大”(Write Amplification) 问题。
          【图4】
  下面我们通过构造场景来验证写入放大效应的存在。
  场景 1:正常向 SD 卡写入 1MB 文件,统计文件写入的耗时。
  场景 2:先用 6KB 的小文件将 SD 卡写满,然后将写入的文件删除。这样就可以保证 SD 卡没有干净的数据块。这时再向 SD 卡写入 1MB 的文件,统计文件写入的耗时。
  下图是分别在三星 9100、三星 9006 以及三星 9300 上进行的测试数据,从测试数据看, 在 SD 卡没有干净数据块的情况下,文件的写入耗时是正常写入耗时的 1.9~6.5 倍,因此测 试结果可以很好地说明“写入放大”效应的存在。
          【图5】

  那么写入放大效应最容易是在什么时候出现呢?外因:手机长期使用,磁盘空间不足。内因:应用触发大量随机写。这时,磁盘I/O 的耗时会产生剧烈的波动,App 能做的只有一件事,即减少磁盘I/O 的操作量,特别是主线程的操作量。那么如何发现、定位、解决这些磁盘I/O 的性能问题呢?当然就要利用我们的工具了。

2 工具集

  工具集如下表。
   【图6】
  STRICTMODE 应该是入门级必备工具了,可以发现并定位磁盘I/O 问题中影响最大的主线程I/O。由下面代码可见,启用方法非常简单。

public void onCreate() { 
   ifDEVELOPER_MODE) { 
   StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 
   .detectDiskReads() 
   .detectDiskWrites() 
   .detectNetwork() 
   .penaltyLog() 
   .build()); 
   super.onCreate(); 
 } 
} 

  原理也非常简单,主要是文件操作(BlockGuardOs.java)、数据库操作(SQLiteConnection. java)和SharePreferences 操作(SharedPreferencesImpl.java)的接口中插入检查的代码。我们截取了一段Android 源码中文件操作的监控实现代码,如下,最后实际调用StrictMode 中的onWriteToDisk 方法,通过创建BlockGuardPolicyException 来打印I/O 调用的堆栈,帮助定位问题。
         【图7】
详细代码: 
http://androidxref.com/4.4.4_r1/xref/libcore/luni/src/main/java/libcore/io/BlockGuardOs.java#91

Perfbox:I/OMonitor

  原理:I/OMonitor的功能可以归结为通过Hook Java层系统I/O的方法,收集区分进程和场景的I/O信息。

1. Hook java方法

  I/O Monitor Hook java方法借鉴了开源项目xposed,网上介绍xposed的文章很多,这里就用流程图来简要说明获取此次I/O操作信息的方法。
                      【图8】

2. 区分进程和场景的I/O 信息收集

  区分进程和场景的I/O 信息收集有以下4个步骤。

(1)app_process 替换

  app_process 是Android 中Java 程序的入口,通过替换app_process 就可以控制入口, 在任何一个应用中运行我们的代码。替换后的app_process 工作流程如下。
                      【图9】

(2) 将libfork.so添加到环境变量LD_PRELOAD中

  在UNIX中,LD_PRELOAD是一个可以影响程序的运行时链接的环境变量,让你可以定义在程序运行前优先加载的动态链接库。而这个功能就可以用来有选择性地载入不同动态链接库中的相同函数。而在zygote进程启动前设置LD_PRELOAD环境变量,这样zygote的所有子进程都会继承这个环境变量。libfork.so实现了一个fork函数,当app_process通过fork函数来启动zygote进程时,会优先使用libfork.so中实现的fork函数,fork函数的流程如下。
                      【图10】

(3) 将XPlatform.jar 添加到环境变量CLASSPATH 中

  将XPlatform.jar 加入到CLASSPATH 中,是为了可以让像common.jar 这种插件型jar 使用XPlatform.jar 中的类。手机QQ 中也存在类似事情,开发的同事把整个工程编译成了两个dex 文件,在手机QQ 启动后,把第二个dex 文件放入CLASSPATH 中(与XPlatform 实现方法不同,但效果相同),这样主dex 可以直接import 并使用第二个dex 中的类。如果不加入CLASSPATH,需要借助DexClassLoader 类来使用另一个jar 包中的类,这样使用起来很麻烦,并且会有很大的限制。
在系统启动过程中,app_process 进程实际上是zygote 进程的前身,所以XPlatform.jar 是在zygote 进程中运行的。
  在XPlatform 中主要Hook 了两个java 方法,来监控system_server 进程和应用进程的启·11· 动,并在这些进程中做一些初始化的操作。这里面用了一个fork的特性,父进程使用fork创建子进程,子进程会继承父进程的所有变量,由于zygote使用fork创建子进程,所以在zygote进程中进行Hook,在它创建的任何一个应用进程和system_server进程也是生效的。
  XPlatform工作流程图如下。
                      【图11】
  这样就实现了在应用进程启动时,控制在指定进程中运行I/O Monitor的功能。

(4) 区分场景的I/O信息收集

  为了实现分场景的I/O信息收集,我们给I/O Monitor添加了一个开关,对应的就是Python控制脚本,这样便可以实现指定场景的I/O信息收集,使测试结果做到更精准。
                      【图12】
  这样我们就实现了区分进程和场景的I/O 信息收集。
  在介绍了我们的工具原理之后,来看一下采集的I/O 日志信息,包括文件路径、进程、线程、读/ 写文件的次数、大小和耗时以及调用的堆栈。
     【图13】
  XPlatform工作流程图中的数据说明:某个文件的一次对应CSV文件中的一行,每次调用系统的API(read或者write方法),读/写次数(readcount, writecount)就加1。读/写耗时(readtime, writetime)是计算open到close的时间。

SQLite性能分析/监控工具 SQL I/O Monitor

  我们知道,数据库操作最终操作的是磁盘上的DB文件,DB文件和普通的文件本质上并无差异,而I/O系统的性能一直是计算机的瓶颈,所以优化数据库最终落脚点往往在如何减少磁盘I/O上。
  无论是优化表结构、使用索引、增加缓存、调整page size等,最终的目的都是减少磁盘I/O,而这些都是我们常规的优化数据库的手段。习惯从分析业务特性、尝试优化策略到验证测试结果的正向思维,那么我们为何不能逆向一次?既然数据库优化的目的都是减少磁盘I/O,那我们能不能直接从磁盘I/O数据出发,看会不会有意想不到的收获。

1.采集数据库I/O数据

  要想实现我们的想法,第一步当然要采集数据库操作过程中对应的磁盘I/O数据。由于之前通过Java Hook技术,获取到了Java层的I/O操作数据,虽然SQLite的I/O操作在libsqlite.so进行,属于Native层,但我们会很自然地想到通过Native Hook采集SQLite的I/O数据。
Native Hook主要有以下实现方式。
  (1)修改环境变量LD_PRELOAD。
  (2)修改sys_call_table。
  (3)修改寄存器。
  (4)修改GOT表。
  (5)Inline Hook。
  下面主要介绍(1)、(4)、(5)三种实现方式。

(1)修改环境变量LD_PRELOAD

  这种方式实现最简单,重写系统函数open、read、write和close,将so库放进环境变量LD_PRELOAD中,这样程序在调用系统函数时,会先去环境变量里面找,这样就会调用重写的系统函数。可以参考看雪论坛的文章“Android使用LD_PRELOAD进行Hook”(http://bbs.pediy.com/showthread.php?t=185693)。
但是这种Hook针对整个系统生效,即系统所有I/O操作都被Hook,造成Hook的数据量巨大,系统动不动就卡死。

(4)修改GOT 表

  引用外部函数的时候,在编译时会将外部函数的地址以Stub 的形式存放在.GOT 表中,加载时linker 再进行重定位,即将真实的外部函数写到此stub 中。Hook 的思路就是替换.GOT 表中的外部函数地址。而libsqlite.so 中的I/O 操作是调用libc.so 中的系统函数进行,所以修改GOT 表的Hook 方案是可行的。
  然而现实总不是一帆风顺的,当我们的方案实现后,发现只能记录到libsqlite.so 中的open 和close 函数调用,而由于sqlite 的内部机制而导致的read/write 调用我们无法记录到。

(5)Inline Hook

  在前两种方案无果后,只能尝试Inline Hook。Inline Hook 可以Hook so 库的内部函数, 我们首先想到的是Hook libsqlite.so 内部I/O 接口posixOpen、seekandread、seekandwrite 以及robust_close。但是在成功的路上总是充满波折,sqlite 内部竟然将大部分的关键函数定义为static 函数,如posixOpen。在C 语言中,static 函数是不导出符号的,而Inline Hook 就是要在符号表中找到对应的函数位置。这样一来,通过Hook sqlite 内部函数的路子又行不通了。

static int posixOpen(const char *zFile, int flags, int mode){ 
return open(zFile, flags, mode); 
}

  既然这样不行,那我们只能更暴力地Hook libc.so 中的open、read、write 和close 方法。因为不管sqlite 里面怎么改,最终还是会调用系统函数,唯一不好的是这样录到了该进程所有的IO 数据。这种方法在自己编译的libsqlite.so 里面证实是可行的。
  正当我满怀欣喜地去调用手机自带的libsqlite.so 库时,读/ 写数据再一次没有被记录到, 我当时的内心几乎是崩溃的。为什么我自己编译的libsqlite.so 库可以,用手机上的就不行呢?没办法,只能再去看如下面的源码,最后在seekAndRead 里面发现,sqlite 定义了很多宏开关,可以决定调用系统函数pread、pread64 以及read 来进行读文件。莫非我自己编的so 和手机里面的so 的编译方式不一样?

static int seekAndRead(unixFile *id, sqlite3_int64 offset, void *pBuf, int cnt){ 
int got; 
int prior = 0;
#if (!defined(USE_PREAD) && !defined(USE_PREAD64)) 
i64 newOffset;
#endif 
TIMER_START; 
do{
#if defined(USE_PREAD) 
got = osPread(id->h, pBuf, cnt, offset); 
SimulateIOError( got = -1 );
#elif defined(USE_PREAD64) 
got = osPread64(id->h, pBuf, cnt, offset); 
SimulateIOError( got = -1 );
#else 
newOffset = lseek(id->h, offset, SEEK_SET); 
SimulateIOError( newOffset-- );

  笔者又Hook 了pread和pread64,这一次终于记录到了完整的I/O数据,原来手机里面的libsqlite.so调用系统的pread64和pwrite64函数来进行I/O操作,同时通过Inline Hook获取到了数据库读/写磁盘时page的类型,sqlite的page类型有表叶子页、表内部页、索引叶子页、索引内部页以及溢出页,采集的数据库日志信息如下。
      【图14】
  费尽了千辛万苦,终于拿到了数据库读/写磁盘的信息,但是这些信息有什么用呢?我们能想到可以有以下用途。

  • 通过I/O数据的量直观地验证数据库优化效果。
  • 通过偏移量找出随机读/写进行优化。

但是我们又面临另外一个问题,因为获取的磁盘信息是基于DB 文件的,而应用层操作数据库是基于表的,同时又缺乏堆栈,很难定位问题。基于此,我们又想到了另外一个解决方法,就是Hook 应用代码的数据库操作,通过堆栈把两者对应起来,这样就可以把应用代码联系起来,更方便分析问题。

2. Hook 应用层SQL 操作

  Hook 应用代码其实就是Hook SQLiteDatabase 里面的数据库增删改查操作,应用代码SQL 语句如下,Java 层Hook 基于Xposed 的方案实现。
         【图15】

  最终可以通过堆栈和磁盘信息对应起来。

【图16】

  获取到了这么多数据,我们在之后的推送中将向大家介绍一些数据库相关的案例,看其如何应用。

  本文选自《Android移动性能实战》,点此链接可在博文视点官网查看此书。
                    图片描述
  想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。
                       图片描述

4
0
查看评论

C语言中容易被忽略的细节(第一篇)

前言:本文的目的是记录C语言中那些容易被忽略的细节。我打算每天抽出一点时间看书整理,坚持下去,今天是第一篇,也许下个月的今天是第二篇,明年的今天又是第几篇呢?……我坚信,好记性不如烂笔头。 第二篇链接:C语言中容易被忽略的细节(第二篇) 第三篇链接:C语言中容易被忽略的细节(...
  • linxin3333520
  • linxin3333520
  • 2014-12-10 17:22
  • 572

ArcGIS教程:DEM数据洼地填充

水文分析是DEM数据应用的一个重要方面,利用DEM生成的集水流域和水流网络数据,是大多数地表水文分析模型的主要支撑数据。基于DEM的地表水文分析的主要内容,是利用水文分析工具,提取相关模型所需的水流方向、汇流累积量、水流长度、河流网络(包括河流网络的分级等)数据,以及对研究区的流域进行分割等。  ...
  • u010687924
  • u010687924
  • 2014-11-07 15:03
  • 1977

哪种经理最容易被淘汰

在外企工作了这些年,目睹了很多高级经理或主管的来来去去。有突然”死亡”的,有自愿离职的,有被动请辞的。不禁想研究一下职业经理为何都短命的原因。记得我的一个老板跟我聊的一句话:爬的越高,空气就越稀薄。实在是很有道理。他本人虽然聪明能干,最终仍然因为一次公司税务的损失而卷铺盖走人。很多人羡慕高级经理人的...
  • lanyd
  • lanyd
  • 2010-02-04 16:17
  • 454

Java基础(容易忽略的java细节)

java基础片,大家经常忽略的一些细节性问题,在此分享一下public class Test1 { public static void main(String[] args) { // TODO Auto-generated method stub int ...
  • zhanggaofeixy
  • zhanggaofeixy
  • 2016-04-16 14:02
  • 342

html、css容易被忽略的小知识点

都说前端容易,谁说的。前端上手快,越往后学习越困难。学习前端快一年了,依然发现一些小的知识点不会。这些经验性的东西不碰到永远学不会。 (1)title前面的logo。 就像这个编辑文章前面的logo。不知道大家知不知道这个图标是如何加载过来的。反正我之前不知道。。。如果大家知道的话就直接忽视掉我...
  • westernRanger
  • westernRanger
  • 2014-11-29 14:03
  • 1207

BBC权威分析:未来哪些职业不容易被淘汰?

人工智能技术正在逐个“扫荡”传统行业,人们一面期待人工智能(AI)为我们的生活带来更多的便利和惊喜,但一方面也在担忧人工智能恐怕会夺走工作机会,造成大量失业。最近,《纽约客》杂志的一张最新封面毫无征兆地在朋友圈里刷了屏。封面上,人类坐地行乞,机器人则扮演了施予者的角色,意指明显——在未来社会,人类的...
  • zw0Pi8G5C1x
  • zw0Pi8G5C1x
  • 2017-12-18 00:00
  • 144

风险预警·11g容易被忽略的导入性能问题

前言       某大型国有银行一套关键系统10g升级到11g,老K负责升级后第一天早上的运行保障;在升级前甲方客户已经先后做了各种测试,以保证升级后不会存在任何性能问题。然而,事与愿违,老K刚到现场,客户应用团队就已经反馈到客户说批量慢了一段时间,根据应用日...
  • DBAXIAOy
  • DBAXIAOy
  • 2017-07-28 13:34
  • 244

容易被忽略CSS特性

CSS初学感觉很简单,但随着学习的深入才感觉CSS的水由多深,平常总会遇到各种坑,先总结一些经常遇到的坑 大小写不敏感 虽然我们平时在写CSS的时候都是用小写,但其实CSS并不是大小写敏感的 .test{ background-COLOR:#a00; width:1...
  • binyao02123202
  • binyao02123202
  • 2014-01-17 16:46
  • 552

几种最容易给老公戴绿帽的职业

1、演员(含歌手)    解放前,演员就是戏子,在民间语言里戏子常常和婊子放在一起做比较,戏子是入不得大户人家的。解放后,演员的地位空前的提高到了政治的角度,那时候,即使上床也是为了革命为了人民,这里就不予批评。随着改革开放的春风,演员这个职业的竞争也开始激烈了,有了竞争,就得经常和导演和投资方、制...
  • metababy
  • metababy
  • 2005-12-29 13:03
  • 1371

java中容易忽略的小细节

如果问大家private 、public和protected 这些修饰符的具体概念,想必大家肯定能回答的出,对了还有一种默认的访问权限 就是没有这些修饰符时(在同一包下,都能被访问);但是大家有没有想过为什么会出现private了? 那我们从它的字面意思来理解一下:被private修饰只...
  • qq_27778869
  • qq_27778869
  • 2016-07-15 11:27
  • 360
    个人资料
    • 访问:3936241次
    • 积分:56598
    • 等级:
    • 排名:第57名
    • 原创:1462篇
    • 转载:83篇
    • 译文:1篇
    • 评论:3787条
    博客专栏
    文章存档
    最新评论