Android 高性能日志写入方案

前言

公司目前在作一款企业级智能客服系统,对于系统稳定性要求很高,不过难保用户在使用中不会出现问题,而 Android SDK 集成在客户的 APP 中,同时因为 Android 碎片化的问题,对于 SDK 的问题排查就显得尤其困难,所以记录下用户的操做日志就显得极为重要。缓存

初始方案

一开始,SDK 记录日志的方式是直接经过写文件,当有一条日志要写入的时候,首先,打开文件,而后写入日志,最后关闭文件。这样作的问题就在于频繁的IO操做,影响程序的性能,并且 SDK 为了保证消息的及时性,还维护了一个后台进程,当其中一个进程进行日志写入时,另外一个就会被锁在门外等着,问题就愈发严重。使用这种方案虽然当前看上去对程序的影响不大,可是随着日志量的增长,更多的IO操做,必定会形成性能瓶颈。bash

下面咱们来分析下直接写入文件的流程:app

  1. 用户发起 write 操做
  2. 操做系统查找页缓存 a.若未命中,则产生缺页异常,而后建立页缓存,将用户传入的内容写入页缓存 b.若命中,则直接将用户传入的内容写入页缓存
  3. 用户 write 调用完成
  4. 页被修改后成为脏页,操做系统有两种机制将脏页写回磁盘 a.用户手动调用 fsync() b.由 pdflush 进程定时将脏页写回磁盘

能够看出,数据从程序写入到磁盘的过程当中,其实牵涉到两次数据拷贝:一次是用户空间内存拷贝到内核空间的缓存,一次是回写时内核空间的缓存到硬盘的拷贝。当发生回写时也涉及到了内核空间和用户空间频繁切换。dom

并且相对于机械硬盘,SSD 存储还有一个“写入放大”的问题。这个问题主要和 SSD 存储的物理结构有关。当 SSD 被所有写过一遍以后,再写入的数据是不能够直接更新,只能够经过覆盖重写,在覆盖以前须要先擦除数据。但写入的最小单位是 Page,擦除的最小单位是 Block,而 Block 远大于 Page,因此在写入新数据时就须要先把 Block 上的数据读出来和要写入的数据合并在一块儿,再把 Block 擦除,最后把读出来的数据从新写入到存储上,这样致使实际写入的数据可能远远大于最开始须要写入的数据。函数

没想到简单的写文件居然涉及了这么多操做,只是对于应用层透明而已。性能

既然每写一次文件会执行这么屡次操做,那么咱们能不能将日志缓存起来,当达到必定的数量后再一次性的写入磁盘中呢?测试

这样确实可以大量减小 IO 次数,可是却会引起另外一个更严重的问题——丢日志优化

把日志缓存在内存中,当程序发生 Crash 或进程被杀后就没法保证日志的完整性,并且因为 SDK 存在多进程,也没法保证多进程下日志的顺序。spa

一个完善的日志方案,须要知足操作系统

  • 高效,不能影响系统性能,不能由于引入了日志模块而形成应用卡顿
  • 保证日志的完整性,若是不能保证日志完整,那么日志收集就没有意义了
  • 对于多进程应用,要保证最终看到的日志顺序的准确性

高性能方案

既然没法减小写入次数,那么咱们能不能在写文件的过程当中去优化呢?

答案是能够的,使用 mmap

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系,函数原型以下

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
复制代码

mmap操做提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较经常使用。

同时 mmap 可以保证日志的完整性,mmap 的回写时机:

  • 内存不足
  • 进程退出
  • 调用 msync 或者 munmap
  • 不设置 MAP_NOSYNC 状况下 30s-60s(仅限FreeBSD)

当映射一个文件后,程序就会在 native 内存中申请一块相同大小的空间,所以建议每次映射一小段内容,如 64k,写满后再从新映射文件后面的内容。

日志写入性能和完整性的问题解决了,那么如何保证多进程下日志的顺序呢?

因为 mmap 是采用共享内存的方式写入数据,若是两个进程同时映射一个文件,那么必定会形成日志覆盖的问题。

既然不能直接保证顺序,那咱们只能退而求其次,两个进程分别映射不一样的文件,天天合并一次,合并时对日志进行排序。

继续优化

根据上述方案,设计 jni 接口,打包 so,引入 SDK,看似没什么问题了,可是做为一款 SDK,总以为包含 so 不太友好,在必定程度上会增长接入的难度。

那么能不能不用 so 呢?

其实 Java 中已经提供了内存映射的实现——MappedByteBuffer

MappedByteBuffer 位于 Java NIO 包下,用于将文件内容映射到缓冲区,使用的便是 mmap 技术。经过 FileChannel 的 map 方法能够建立缓冲区

RandomAccessFileraf = new RandomAccessFile(file, "rw");
MappedByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, position, size);
复制代码

为了测试 MappedByteBuffer 的效率,咱们把 64byte 的数据分别写入内存、MappedByteBuffer 和磁盘文件 50 万次,并统计耗时

方法 耗时
内存 384ms
MappedByteBuffer 700ms
磁盘文件 16805ms

能够看出 MappedByteBuffer 虽然不及写入内存的性能,可是相比较写入磁盘文件,已经有了质的提高。

总结

本文主要分析了直接写文件记录日志方式存在的问题,并引伸出高性能文件写入方案 mmap,兼顾了写入性能和完整性,并经过补偿方案确保多进程下日志的顺序。最后发现了内存映射在 Java 层的实现,避免了引入 so。

迁移自个人简书 2018.01.28

相关文章
相关标签/搜索