作者: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@timeStampMillis.extentions
- 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 常見的檔案
還包括一些系統其它的錯誤日誌,記憶體,重啟相關的等等。
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方案兩節