一、背景
隨著公司應用的逐漸增多,需要集中收集公司部分應用線上運行的一些崩潰數據和日志來進行分析處理,在此實踐過程中了解到系統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系統一些不常見的服務有一個了解。