随着公司应用的逐渐增多,需要集中收集公司部分应用线上运行的一些崩溃数据和日志来进行分析处理,在此实践过程中了解到系统data/system/dropbox目录会生成所有应用的相关日志文件。
这个目录是由Android系统服务之一DropBoxManagerService来管理,所以由此详细阅读了DropBoxManagerService相关的源码,以下简称DBMS。
DBMS可能是Android系统服务源码较少的一个,所以阅读起来相对比较简单,阅读之后发现,其实这就是一个简易的日志文件管理服务。
我们在对应用本地的部分日志文件进行记录和管理的时候,恰巧可以借鉴DBMS源码对于文件管理的设计方案。
假设不读源码,如果我们自己设计日志文件管理系统,应该需要考虑哪些?
除了最基础的获取各类日志文件的方案,我们针对文件管理可以提出几个需要考虑的点:
我们带着以上问题来对DBMS进行一个了解
DropBoxManagerService是Android系统的服务之一,采用C/S结构:
整体架构关系如下图所示:
这个目录的目录结构如下图所示:
里面存放的都是系统的一些日志文件,针对不同类型的文件,文件名称和后缀也有所不同。
2.1.1 文件格式
tag@timeStampMillis.extentions
这种文件命名方式优点是可以一眼看出这是什么类型的文件。
2.1.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.1 默认基础配置及文件清除策略
这些配置存在系统的setting数据库里面,可以通过settings.global来访问配置。
文件存储的配置主要包括以下几个维度:
根据以上配置,我们可以知道该目录下的日志文件清除策略,触发配置上限后会及时的删除文件。
在以下三种情况会执行文件清除策略,防止DropBox占用太多的空间:
同时在添加文件的时候,超过配置的可占用空间,会被丢弃。
/**
* 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两个权限。
这块涉及到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
}
}
初始化的时机:
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.1 回答我们阅读前提出的问题
① 存取日志的策略
② 设计哪些防呆策略
③ 对外提供哪些接口
④ 如何保证性能
// 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.
⑤ 多进程的问题如何解决
⑥ 文件丢失该如何处理
⑦ 文件变化如何通知使用方
2.5.2 其它点
2.5.3 作为使用者的看法
当然,我在使用源码的过程中,也发现我个人觉得可以优化的点。
背景:
部分应用希望上报应用运行时的一些日志,包括运行时log,崩溃log,Hprof内存快照,捕获异常等等
需求:
需要设计一套客户端的日志文件收集、管理及上报一个功能
参考:
整体方案方案采用生产者-消费者模型,其中几个关键节点
整体的流程图如下:
以上具体方案不作为本次重点,不再详述。
通过网络课程的学习,了解到mmap的性能非常高,所以最终采用“多进程写+mmap”的方案,并且避免了跨进程的调用堆积,效率很高
参照DBMS添加文件的实时和延时通知方案,上报也分为实时上报和延时上报
3.9.1 质量监控
3.9.2 容灾监控
本文主要讲了两块内容:
1、DropBoxManagerService源码阅读与解析,包括接口设计、文件存储的管控机制和策略,多进程的处理,异常防呆机制
2、应用日志收集与上报方案,主要参考DropBoxManagerService源码的设计
我们经常强调源码阅读,源码究竟能给我们带来什么呢?我认为主要有以下几点:
本文抛砖引玉,借助以上案例简单地讲了一下DBMS源码以及源码阅读的应用,希望在源码阅读方面能够带给大家一些启发,同时对Android系统一些不常见的服务有一个了解。