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