一、前言:移動端為什麼要三方日誌系統
日誌系統用於記錄使用者行為和資料以及崩潰時的執行緒呼叫棧,以幫助程式設計師解決問題,優化使用者體驗。
iOS系統就有自帶Crash收集應用程式“ReportCrash”來收集App Crash資訊,我也深入瞭解過iOS收集Crash 資訊的過程並記錄在此 CPU發生異常到生成Crash Log的過程 , 但使用者遇到的很多問題不僅僅是Crash,更何況有些情況僅靠Crash Log並不能定位Crash,而且ReportCrash 收集的Crash資訊還需要使用者同意才可以和開發者共享。為了說明使用者日誌的重要性,這裡引入一個faceu 團隊-輕顏相機-Tom哥的調侃
“譬如使用者反饋,拍照偏黃,中間經過了十幾個渲染管線,沒有log真呵呵,你又不可能讓使用者再拍一次”
因此大多數App 都帶有Crash收集框架和日誌收集框架,而Crash資訊也是日誌資訊的一種,為什麼要分成兩個框架去收集呢?因為資訊採集方式不一樣,Crash收集框架通過捕獲系統傳送來的 Mach 異常和 Unix 訊號進行資訊採集、而日誌收集框架是程式設計師主動程式碼觸發的資訊採集,資訊採集部分共用程式碼很少,所以分成兩個框架也更易於維護。
這裡介紹的日誌系統是收集非Crash 資訊的日誌系統。該系統分為三部分:
- 採集部分:使用微信開源的日誌採集元件XLog,該元件具有安全性、流暢性、完整性、容錯性 的優點。
- 傳輸部分:使用Spring Boot 開發Web伺服器,Web伺服器提供簡單的檔案上傳和下載、檔案上傳白名單獲取和設定的功能。
- 管理部分:使用Ant Design Pro 框架來開發後臺管理系統,在管理系統中提供頁面進行設定日誌上傳白名單,及日誌下載。
下面和大家聊一聊我技術選型的過程
二、技術選型:本日誌系統的技術棧
都說技術服務業務,考慮技術選型當然離不開業務需求。
其實團隊專案之前就有日誌系統,沒有使用三方框架,而是自己寫了一些簡單的策略,如按優先順序寫檔案或上報伺服器。但仍滿足不了客戶端開發的需求
- 有個致命的缺點是:找日誌看的話沒有視覺化平臺,日誌上報的介面對應的服務端同學早就離職了,而找別的服務端同學幫撈日誌比較費時費力。
- 其次,日誌明文寫檔案保證不了安全性、閃退時無法保證Log的 完整性 、本地日誌管理策略也沒有對檔案數量、單個檔案大小做限制
- 再者,日誌只輸出到console和檔案,想要資料通過web socket實時輸出到web頁面展示的話,程式碼層面不易於擴充套件,這個功能可以幫助在真機除錯push、測試同學實時檢視埋點資訊。
開發有時間限制,需求也就有優先順序之分
2.1 最高優先順序需求:穩定的日誌傳輸服務和視覺化的日誌管理
服務端和前端同學人力緊張,而且這個不是掙錢的產品需求,不一定能爭取到排期讓服務端和前端同學支援“穩定的日誌傳輸服務和視覺化的日誌管理”這個需求,而且我們的Crash 收集和檢視工具都是用的騰訊的,不是同一個公司同一個部門,更別指望他們來支援了,只能網上尋找解決方案。
2.1.1 Seafile 個人網盤 + CocoaLumberjack 日誌採集
本人服務端開發經驗匱乏,只搞過vps搭建vpn和部落格,並沒有玩過web伺服器,找到一個不需要搭建web伺服器的方案。伺服器搭建Seafile 個人網盤服務
Seafile,是一套中國國產的開源、專業、可靠的雲端儲存專案管理軟體,解決檔案集中儲存、共享和跨平臺訪問等問題。正式釋出於2012年10月。除了一般網盤所提供的雲端儲存以及共享功能外,Seafile還提供訊息通訊、群組討論等輔助功能,幫助員工更好的圍繞檔案展開協同工作,已有10多萬使用者使用。
Seafile 是比較成熟的方案了,還提供介面來上傳、下載檔案,趕緊買個10塊/月的騰訊雲學生機搭個Seafile 個人網盤驗證一下。
半天業餘時間就解決了“穩定的日誌傳輸服務和視覺化的日誌管理“,接著又調研了下客戶端日誌採集框架,發現 CocoaLumberjack start數量很多,最近還有提交記錄,而且框架設計得易於擴充套件,已經有基於CocoaLumberjack 去解決“有通過web socket實時輸出log到web頁面”問題的方案,另外,CocoaLumberjack 還支援對檔案數量、單個檔案大小的設定。CocoaLumberjack 這個方案看著挺好,馬上又擼了個Demo去驗證。
拿著Demo去和同事討論,總結了幾個問題:
-
Seafile,高定製化犧牲了可擴充套件性
- 除了檔案上傳下載外,無法自定義介面來進行更多的C/S互動,比如客戶端詢問服務端日誌上傳型別,是上傳使用者日誌還是上傳資料庫檔案,是傳送到微信好友還是上傳到伺服器。
- 資料儲存孤獨,無法和其他資料進行聯動展示,還是可擴充套件性不好。比如使用者反饋的問題的日誌檔案應該和Crash收集檔案放到同一個地方才合理,雖然Crash 收集用的是第三方框架,無法自己去改程式碼做擴充套件,萬一以後不跟那個三方框架合作了呢,或者使用者日誌的資料需要和其他資料進行聯動呢。
-
CocoaLumberjack 仍無法百分百保證Log的 完整性 ,在App Crash時無法保證Log 已經寫到檔案。因為CocoaLumberjack是通過 -[NSFileHandle writeData:] 來進行寫檔案的,此方式除了無法保證Log完整性外,相對mmap中使用記憶體對映檔案來執行寫操作的方案也較慢
2.2 方案優化
既然找不到服務端和前端同學來幫忙,那就硬著頭皮自己上吧。分解任務,逐個擊破!將日誌系統分成相對獨立的三部分 採集部分、傳輸部分、管理部分
- 採集部分:使用微信開源的日誌採集元件XLog,該元件具有安全性、流暢性、完整性、容錯性 的優點。
- 傳輸部分:使用Spring Boot 開發Web伺服器,Web伺服器提供簡單的檔案上傳和下載、檔案上傳白名單獲取和設定的功能。
- 管理部分:使用Ant Design Pro 框架來開發後臺管理系統,在管理系統中提供頁面進行設定日誌上傳白名單,及日誌下載。
2.2.1 採集部分:高效能日誌採集元件 XLog
流暢性是首要考慮
在採集部分,之前一直沒有提到 流暢性 的重要程度,其實這個才是日誌採集元件的最高優先順序需求,不能因為日誌頻繁寫檔案導致應用程式卡頓或耗電量增加。因為專案中之前沒有高頻高密度地使用日誌,沒能及早意識到流暢性 的重要程度。頻繁寫檔案為什麼會卡頓?
當寫檔案的時候,並不是把資料直接寫入了磁碟,而是先把資料寫入到系統的快取(dirty page)中,系統一般會在下面幾種情況把 dirty page 寫入到磁碟:
- 通過頁的 flag 標記為有改動,作業系統定時將這種 dirty page 寫回到磁碟上,時機不可控。
- 呼叫使用者態的寫介面->觸發核心態的sys_write->檔案系統將資料寫回磁碟。而檔案系統回寫磁碟的時機也是不可控的,發現 dirty page 佔用記憶體超過系統記憶體一定比例後回寫。
而且資料從程式寫入到磁碟的過程中,牽涉到兩次資料拷貝:一次是使用者空間記憶體拷貝到核心空間的快取,一次是回寫時核心空間的快取到硬碟的拷貝。其中核心空間和使用者空間頻繁切換的話也帶來效能損耗。
保證流暢性無法兼顧完整性
避免頻繁寫檔案,先在記憶體中建立buffer,合適時在進行寫檔案。這個方式雖然保證了流暢性,缺無法保證完整性,而且集中壓縮日誌會導致 CPU 短時間飆高。程式發生Crash的話記憶體中的資料還沒有持久化,實時寫檔案的話又無法保證流暢性,該如何是好?
mmap 保證流暢性和完整性
mmap 是使用邏輯記憶體對磁碟檔案進行對映,中間只是進行對映沒有任何拷貝操作,避免了寫檔案的資料拷貝。操作記憶體就相當於在操作檔案,避免了核心空間和使用者空間的頻繁切換。
為了驗證 mmap 是否真的有直接寫記憶體的效率,微信團隊寫了一個簡單的測試用例:把512 Byte的資料分別寫入150 kb大小的記憶體和 mmap,以及磁碟檔案100w次並統計耗時
mmap 除了能保證 流暢性 ,還能兼顧日誌的 完整性,下面這些情況回自動回寫磁碟
- 記憶體不足
- 程式退出
- 呼叫這兩個函式
msync(mmap_ptr, mmap_size, MS_ASYNC)
同步,非同步寫回磁碟munmap(mmap_ptr, mmap_size)
解除一個map,內容會寫回磁碟
- 不設定 MAP_NOSYNC 情況下 30s-60s(僅限FreeBSD)
xlog 還保證了 安全性 和 容錯性
通過壓縮和加密可以保證日誌資訊非明文寫入磁碟,同時減少所佔用的 mmap 的大小。策略是
在寫進邏輯記憶體之前就把日誌先進行壓縮,再進行加密,最後再寫入到邏輯記憶體中
微信選擇的具體壓縮方案可以參考文章 cloud.tencent.com/developer/a… ,簡單來說就是 能夠保證日誌容錯性的流式壓縮,
即使壓縮單位中有部分資料損壞,因為是流式壓縮,並不影響這個單位中損壞資料之前的日誌的解壓,只會影響這個單位中這個損壞資料之後的日誌。
所以一句話總結xlog 為什麼具有安全性、流暢性、完整性、容錯性 的優點
使用流式壓縮方式對單行日誌進行壓縮,壓縮加密後寫進作為 log 中間 buffer的 mmap 中,當 mmap 中的資料到達一定大小後再寫進磁碟檔案中
另外mmap 相關的API 如下,參考開源的XLog,你也可以給團隊定製基於mmap的日誌採集元件
FILE *fp = fopen(file_path, "wb+");
file_num = fileno(fp);
ftruncate(file_num, size); // 調整size
char *mmap_ptr = (char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, file_num, 0);
// 然後就可以對 mmap_ptr 進行讀寫了
munmap(mmap_ptr, mmap_size); // 解除一個map,內容會寫回磁碟
msync(mmap_ptr, mmap_size, MS_ASYNC); // 同步,非同步寫回磁碟
擴容辦法:先解除munmap,調大檔案大小,重新調 mmap 對映即可
複製程式碼
手淘的SatanWoo 五子棋大佬在解析xlog原始碼的時候給出了兩點使用mmap時需要額外注意的點,文章在此
- 注意點1: 如果我們嘗試開啟mmap成功了,但是mmap對應的資料地址是NULL,那我們必須停止對映。因為NULL所代表的地址處於核心態,一旦對映了,勢必造成Crash。
- 注意點2:使用mmap的情況下,如果上次應用斷電了、Crash,日誌的資訊還是存在的,但是並不一定能及時的轉換成我們想要的日誌檔案。因此我們首先檢查下mmap檔案裡面有沒有資料,有的話先把這部分轉換成日誌。
2.2.2 傳輸部分:使用Spring Boot 快速開發Web伺服器
在網上有很多介紹Spring Boot的文章,螞蟻金服的一位前輩寫了篇 Spring Boot快速開始指南 我看還不錯,介紹了Spring-Boot的知識點線路圖和基本概念,還有如何快速建立一個Spring Boot 應用。
我在自己的學生機開發完,用postman 除錯完介面再去找公司運維要機器資源,部署到了公司機器上。由於我也是照葫蘆畫瓢,只是使用Spring Boot提供了簡單的檔案上傳和下載功能,暫時無法在這一塊深入介紹自己的經驗,不過日誌系統對應的Spring Boot部分我已經放到了github,感興趣可以clone 下來跑跑看 github.com/HonchWong/H…
專案名字是RDA,也就是研發助手的英文,意味著這個Spring-Boot應用不會止步於此。因為平時iOS業務開發中會經常遇到些阻礙效率的問題,會想出很多“牛逼”的技術方案去解決,但僅僅熟悉Cocoa 框架是實現不了的,少不了服務端的支援,比如最近業餘在做的三個需求,【客戶端視覺化Mock資料:提供視覺化的介面去設定Mock網路資料,無需硬編碼Mock和減少編譯時間】、【客戶端視覺化一鍵生成bug單:提供視覺化的介面去輸入bug描述,並生成bug單,提高研發效率】、【客戶端埋點管理平臺:提供平臺去管理埋點需求、驗證埋點、埋點資訊自定義展示】,這也是立個flag,技術服務業務,業務必將反哺於技術,19年好好學習服務端知識,再來完善這篇文章 :)
2.2.3 管理部分:直接fork "Ant Design Pro"進行改裝
Ant Design Pro 是螞蟻金服團隊在 Ant Design 的設計規範與元件庫基礎上推出的一套 React 實現的企業級中後臺前端/設計解決方案。基於這個框架可以快速開發後臺管理系統,如果你有React Native的開發經驗,那麼對基於React開發的Ant Design Pro 肯定也是上手很快。本日誌系統使用到的 Ant Design Pro 放到了github github.com/HonchWong/H…
三、本日誌系統如何使用
除了服務端和前端的Demo,iOS端的Demo我已經提交到github。需要從github下載兩個工程 web伺服器工程 github.com/HonchWong/H… 、 iOS端工程github.com/HonchWong/H…
- 本地執行web伺服器
- 環境搭建,安裝JDK、Maven
- HCRDA-SpringBoot/src/main/resources/application.properties 修改配置檔案中的使用者日誌資料夾位置,比如,這個是我本地存放的位置
userlog.dir.path=/Users/huanghongchang/Desktop/userLog
- 在HCRDA-SpringBoot 目錄下執行 mvn spring-boot:run
- 訪問 http://localhost:9080
- 執行iOS工程
- pod install 安裝依賴
- 修改 HCLogFileUploadManager.m 中的 static NSString *_hostURL 為 localhost的ip
3.1 XLog使用
-
初始化xlog。 涉及四個API,具體呼叫可以參考Demo工程中的 XLogHelper.m +setupXlog
setxattr([[self xlogFileDirPath] UTF8String], attrName, &attrValue, sizeof(attrValue), 0, 0);
該API是系統庫提供,為了防止該路徑下的日誌檔案被 iCloud 同步。xlogger_SetLevel(kLevelDebug);
設定log級別appender_set_console_log(true);
debug模式下控制檯是否列印logappender_open(kAppednerAsync, [logPath UTF8String], "Test");
開啟log目錄下的日誌檔案,進行一些初始化操作。
-
打log,
xlogger_Write(&info, message.UTF8String)
,Demo用巨集定義封裝了該API -
需要在APP終止方法applicationWillTerminate中反初始化
appender_close()
以上只是Demo中的使用方式,更多詳細API 可以參考xlog的wiki Mars iOS/OS X 介面詳細說明,Demo中對log日誌進行加密,appender_open 提供了引數傳入公鑰,xlog的加密使用可以參考文件 Xlog 加密使用指引
3.2 日誌上傳和下載
本日誌系統日誌上傳的策略採用白名單的方式,在後臺管理系統設定白名單,白名單包含使用者唯一識別符號uin及對應的上傳的日誌型別(如資料庫or日誌log),使用者輸入白名單中的uin,獲取需要上傳的日誌型別進行日誌上傳。
在HCRDA-SpringBoot 目錄下執行 mvn spring-boot:run 後,在瀏覽器中訪問 http://localhost:9080, 輸入賬戶admin 密碼 123,在使用者日誌-指定使用者中可以看到有白名單中已經有八個,目前Demo 中只實現了【選擇日誌上傳單個檔案】和【上傳全部日誌】
設定白名單後便可在Demo 中走上傳流程,操作步驟如下
在後臺管理系統中可以檢視和下載上傳的日誌
3.3 檢視日誌
我這裡並沒有對xlog檔案進行加密,而是對zip檔案進行了加密,而密碼用了xor的方式對字串進行混淆避免用hopper等反彙編工具直接檢視,當然這個還不是絕對安全,還是可以通過逆向App,動態除錯,獲取zip檔案的密碼,只不過也加大了破解的難度。雖然沒有加密,但單行log還是進行了流壓縮,需要用這個指令碼進行解壓decode_mars_nocrypt_log_file
執行指令碼 python decode_mars_nocrypt_log_file.py xxx.xlog