本文来自网易云社区

作者:网易七鱼 Android 开发团队

前言

网易七鱼作为一款企业级智能客服系统,对于系统稳定性要求很高,不过难保用户在使用中不会出现问题,而 Android SDK 安装在用户的手机上,同时由于 Android 碎片化的问题,对于 Android SDK 的问题排查就显得尤为困难,因此记录下用户的操作日志就显得极为重要。

声明:网易七鱼仅记录操作日志,用于还原问题,不会记录用户的隐私信息。

初始方案

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

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

  1. 用户发起 write 操作

  2. 操作系统查找页缓存
    a.若未命中,则产生缺页异常,然后创建页缓存,将用户传入的内容写入页缓存
    b.若命中,则直接将用户传入的内容写入页缓存

  3. 用户 write 调用完成

  4. 页被修改后成为脏页,操作系统有两种机制将脏页写回磁盘
    a.用户手动调用 fsync()
    b.由 pdflush 进程定时将脏页写回磁盘

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

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

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

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

这样确实能够大量减少 IO 次数,但是却会引发另一个更严重的问题——丢日志

把日志缓存在内存中,当程序发生 Crash 或进程被杀后就无法保证日志的完整性,而且由于七鱼存在多进程,也无法保证多进程下日志的顺序。

一个完善的日志方案,需要满足

  • 高效,不能影响系统性能,不能因为引入了日志模块而造成应用卡顿

  • 保证日志的完整性,如果不能保证日志完整,那么日志收集就没有意义了

  • 对于多进程应用,要保证最终看到的日志顺序的准确性

高性能方案

既然无法减少写入次数,那么我们能不能在写文件的过程中去优化呢?

答案是可以的,使用 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 方法可以创建缓冲区

MappedByteBuffer raf = 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。

网易云免费体验馆,0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区

相关文章:
【推荐】 常用数据清洗方法大盘点
【推荐】 浅谈由管理者角色引出的B端产品设计思考点
【推荐】 Android TV 开发 (1)

网易七鱼 Android 高性能日志写入方案的更多相关文章

  1. React Native学习(八)—— 对接七鱼客服

    本文基于React Native 0.52 Demo上传到Git了,有需要可以看看,写了新内容会上传的.Git地址 https://github.com/gingerJY/React-Native-D ...

  2. Android 优质精准的用户行为统计和日志打捞方案

    Android 自定义优质精准的用户行为和日志打捞方案 Tamic csdn博客 :http://blog.csdn.net/sk719887916/article/details/51398416 ...

  3. 【腾讯Bugly干货分享】微信mars 的高性能日志模块 xlog

    本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/581c2c46bef1702a2db3ae53 Dev Club 是一个交流移动 ...

  4. 【腾讯Bugly干货分享】微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ff5932cde42f1f03de29b1 本文来源: 微信客户端开发团队 ...

  5. 七、Android学习第六天——SQLite与文件下载(转)

    (转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 七.Android学习第六天——SQLite与文件下载 SQLite SQ ...

  6. 大叔也说Xamarin~Android篇~日志的记录

    回到目录 无论哪个平台,开始哪种应用程序,日志总是少不了的,大家在Lind.DDD里也可以看到大叔的日志组件,而在xamarin进行移动开发时,为了更好的调试,记录运行的情况,日志也是必须的,这讲主要 ...

  7. Android将Log写入文件

    为什么要将Log写入文件 运行应用程序的时候,大多数是不会连接着IDE的: 而当应用程序崩溃时,我们需要收集复现步骤,在设备上复现,并进行Debug: 而由于Android手机的多样性,有些问题是某个 ...

  8. Android输出日志Log类

    android.util.Log常用的方法有以下5个: Log.v() Log.d() Log.i() Log.w() 以及 Log.e().根据首字母分别对应VERBOSE,DEBUG,INFO,W ...

  9. Android app日志保存功能

    每一个App应用应该都需要有日志保存的功能,日志保存可以记录App运行中所遇到的问题,查Bug也比较方便 等等: Android日志保存功能,保存某几天的最新日志文件到某个目录,直接看是如何代码实现的 ...

随机推荐

  1. django get_object_or_404

    django get_object_or_404 是django shortcuts模块里面一个比较简便的方法,特别是用django get来操作数据库的时候,可以帮 我们少写一些代码,加快开发速度. ...

  2. Windows彻底卸载系统自带的office

    由于自带office导致按照新的office会提示要先卸载原来32位的office,又在控制面板或软件管理工具中找不到office,用如下方法删除 1.在C盘删除office文件夹 2.删除注册表 1 ...

  3. DataGrid添加滚动条

    DataGrid中是没有滚动条的,要添加滚动条需要借助<div>层 <div style="overflow:auto;width:95%;height:95%" ...

  4. 20145223 杨梦云 《网络对抗》shellcode实验+return-to-libc实验

    20145223 杨梦云 <网络对抗>shellcode实验+return-to-libc实验 shellcode注入实践 Shellcode基础知识 ·Shellcode实际是一段代码( ...

  5. if __name__ == "__main__"如何正确理解

    粗略来讲,__name__是当前模块,当模块被直接运行时模块名为__main__.这句话的意思是,当模块被直接执行时,代码将运行,当模块是被导入时,代码不被运行 例如,执行one.py # file ...

  6. HDU 1045 Fire Net(DFS 与8皇后问题类似)

    Fire Net Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Su ...

  7. HDU 2018母牛的故事(类似斐波那契,找规律)

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=2018 母牛的故事 Time Limit: 2000/1000 MS (Java/Others)     ...

  8. HDU 1286 找新朋友 (欧拉公式或者标记法(其实就是欧拉公式的思想))

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=1286 找新朋友 Time Limit: 2000/1000 MS (Java/Others)    M ...

  9. MyEclipse 远程调试Tomcat

    当Web项目部署在服务器之后,当项目出现问题的时候就需要远程调试[远程调试的代码要与本地代码一致] 配置远程调试的具体步骤如下: 1.Linux 中配置tomcat在catalina.sh中添加如下C ...

  10. free -g 说明

    free -g 说明: free -g -/+ buffers/cache 说明: buffer 写缓存,表示脏数据写入磁盘之前缓存一段时间,可以释放.sync命令可以把buffer强制写入硬盘 ca ...