作者:vivo 互联网客户端团队- Ma Lian
借助系统DropBoxManagerService对于系统文件目录dropbox管理的设计,了解其文件管理的规则、运行机制、读写机制、管控机制,根据其设计一个客户端日志文件管理与上报功能
一、背景
随着公司应用的逐渐增多,需要集中收集公司部分应用线上运行的一些崩溃数据和日志来进行分析处理,在此实践过程中了解到系统data/system/dropbox目录会生成所有应用的相关日志文件。
这个目录是由Android系统服务之一DropBoxManagerService来管理,所以由此详细阅读了DropBoxManagerService相关的源码,以下简称DBMS。
DBMS可能是Android系统服务源码较少的一个,所以阅读起来相对比较简单,阅读之后发现,其实这就是一个简易的日志文件管理服务。
我们在对应用本地的部分日志文件进行记录和管理的时候,恰巧可以借鉴DBMS源码对于文件管理的设计方案。
假设不读源码,如果我们自己设计日志文件管理系统,应该需要考虑哪些?
除了最基础的获取各类日志文件的方案,我们针对文件管理可以提出几个需要考虑的点:
-
存取日志采用何种策略
-
设计哪些防呆策略
-
是否需要对外提供接口,提供哪些接口
-
如何保证性能
-
多进程的问题如何解决
-
文件丢失该如何处理
-
文件变化如何通知使用方
我们带着以上问题来对DBMS进行一个了解。
二、DropBoxManagerService简介
DropBoxManagerService是Android系统的服务之一,采用C/S结构:
-
Client端:DropboxManager,用于对应用层提供接口。
-
Server端:DropBoxManagerService,管理系统目录(data/system/dropbox)的系统服务。
-
系统Setting数据库:负责管理DBMS的一些配置信息。
整体架构关系如下图所示:
2.1 DropBox目录简介
这个目录的目录结构如下图所示:
里面存放的都是系统的一些日志文件,针对不同类型的文件,文件名称和后缀也有所不同。
2.1.1 文件格式
-
tag:代表日志类型,常见的tag:data_app_anr,system_app_crash,data_app_nativecrash,其中data_app表示普通应用,system_app表示系统应用。
-
timeStampMillis:日志的时间戳,一般情况下等于崩溃的时间,有些情况下系统会做一些调整。
-
extentions:后缀名,常见的文件后缀名:.txt,.lost,.txt.gz,.tmp,一般的日志文件都是.txt或者.txt.gz,文件被删除后的记录会以.lost命名
这种文件命名方式优点是可以一眼看出这是什么类型的文件。
2.1.2 常见的文件
-
JE文件:[email protected]
-
NE文件:[email protected]
-
ANR文件:[email protected]
还包括一些系统其它的错误日志,内存,重启相关的等等。
2.2 提供的接口
2.2.1 添加文件
addData/addFile/addEntry
2.2.2 获取文件
getNextEntry,根据tag和时间戳来获取想要的文件。
2.2.3 dump目录信息
获取DropBox目录的一些信息:文件个数,文件列表,文件详细信息等,可以通过命令行操作(dumpsys dropbox)。
$ dumpsys dropbox
Drop box contents: 131 entries
Max entries: 1000
// 以下省略......
2.2.4 其它CMD命令
提供其他一些CMD操作的命令,如set-rate-limit,add-low-priority等等。
2.3 目录管控配置
2.3.1 默认基础配置及文件清除策略
这些配置存在系统的setting数据库里面,可以通过settings.global来访问配置。
文件存储的配置主要包括以下几个维度:
-
文件存活时长(默认3天);
-
最大存储文件数量(默认1000个);
-
低内存情况下最大文件数量(默认300个);
-
DropBox目录所能使用的空间(默认10MB);
-
DropBox目录最多占可用存储(可用存储=系统可用存储-系统总存储*预留比例)的比例(10%);
-
DropBox使用需要预留的存储占总存储的比例(10%);
-
清除空间时扫描磁盘空间的时间间隔;
-
需要压缩的最小文件大小。
根据以上配置,我们可以知道该目录下的日志文件清除策略,触发配置上限后会及时的删除文件。
在以下三种情况会执行文件清除策略,防止DropBox占用太多的空间:
-
设备低内存;
-
setting配置发生变更;
-
添加文件。
同时在添加文件的时候,超过配置的可占用空间,会被丢弃。
/**
* Trims the files on disk to make sure they aren't using too much space.
* @return the overall quota for storage (in bytes)
*/
private synchronized long trimToFit() throws IOException {
return mCachedQuotaBlocks * mBlockSize;
}
2.3.2 文件删除及标记处理策略
在上述策略不满足后,部分文件会被删除,删除后,会在DropBox添加一个.lost的空文件标记被删除的文件。
2.3.3 文件类型管控
DropBoxMangerService对于可存储的文件类型也有控制,主要是对于TAG的控制。
public boolean isTagEnabled(String tag) {}
2.3.4 权限管控
使用DropBox需要READ_LOGS权限和PACKAGE_USAGE_STATS两个权限。
2.4 读写策略
这块涉及到DBMS几个关键方法和属性,主要涉及到初始化(init),添加文件(addEntry),获取文件(getNextEntry),文件类型(EntryFile)。
DBMS作为系统服务会由SystemServer启动,添加文件(addEntry)和获取文件(getNextEntry)在调用时会先进行初始化(init)。
其中每个文件都会转换成一个EntryFile类来管理,关系见下图:
下面了解一下初始化,EntryFile,添加文件和获取文件的具体内容:
2.4.1 初始化
初始化会将DropBox文件列表缓存到内存中。
/** If never run before, scans disk contents to build in-memory tracking data. */
private synchronized void init() throws IOException {
// 省略代码......
File[] files = mDropBoxDir.listFiles(); // 列出所有文件
for (File file : files) {
EntryFile entry = new EntryFile(file, mBlockSize); // 一个日志文件对应一个EntryFile对象
enrollEntry(entry); // 加入到mAllFiles
}
}
初始化的时机:
-
设备存储容量低广播回调
-
设置配置项修改
-
添加日志文件
-
获取日志文件
-
dump 命令行列出DropBox的一些内容
2.4.2 EntryFile文件属性
每个文件对应一个EntryFile,用block数来统计大小,DBMS涉及的读写都是根据磁盘的blockSize来进行,效率会更高。
static final class EntryFile implements Comparable<EntryFile> {
public final String tag; // 日志文件的tag,类型
public final long timestampMillis; // 日志文件的时间戳
public final int flags; // 日志文件的flag,标志TEXT,EMPTY,GZIPPED
public final int blocks; // 存放文件的块数
}
2.4.3 添加文件
添加一个日志文件,常见的在Ams中的addErrorToDropBox方法调用。
添加文件管控策略
① .lost的文件格式不允许添加。
// 如果添加.lost的文件,抛异常
if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
② 配置不允许记录的TAG,不会被添加。
// 从设置里面读取这个tag是否被允许记录
if (!isTagEnabled(tag)) return;
③ 根据系统设置的磁盘块大小进行写入,提高写入效率。
int bufferSize = mBlockSize;
④ 异常时间戳文件矫正:写入文件前会将超过当前时间10s的文件修改时间后重新命名并加入到缓存文件列表中。
// 找出当前时间10s之后的所有文件
SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
EntryFile[] future = null;
if (!tail.isEmpty()) {
future = tail.toArray(new EntryFile[tail.size()]);
tail.clear(); // 从文件列表中mAllFiles清除掉超过当前时间的
}
// 省略代码......
for (EntryFile late : future) {
if ((late.flags & DropBoxManager.IS_EMPTY) == 0) { // 将这些超过当前时间的文件重命名,时间戳依次+1,并且重新加入到mAllFiles中
enrollEntry(new EntryFile());
}
}
⑤ 添加文件的顺序,先创建临时文件,然后使用文件的rename方法,rename方法是原子操作,保证并发操作的安全。
// 通过rename方法保存文件,保证并发操作的安全
temp.renameTo(file))
⑥ 文件添加完成之后通过发送广播通知,广播分为实时广播和延迟广播,延迟广播用来通知优先级较低的文件。
//低优先级的可以发送延时广播
mHandler.maybeDeferBroadcast(tag, time);
//高优先级的发送实时广播
mHandler.sendBroadcast(tag, time);
2.4.4 获取文件
DBMS获取文件的逻辑比较简单,根据方法名getNextEntry(String tag, long millis,...)我们可以见名知意,主要根据使用者传入的时间戳,找出这个时间戳往后的第一个文件。
for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
return new DropBoxManager.Entry(entry.tag, entry.timestampMillis, file, entry.flags);
}
2.5 源码阅读总结
2.5.1 回答我们阅读前提出的问题
① 存取日志的策略
-
会在低存储,添加获取文件等时机将文件列表初始化到内存中。
② 设计哪些防呆策略
-
提供了文件大小,存储占比等限制。
-
会在低存储,配置更改的时候清除文件。
-
配置保存在setting中,然后通过ContentObserver来监听配置变化。
③ 对外提供哪些接口
-
提供添加获取,以及cmd命令相关的接口,开发调试都能兼顾。
④ 如何保证性能
-
从源码的注解可以看出,目前每个Entry无论大小都对应一个文件效率是比较低,源码也列出了TODO,考虑用单文件队列来优化。
// TODO: This implementation currently uses one file per entry, which is
// inefficient for smallish entries -- consider using a single queue file
// per tag (or even globally) instead.
-
采用文件系统块大小来读写来提高效率。
⑤ 多进程的问题如何解决
-
文件操作都是先写temp,然后采用rename的方案来保证原子操作从而保证并发操作的安全。
-
addEntry和getNextEntry都做了加锁处理。
⑥ 文件丢失该如何处理
-
文件被删除后,会用一个同名的空文件来替代,从而标记有文件被删除了。
⑦ 文件变化如何通知使用方
-
通过发广播的方式来通知外界,针对不同优先级的文件又设置实时和延时广播。
2.5.2 其它点
-
文件存储不光限制大小,也会限制文件类型
-
文件不是全部压缩的,超过一定大小的文件会进行压缩
-
文件命名有讲究,包含了应用类型,崩溃信息,发生时间等相关信息
-
文件获取是根据时间戳先后来获取的,对于时间戳异常的文件会进行时间上的调整
2.5.3 作为使用者的看法
当然,我在使用源码的过程中,也发现我个人觉得可以优化的点。
-
在使用中,部分文件命名应该加上包名,类似应用产生的崩溃文件,可以按包名区分文件,对使用更友好,当然这个设计的初衷是给系统统一使用,可能不对外开放。
-
权限管控过于单一,对于业务本身的一些异常日志,应当支持自由查看。
-
这些文件的信息应该用数据库维护起来更好,方便使用者用,当然可能设计可能会变得更复杂,不够简约。
三、源码阅读应用–日志文件管理&上报设计
3.1 概述
背景:
部分应用希望上报应用运行时的一些日志,包括运行时log,崩溃log,Hprof内存快照,捕获异常等等
需求:
需要设计一套客户端的日志文件收集、管理及上报一个功能
参考:
-
日志保存管理方案可以参考DBMS中的一些策略
-
日志上传方案参考业内已有的一些优秀模型
3.2 方案
整体方案方案采用生产者-消费者模型,其中几个关键节点:
-
生产者:应用的多个进程,他们可能会生成不同类型的日志,并写入到指定的文件目录
-
临时文件目录:根据文件类型、优先级设置不同目录来存放临时文件
-
上报数据目录:临时文件目录中的文件会通过rename方案写到上报数据目录
-
消费者:上报进程,上报进程会通过FileObserver监听变化,从而来上报文件
整体的流程图如下:
3.3 确定对外接口
-
获取文件的接口
-
存文件的接口
-
统计文件(类型,数量)的接口
-
更改部分配置策略的接口
-
主动上报的接口
-
其它自定义参数的接口
3.4 确定收集管控策略
-
是否允许收集:该配置关闭后,本地不会执行任何收集行为
-
日志存储目录:私有目录固化出一个空间
-
文件命名方式:参照DBMS,进程名_日志类型_前后台@时间戳.txt.gz
-
日志类型开关:每个日志类型设置是否允许手机
-
收集日志类型:崩溃日志,运行时日志,内存快照,捕获日志,其它自定义日志等
-
日志存活时长:参照DBMS,超过一定时间,则删除文件
-
日志存储空间:参照DBMS,设置一个手机可用存储的比例·
-
日志文件数量:超过指定数量,则删除部分文件;参照DBMS,当可用存储较低的情况,应该存储更少的文件数量
-
其余初始化的一些时机,同样参考DBMS
3.5 确定上报管控策略
-
是否允许上报,该配置关闭后,不允许上报行为
-
是否允许在流量情况下上报,该配置设置不允许后,只允许在wifi情况下上报
-
流量情况下单次、单日、单月最多可上报的文件大小,该配置控制流量情况下,应用在上报时可以上报的文件大小
-
wifi情况下单次、单日、单月最多可上报的文件大小,该配置控制wifi情况下,应用在上报时可以上报的文件大小
-
上报间隔时间,该配置控制低优先级的文件上报时间间隔
-
上报失败次数限制,该配置控制在失败一定次数以后,不再允许上报
-
上报优先级(低优先级的日志无需频繁上报)
-
弱网络情况本次上报的文件大小
-
单次、单日、单月允许使用的流量大小,该配置控制应用在上报时可以使用的流量大小
-
可上报的最低电量限制,该配置控制上报情况下最小电量限制
3.6 收集日志方案
-
DropBox日志:先读取到本地,然后存储上报
-
运行时日志:利用adb logcat命令输出日志到本地储存上
-
内存快照:dump Hprof文件,然后进行一些裁剪,以便于能够以更小的体积上传
-
其它日志:实时输出记录到本地,按需上报
以上具体方案不作为本次重点,不再详述。
3.7 写入日志方案
通过网络课程的学习,了解到mmap的性能非常高,所以最终采用“多进程写+mmap”的方案,并且避免了跨进程的调用堆积,效率很高
3.8 上报日志方案
参照DBMS添加文件的实时和延时通知方案,上报也分为实时上报和延时上报
-
实时上报:出现一份日志,就直接上报,针对重要性较高的日志
-
延时上报:达到一定数量,或者达到一定时间进行上报
3.9 数据监控
3.9.1 质量监控
3.9.2 容灾监控
四、总结
本文主要讲了两块内容:
1、DropBoxManagerService源码阅读与解析,包括接口设计、文件存储的管控机制和策略,多进程的处理,异常防呆机制
2、应用日志收集与上报方案,主要参考DropBoxManagerService源码的设计
我们经常强调源码阅读,源码究竟能给我们带来什么呢?我认为主要有以下几点:
-
编码技术的提升
-
分析问题的思路
-
解决方案的设计
-
设计模式的应用
本文抛砖引玉,借助以上案例简单地讲了一下DBMS源码以及源码阅读的应用,希望在源码阅读方面能够带给大家一些启发,同时对Android系统一些不常见的服务有一个了解。
参考:
-
Android12.0《DropBoxMangerService源码》
-
极客时间《Android开发高手课》关于高性能上报方案和高性能I/O方案两节