Android高性能日志寫入方案的實(shí)現(xiàn)
前言
公司目前在做一款企業(yè)級智能客服系統(tǒng),對于系統(tǒng)穩(wěn)定性要求很高,不過難保用戶在使用中不會出現(xiàn)問題,而 Android SDK 集成在客戶的 APP 中,同時由于 Android 碎片化的問題,對于 SDK 的問題排查就顯得尤為困難,因此記錄下用戶的操作日志就顯得極為重要。
初始方案
一開始,SDK 記錄日志的方式是直接通過寫文件,當(dāng)有一條日志要寫入的時候,首先,打開文件,然后寫入日志,最后關(guān)閉文件。這樣做的問題就在于頻繁的IO操作,影響程序的性能,而且 SDK 為了保證消息的及時性,還維護(hù)了一個后臺進(jìn)程,當(dāng)其中一個進(jìn)程進(jìn)行日志寫入時,另一個就會被鎖在門外等著,問題就愈發(fā)嚴(yán)重。使用這種方案雖然當(dāng)前看上去對程序的影響不大,但是隨著日志量的增加,更多的IO操作,一定會造成性能瓶頸。
下面我們來分析下直接寫入文件的流程:
- 用戶發(fā)起 write 操作
- 操作系統(tǒng)查找頁緩存
a.若未命中,則產(chǎn)生缺頁異常,然后創(chuàng)建頁緩存,將用戶傳入的內(nèi)容寫入頁緩存
b.若命中,則直接將用戶傳入的內(nèi)容寫入頁緩存 - 用戶 write 調(diào)用完成
- 頁被修改后成為臟頁,操作系統(tǒng)有兩種機(jī)制將臟頁寫回磁盤
a.用戶手動調(diào)用 fsync()
b.由 pdflush 進(jìn)程定時將臟頁寫回磁盤
可以看出,數(shù)據(jù)從程序?qū)懭氲酱疟P的過程中,其實(shí)牽涉到兩次數(shù)據(jù)拷貝:一次是用戶空間內(nèi)存拷貝到內(nèi)核空間的緩存,一次是回寫時內(nèi)核空間的緩存到硬盤的拷貝。當(dāng)發(fā)生回寫時也涉及到了內(nèi)核空間和用戶空間頻繁切換。
而且相對于機(jī)械硬盤,SSD 存儲還有一個“寫入放大”的問題。這個問題主要和 SSD 存儲的物理結(jié)構(gòu)有關(guān)。當(dāng) SSD 被全部寫過一遍之后,再寫入的數(shù)據(jù)是不可以直接更新,只可以通過覆蓋重寫,在覆蓋之前需要先擦除數(shù)據(jù)。但寫入的最小單位是 Page,擦除的最小單位是 Block,而 Block 遠(yuǎn)大于 Page,所以在寫入新數(shù)據(jù)時就需要先把 Block 上的數(shù)據(jù)讀出來和要寫入的數(shù)據(jù)合并在一起,再把 Block 擦除,最后把讀出來的數(shù)據(jù)重新寫入到存儲上,這樣導(dǎo)致實(shí)際寫入的數(shù)據(jù)可能遠(yuǎn)遠(yuǎn)大于最開始需要寫入的數(shù)據(jù)。
沒想到簡單的寫文件竟然涉及了這么多操作,只是對于應(yīng)用層透明而已。
既然每寫一次文件會執(zhí)行這么多次操作,那么我們能不能將日志緩存起來,當(dāng)達(dá)到一定的數(shù)量后再一次性的寫入磁盤中呢?
這樣確實(shí)能夠大量減少 IO 次數(shù),但是卻會引發(fā)另一個更嚴(yán)重的問題——丟日志
把日志緩存在內(nèi)存中,當(dāng)程序發(fā)生 Crash 或進(jìn)程被殺后就無法保證日志的完整性,而且由于 SDK 存在多進(jìn)程,也無法保證多進(jìn)程下日志的順序。
一個完善的日志方案,需要滿足
- 高效,不能影響系統(tǒng)性能,不能因?yàn)橐肓巳罩灸K而造成應(yīng)用卡頓
- 保證日志的完整性,如果不能保證日志完整,那么日志收集就沒有意義了
- 對于多進(jìn)程應(yīng)用,要保證最終看到的日志順序的準(zhǔn)確性
高性能方案
既然無法減少寫入次數(shù),那么我們能不能在寫文件的過程中去優(yōu)化呢?
答案是可以的,使用 mmap
mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系,函數(shù)原型如下
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap操作提供了一種機(jī)制,讓用戶程序直接訪問設(shè)備內(nèi)存,這種機(jī)制,相比較在用戶空間和內(nèi)核空間互相拷貝數(shù)據(jù),效率更高。在要求高性能的應(yīng)用中比較常用。
時 mmap 能夠保證日志的完整性,mmap 的回寫時機(jī):
- 內(nèi)存不足
- 進(jìn)程退出
- 調(diào)用 msync 或者 munmap
- 不設(shè)置 MAP_NOSYNC 情況下 30s-60s(僅限FreeBSD)
當(dāng)映射一個文件后,程序就會在 native 內(nèi)存中申請一塊相同大小的空間,因此建議每次映射一小段內(nèi)容,如 64k,寫滿后再重新映射文件后面的內(nèi)容。
日志寫入性能和完整性的問題解決了,那么如何保證多進(jìn)程下日志的順序呢?
由于 mmap 是采用共享內(nèi)存的方式寫入數(shù)據(jù),如果兩個進(jìn)程同時映射一個文件,那么一定會造成日志覆蓋的問題。
既然不能直接保證順序,那我們只能退而求其次,兩個進(jìn)程分別映射不同的文件,每天合并一次,合并時對日志進(jìn)行排序。
繼續(xù)優(yōu)化
根據(jù)上述方案,設(shè)計(jì) jni 接口,打包 so,引入 SDK,看似沒什么問題了,但是作為一款 SDK,總覺得包含 so 不太友好,在一定程度上會增加接入的難度。
那么能不能不用 so 呢?
其實(shí) Java 中已經(jīng)提供了內(nèi)存映射的實(shí)現(xiàn)——MappedByteBuffer
MappedByteBuffer 位于 Java NIO 包下,用于將文件內(nèi)容映射到緩沖區(qū),使用的即是 mmap 技術(shù)。通過 FileChannel 的 map 方法可以創(chuàng)建緩沖區(qū)
RandomAccessFileraf = new RandomAccessFile(file, "rw"); MappedByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, position, size);
為了測試 MappedByteBuffer 的效率,我們把 64byte 的數(shù)據(jù)分別寫入內(nèi)存、MappedByteBuffer 和磁盤文件 50 萬次,并統(tǒng)計(jì)耗時
方法 | 耗時 |
---|---|
內(nèi)存 | 384ms |
MappedByteBuffer | 700ms |
磁盤文件 | 16805ms |
可以看出 MappedByteBuffer 雖然不及寫入內(nèi)存的性能,但是相比較寫入磁盤文件,已經(jīng)有了質(zhì)的提升。
總結(jié)
本文主要分析了直接寫文件記錄日志方式存在的問題,并引申出高性能文件寫入方案 mmap,兼顧了寫入性能和完整性,并通過補(bǔ)償方案確保多進(jìn)程下日志的順序。最后發(fā)現(xiàn)了內(nèi)存映射在 Java 層的實(shí)現(xiàn),避免了引入 so。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Android自定義收音機(jī)搜臺控件RadioRulerView
這篇文章主要為大家詳細(xì)介紹了Android自定義收音機(jī)搜臺控件RadioRulerView的相關(guān)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04關(guān)于Android CountDownTimer的使用及注意事項(xiàng)
這篇文章主要介紹了關(guān)于Android CountDownTimer的使用及注意事項(xiàng),具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11保存ListView上次的滾動條的位置實(shí)例(必看)
下面小編就為大家?guī)硪黄4鍸istView上次的滾動條的位置實(shí)例(必看)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03Kotlin中常見內(nèi)聯(lián)擴(kuò)展函數(shù)的使用方法教程
在Kotlin中,使用inline修飾符標(biāo)記內(nèi)聯(lián)函數(shù),既會影響到函數(shù)本身, 也影響到傳遞給它的Lambda表達(dá)式,這兩者都會被內(nèi)聯(lián)到調(diào)用處。下面這篇文章主要給大家介紹了關(guān)于Kotlin中常見內(nèi)聯(lián)擴(kuò)展函數(shù)的使用方法,需要的朋友可以參考下。2017-12-12Android系列---JSON數(shù)據(jù)解析的實(shí)例
JSON(JavaScript Object Notation)和XML,并稱為客戶端和服務(wù)端交互解決方案的倚天劍和屠龍刀,這篇文章主要介紹了Android系列---JSON數(shù)據(jù)解析的實(shí)例,有興趣的可以了解一下。2016-11-11Android使用gallery和imageSwitch制作可左右循環(huán)滑動的圖片瀏覽器
本文主要介紹了android使用gallery和imageSwitch制作可左右循環(huán)滑動的圖片瀏覽器的示例代碼。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04Android實(shí)現(xiàn)登陸界面的記住密碼功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)登陸界面的記住密碼功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Android自定義View的三種實(shí)現(xiàn)方式總結(jié)
本篇文章主要介紹了Android自定義View的三種實(shí)現(xiàn)方式總結(jié),非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2017-02-02關(guān)于Kotlin委托你必須重視的幾個點(diǎn)
委托模式已經(jīng)被證明是實(shí)現(xiàn)繼承的一個很好的替代方式,下面這篇文章主要給大家介紹了關(guān)于Kotlin委托你必須重視的幾個點(diǎn),文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-01-01