本文来自网易云社区

作者:网易七鱼 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. 【[USACO08NOV]奶牛混合起来Mixed Up Cows】

    首先我们能够一眼看到4 <= N <= 16,那么就是它了,我们要压缩的状态就是它了 那么之后能我们用这个状态表示什么呢,我们要表示的显然是每只奶牛是否在队伍中 比如说10吧,转成二进制后 ...

  2. Educational Codeforces Round 55 (Rated for Div. 2) C. Multi-Subject Competition 【vector 预处理优化】

    传送门:http://codeforces.com/contest/1082/problem/C C. Multi-Subject Competition time limit per test 2 ...

  3. MyBatis(3)开发dao方法

    本次全部学习内容:MyBatisLearning       SqlSession         SqlSession是一个面向用户(程序员)的接口.         SqlSession中提供了很 ...

  4. 支持向量机通俗导论(理解SVM的三层境界)[转]

    作者:July .致谢:pluskid.白石.JerryLead.说明:本文最初写于2012年6月,而后不断反反复复修改&优化,修改次数达上百次,最后修改于2016年11月.声明:本文于201 ...

  5. dva框架使用详解及Demo教程

    dva框架的使用详解及Demo教程 在前段时间,我们也学习讲解过Redux框架的基本使用,但是有很多同学在交流群里给我的反馈信息说,redux框架理解上有难度,看了之后还是一脸懵逼不知道如何下手,很多 ...

  6. tomcat启动startup.bat一闪而过【亲测有效】

    遇到很多次运行startup.bat后,一个窗口一闪而过的问题,但是从来没去纠正怎样修改配置才是正确的,现在从网上查阅的资料整理如下:tomcat在启动时,会读取环境变量的信息,需要一个CATALIN ...

  7. 复习宝典之MyBatis

    查看更多宝典,请点击<金三银四,你的专属面试宝典> 第五章:MyBatis MyBatis是一个可以自定义SQL.存储过程和高级映射的持久层框架. 1)创建sqlsession的流程 my ...

  8. xtrabackup全量备份+binlog基于时间点恢复

    1.通过xtrabackup的备份恢复数据库. 2.找到start-position和binlog名称 cat xtrabackup_info 3.导出mysqlbinlog为sql文件,并确定恢复的 ...

  9. diff命令--比较两个文件的命令

    可以使用 --brief 来比较两个文件是否相同,使用 -c参数来比较这两个文件的详细不同之处,这绝对是判断文件是否被篡改的有力神器,

  10. vue使用axios调用豆瓣API跨域问题

    最近做了一个vue小demo,使用了豆瓣开源的API,通过ajax请求时需要跨域才能使用.   封面.jpg 一.以下是豆瓣常用的开源接口: 正在热映 :https://api.douban.com/ ...