网易七鱼 Android 高性能日志写入方案
本文来自网易云社区
作者:网易七鱼 Android 开发团队
前言
网易七鱼作为一款企业级智能客服系统,对于系统稳定性要求很高,不过难保用户在使用中不会出现问题,而 Android SDK 安装在用户的手机上,同时由于 Android 碎片化的问题,对于 Android SDK 的问题排查就显得尤为困难,因此记录下用户的操作日志就显得极为重要。
声明:网易七鱼仅记录操作日志,用于还原问题,不会记录用户的隐私信息。
初始方案
一开始,网易七鱼记录日志的方式是直接通过写文件,当有一条日志要写入的时候,首先,打开文件,然后写入日志,最后关闭文件。这样做的问题就在于频繁的IO操作,影响程序的性能,而且七鱼为了保证消息的及时性,还维护了一个后台进程,当其中一个进程进行日志写入时,另一个就会被锁在门外等着,问题就愈发严重。使用这种方案虽然当前看上去对程序的影响不大,但是随着日志量的增加,更多的IO操作,一定会造成性能瓶颈。
下面我们来分析下直接写入文件的流程:
用户发起 write 操作
操作系统查找页缓存
a.若未命中,则产生缺页异常,然后创建页缓存,将用户传入的内容写入页缓存
b.若命中,则直接将用户传入的内容写入页缓存用户 write 调用完成
页被修改后成为脏页,操作系统有两种机制将脏页写回磁盘
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 高性能日志写入方案的更多相关文章
- React Native学习(八)—— 对接七鱼客服
本文基于React Native 0.52 Demo上传到Git了,有需要可以看看,写了新内容会上传的.Git地址 https://github.com/gingerJY/React-Native-D ...
- Android 优质精准的用户行为统计和日志打捞方案
Android 自定义优质精准的用户行为和日志打捞方案 Tamic csdn博客 :http://blog.csdn.net/sk719887916/article/details/51398416 ...
- 【腾讯Bugly干货分享】微信mars 的高性能日志模块 xlog
本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/581c2c46bef1702a2db3ae53 Dev Club 是一个交流移动 ...
- 【腾讯Bugly干货分享】微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ff5932cde42f1f03de29b1 本文来源: 微信客户端开发团队 ...
- 七、Android学习第六天——SQLite与文件下载(转)
(转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 七.Android学习第六天——SQLite与文件下载 SQLite SQ ...
- 大叔也说Xamarin~Android篇~日志的记录
回到目录 无论哪个平台,开始哪种应用程序,日志总是少不了的,大家在Lind.DDD里也可以看到大叔的日志组件,而在xamarin进行移动开发时,为了更好的调试,记录运行的情况,日志也是必须的,这讲主要 ...
- Android将Log写入文件
为什么要将Log写入文件 运行应用程序的时候,大多数是不会连接着IDE的: 而当应用程序崩溃时,我们需要收集复现步骤,在设备上复现,并进行Debug: 而由于Android手机的多样性,有些问题是某个 ...
- Android输出日志Log类
android.util.Log常用的方法有以下5个: Log.v() Log.d() Log.i() Log.w() 以及 Log.e().根据首字母分别对应VERBOSE,DEBUG,INFO,W ...
- Android app日志保存功能
每一个App应用应该都需要有日志保存的功能,日志保存可以记录App运行中所遇到的问题,查Bug也比较方便 等等: Android日志保存功能,保存某几天的最新日志文件到某个目录,直接看是如何代码实现的 ...
随机推荐
- Codeforces 962D - Merge Equals
链接: http://codeforces.com/problemset/problem/962/D 题意: 给出一个整数序列.选择其中最小且出现两次(或以上)的数,把最左边的两个从序列中移除,然后把 ...
- 利用SPF记录缺失发送伪造邮件
SPF,也就是 Sender Policy Framework 的缩写,是一种以IP地址认证电子邮件发件人身份的技术,是非常高效的垃圾邮件解决方案. 如何查询所属域名邮箱的SPF记录? 查询的结果,从 ...
- stixel上边缘
上图是2^x-1的曲线,取值范围在(-1,正无穷) 上面两个公式组成了隶属函数(membership)表示隶属度,隶属度就是衡量这个点同下边缘点是否属于同一个物体.实际上M函数就是2^x-1,但M函数 ...
- springmvc进阶(5):mvc:default-servlet-handler详解
我们在配置dispatchServlet时配置<url-pattern>/</url-pattern>拦截所有请求,这时候dispatchServlet完全取代了default ...
- 【题解】洛谷P4281 [AHOI2008] 紧急集合(求三个点LCA)
洛谷P4281:https://www.luogu.org/problemnew/show/P4281 思路 答案所在的点必定是三个人所在点之间路径上的一点 本蒟蒻一开始的想法是:先求出2个点之间的L ...
- 身份认证系统(三)什么是OAuth2
本文准备用最简单的语言告诉大家什么是OAuth2 ,OAuth2是干什么的. 我们有一个资源服务器,资源服务器中有一系列的用户数据. 现在有一个应用想想要获取我们的用户数据. 那么最简单的方法就是我们 ...
- HTML基础之常用标签
Meta 标签介绍 Meta的属性有两种:name和http-equiv name属性用于描述网页,对应于content <meta name="Generator" con ...
- SQLMAP使用详解
使用示例 python sqlmap.py -u "http://xx.com/member.php?id=XX" -p id --dbms "Mysql" ...
- 『ACM C++』 Codeforces | 1066A - Points in Segments
大一生活真 特么 ”丰富多彩“ ,多彩到我要忙到哭泣,身为班长,很多班级的事情需要管理,也是,什么东西都得体验学一学,从学生会主席.团委团总支.社团社长都体验过一番了,现在差个班长也没试过,就来体验了 ...
- Linux每日一命令:【00】总纲
Linux每日一命令更新频率为每周5篇. 文章结构如下: 简介 语法 选项 参数 常用实例 实用技巧(可选) 参考文档 文章目录如下: 2018-02-19 20:15 -- Linux每日一命令:[ ...