基础认识
mars 是微信官方的终端基础组件
- C++ 编写(为了兼容多平台)
- 业务性无关,平台性无关的基础组件
- 支持接入 Android 或者 iOS/OS X 或者 Windows
- comm:可以独立使用的公共库,包括 socket、线程、消息队列、协程等;
- xlog:高可靠性高性能的运行期日志组件;
- SDT: 网络诊断组件;
- STN: 信令分发网络模块,也是 Mars 最主要的部分。
优势
对于终端设备来说,打日志并不只是把日志信息写到文件里这么简单。除了流畅性,** 完整性, 容错性,还有一个最重要的是安全性**。基于不怕被破解,但也不能任何人都能破解的原则, 对日志的规范比加密算法的选择更为重要,所以这里并没有讨论这一点。
一个优秀的终端日志模块无论怎么设计都必须做到:
- 不能把用户的隐私信息打印到日志文件里,不能把日志明文打到日志文件里。
- 不能影响程序的性能。最基本的保证是使用了日志不会导致程序卡顿。
- 不能因为程序被系统杀掉,或者发生了 crash,crash 捕捉模块没有捕捉到导致部分时间点没有日志, 要保证程序整个生命周期内都有日志。
- 不能因为部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。
上面这几点也即一直强调的 安全性 流畅性 完整性 容错性, 它们之间存在着矛盾关系:
- 如果直接写文件会卡顿,但如果使用内存做中间 buffer 又可能丢日志
- 如果不对日志内容进行压缩会导致 IO 卡顿影响性能,但如果压缩,部分损坏可能会影响整个压缩块,而且为了增大压缩率集中压缩又可能导致 CPU 短时间飙高。
mars 的日志模块 xlog 就是在兼顾这四点的前提下做到:高性能高压缩率、不丢失任何一行日志、避免系统卡顿和 CPU 波峰。
使用(Android)
gradle 接入我们提供了两种接入方式:mars-wrapper 或者 mars-core。如果你只是想做个 sample 推荐使用 mars-wrapper,可以快速开发;但是如果你想把 mars 用到你的 app 中的话,推荐使用 mars-core,可定制性更高。
mars-wrapper
在 app/build.gradle 中添加 mars-wrapper 的依赖:
dependencies {
compile 'com.tencent.mars:mars-wrapper:1.2.0'
}
或者
mars-core
在 app/build.gradle 中添加 mars-core 的依赖:
dependencies {
compile 'com.tencent.mars:mars-core:1.2.2'
}
或者
mars-xlog
如果只想使用 xlog,可以只加 xlog 的依赖(mars-core,mars-wrapper 中都已经包括 xlog):
dependencies {
compile 'com.tencent.mars:mars-xlog:1.0.7'
}
接着往下操作之前,请先确保你已经添加了 mars-wrapper 或者 mars-core 或者 mars-xlog 的依赖
Xlog Init
在程序启动加载 Xlog 后紧接着初始化 Xlog。但要注意如果你的程序使用了多进程,不要把多个进程的日志输出到同一个文件中,保证每个进程独享一个日志文件。而且保存 log 的目录请使用单独的目录,不要存放任何其他文件防止被 xlog 自动清理功能误删。
System.loadLibrary("c++_shared");
System.loadLibrary("marsxlog");
final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
final String logPath = SDCARD + "/marssample/log";
// this is necessary, or may crash for SIGBUS
final String cachePath = this.getFilesDir() + "/xlog"
//init xlog
if (BuildConfig.DEBUG) {
Xlog.appenderOpen(Xlog.LEVEL_DEBUG, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
Xlog.setConsoleLogOpen(true);
} else {
Xlog.appenderOpen(Xlog.LEVEL_INFO, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
Xlog.setConsoleLogOpen(false);
}
Log.setLogImp(new Xlog());
程序退出时关闭日志:
Log.appenderClose();
如果你想修改 Xlog 的加密算法或者长短连的加解包部分甚至更改组件的其他部分,可以参考这里
Log生成完毕后,会在指定的路径下生成相应的日志文件:
shell@R7:/sdcard/marssample/log $ ll
-rw-rw---- root sdcard_r 153600 2016-12-30 17:06 MarsSample.mmap2
-rw-rw---- root sdcard_r 29633 2016-12-30 17:06 MarsSample_20161230.xlog
其中MarsSample.mmap2是缓存文件,不用关心,我们需要的是.xlog文件,我们把这个文件pull出来,使用Mars提供的Python脚本进行解密。
找到Mars源码log/crypt/decode_mars_log_file.py下的这个文件,执行:
➜ mars_xlog_sdk python decode_mars_log_file.py ~/Downloads/log/MarsSample_20161230.xlog
即可生成对应的log文件,用Sublime即可打开
常用 API 说明
Xlog.java
public static native void appenderOpen(int level, int mode, String cacheDir, String logDir, String nameprefix, int cacheDays, String pubkey);
初始化日志需要调用的接口。
-
level: 日志级别,变量见 Xlog.java 里
LEVEL_XX
, Debug版本推荐LEVEL_DEBUG
, Release 版本推荐LEVEL_INFO
。 -
mode : 文件写入模式,分异步和同步,变量定义见 Xlog.java 里
AppednerModeXX
, Release版本一定要用AppednerModeAsync
, Debug 版本两个都可以,但是使用 AppednerModeSync 可能会有卡顿。 - cacheDir : 缓存目录,当 logDir 不可写时候会写进这个目录,可选项,不选用请给 "", 如若要给,建议给应用的 /data/data/packname/files/log 目录。
- logDir : 日志写入目录,请给单独的目录,除了日志文件不要把其他文件放入该目录,不然可能会被日志的自动清理功能清理掉。
- nameprefix : 日志文件名的前缀,例如该值为TEST,生成的文件名为:TEST_20170102.xlog。
- cacheDays : 一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。
-
pubkey : 加密所用的 pub_key,具体可参考 Xlog 加密指引。
public static native void setConsoleLogOpen(boolean isOpen);
- 是否会把日志打印到 logcat 中, 默认不打印。
- isOpen : true 为打印,false为不打印。
Log.java
该文件包含打印日志最常调用的接口。
public static void setLogImp(LogImp imp)
设置 Log 的具体实现,这里必须调用 Log.setLogImp(new Xlog());
日志才会写到 Xlog 中。
public static void appenderFlush(boolean isSync)
当日志写入模式为异步时,调用该接口会把内存中的日志写入到文件。
- isSync : true 为同步 flush,flush 结束后才会返回。 false 为异步 flush,不等待 flush 结束就返回。强制将缓存中的输出流(字节流,字符流等)强制输出.
public static void appenderClose()
关闭日志,在程序退出时调用。
public static void f/e/w/i/d(final String tag, final String msg)
写日志时调用的接口,对应不同级别的日志。
问题
重要技术点
技术实现思路:使用流式方式对单行日志进行压缩,压缩加密后写进作为 log 中间 buffer的 mmap 中
技术水准有多牛逼,就看这个
mmap
如果大家对binder的实现有看过就知道,binder就是使用mmap来实现一次拷贝的跨进程的。
mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。
等于写内存的速度:把512 Byte的数据分别写入150 kb大小的内存和 mmap,以及磁盘文件100w次并统计耗时
xlog实现方案:先压缩再加密效率比较高,这个顺序不能改变。而且在写入 mmap 之前先进行压缩,也会减少所占用的 mmap 的大小,进而减少 mmap 所占用内存的大小。就是其他模块每写一行日志日志模块就必须进行压缩。
压缩方案
第三种是把整个 app 生命周期作为一个压缩单位进行压缩,如果这个压缩单位中有数据损坏,那么后面的日志也都解压不出来。
但其实在短语式压缩过程中,滑动窗口并不是无限大的,一般是 32kb ,所以只需要把一定大小作为一个压缩单位就可以了。这也就是第四个方案, 这样的话即使压缩单位中有部分数据损坏,因为是流式压缩,并不影响这个单位中损坏数据之前的日志的解压,只会影响这个单位中这个损坏数据之后的日志。
使用流式压缩后,和之前使用通用压缩的的日志方案进行了对比(耗时为单行日志的平均耗时):
多条日志同时压缩会导致 CPU 曲线短时间内极速升高,进而可能会导致程序卡顿,而流式压缩是把时间分散在整个生命周期内,CPU 的曲线更平滑,相当于把压缩过程中使用的资源均分在整个 app 生命周期内。虽说时间上升了,但是分摊后CPU曲线更为平滑,实际上是性能的提升。
参考内容
github Mars 项目
mars Android 接入指南
聊聊微信 Xlog
【Dev Club 分享】微信mars 的高性能日志模块 xlog