本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57b6a...android
Dev Club 是一個交流移動開發技術,結交朋友,擴展人脈的社羣,成員都是通過審覈的移動開發工程師。每週都會舉行嘉賓分享,話題討論等活動。算法
本期,咱們邀請了騰訊 WXG iOS 開發工程師——張三華,爲你們分享《微信 iOS SQLite 源碼優化實踐》。sql
SQLite是微信iOS選用的數據庫,隨着微信iOS客戶端業務的增加,在重度用戶的場景下,性能瓶頸逐漸顯現。靠單純地修改SQLite的參數配置,已經不能完全解決問題,所以咱們嘗試從源碼開始作深刻的優化。數據庫
內容大致框架:性能優化
SQLite對於多線程的處理和不足及微信的優化微信
SQLite在I/O上可壓榨的性能多線程
其餘細節優化架構
下面是本期分享內容整理併發
Hello,你們好,我是張三華,目前在微信主要負責 iOS 的基礎優化工做。第一次進行這種微信羣分享,可能準備的不是太充分。如有任何疑問,歡迎在分享結束後提問。app
下面開始咱們今天的分享。
SQLite 是咱們在移動端經常使用的數據庫,微信也是基於它封裝了一層 ObjC 接口。咱們知道,微信裏消息的收發是很頻繁的,尤爲是對於重度用戶,這對於數據庫的多線程併發和 I/O 是很大的挑戰。
一般對這部分作優化,有兩種方式:
一是修改 SQLite 的參數,如 Cache Size 等
二是改業務層調用,如主線程操做 dispatch 到子線程。
然而,前者有明顯的瓶頸,後者則是個 endless 的工做。咱們但願能一勞永逸地解決同類問題。這就是咱們本次所要分享的優化。
咱們先講 SQLite 所提供的多線程併發方案。它對這方面的支持作的很不錯,在使用上,只需
開啓句柄多線程支持的配置 PRAGMA SQLITE_THREADSAFE=2
確保同一個句柄同一時間只有一個線程在操做
(可選)開啓 WAL 模式 PRAGMA journal_mode=WAL
此時寫操做會先 append 到 wal 文件末尾,而不是直接覆蓋舊數據。而讀操做開始時,會記下當前的 WAL 文件狀態,而且只訪問在此以前的數據。這就確保了多線程讀與讀、讀與寫之間能夠併發地進行。
而寫與寫之間仍會互相阻塞。SQLite 提供了 Busy Retry 的方案,即發生阻塞時,會觸發 Busy Handler,此時可讓線程休眠一段時間後,從新嘗試操做。重試必定次數依然失敗後,則返回 SQLITE_BUSY
錯誤碼。
下面這段代碼是 SQLite 默認的 Busy Handler
上面介紹了 SQLite 多線程併發方案,接下來咱們把焦點放在 Busy Retry 這個方案的不足上。
Busy Retry 的方案雖然基本能解決問題,但對性能的壓榨作的不夠極致。在 Retry 過程當中,休眠時間的長短和重試次數,是決定性能和操做成功率的關鍵。
然而,它們的最優值,因不一樣操做不一樣場景而不一樣。若休眠時間過短或重試次數太多,會空耗 CPU 的資源;若休眠時間過長,會形成等待的時間太長;若重試次數太少,則會下降操做的成功率。以下圖
能夠看到
CPU空轉那段,線程一操做還沒結束,這裏空耗了 CPU 的資源
線程閒置那段,線程一已經結束,而線程二仍在等待,空耗了時間
對於這個的優化,簡單的方法能夠是修改休眠時間,盡最大限度縮短以上兩段空耗的資源。
咱們經過 A/B Test 對不一樣休眠時間進行了實驗,獲得了以下的結果
能夠看到,假若休眠時間與重試成功率的關係,按照綠色的曲線進行分佈,那麼 p 點的值也不失爲該方案的一個次優解。然而不一樣業務和操做的需求,仍是有很大的不一樣的。
既然 SQLite 的方案不行,咱們就要開始往深層探索新的可能性了。
SQLite是一個適配不一樣平臺的數據庫,不只支持多線程併發,還支持多進程併發。它的核心邏輯能夠分爲兩部分:
Core 層。包括了接口層、編譯器和虛擬機。經過接口傳入 SQL 語句,由編譯器編譯SQL生成虛擬機的操做碼 opcode。而虛擬機是基於生成的操做碼,控制 Backend 的行爲。
Backend 層。由 B-Tree、Pager、OS 三部分組成,實現了數據庫的存取數據的主要邏輯。
在架構最底端的 OS 層是對不一樣操做系統的系統調用的抽象層。它實現了一個 VFS(Virtual File System),將 OS 層的接口在編譯時映射到對應操做系統的系統調用。鎖的實現也是在這裏進行的。
SQLite 經過兩個鎖來控制併發。第一個鎖對應 DB 文件,經過5種狀態進行管理;第二個鎖對應WAL文件,經過修改一個 16-bit 的 unsigned short int 的每個 bit 進行管理。儘管鎖的邏輯有一些複雜,但此處並不需關心。這兩種鎖最終都落在 OS 層的 sqlite3OsLock、sqlite3OsUnlock 和 sqlite3OsShmLock 上具體實現。
它們在鎖的實現比較相似。以 lock 操做在 iOS 上的實現爲例:
經過 pthread_mutex_lock 進行線程鎖,防止其餘線程介入。而後比較狀態量,若當前狀態不可跳轉,則返回 SQLITE_BUSY
經過 fcntl 進行文件鎖,防止其餘進程介入。若鎖失敗,則返回 SQLITE_BUSY
而 SQLite 選擇 Busy Retry 的方案的緣由也正是在此
文件鎖沒有線程鎖相似 pthread_cond_signal 的通知機制。當一個進程的數據庫操做結束時,沒法經過鎖來第一時間通知到其餘進程進行重試。所以只能退而求其次,經過屢次休眠來進行嘗試。
搞清楚了 SQLite 併發的實現,咱們就是能夠開始改造了。
咱們知道,iOS app 是單進程的,並沒有多進程併發的需求,這和 SQLite 的設計初衷是不相同的。這就給咱們的優化提供了理論上的基礎。在 iOS 這一特定場景下,咱們能夠捨棄兼容性,提升併發性。
新的方案修改成,當 OS 層進行 lock 操做時:
經過 pthread_mutex_lock 進行線程鎖,防止其餘線程介入。而後比較狀態量,若當前狀態不可跳轉,則將當前指望跳轉的狀態,插入到一個 FIFO 的 Queue 尾部。最後,線程經過 pthread_cond_wait 進入 休眠狀態,等待其餘線程的喚醒。
忽略文件鎖
當 OS 層的 unlock 操做結束後:
取出 Queue 頭部的狀態量,並比較狀態是否可以跳轉。若可以跳轉,則經過 pthread_cond_signal_thread_np 喚醒對應的線程重試。
新的方案能夠在 DB 空閒時的第一時間,通知到其餘正在等待的線程,最大程度地下降了空等待的時間,且準確無誤。
此外,因爲 Queue 的存在,當主線程被其餘線程阻塞時,能夠將主線程的操做「插隊」到 Queue 的頭部。當其餘線程發起喚醒通知時,主線程能夠有更高的優先級,從而下降用戶可感知的卡頓
上面介紹了多線程併發的優化,接下來將介紹 I/O 方面的優化。
提到 I/O 效率的提高,最容易想到的就是 mmap了,它能夠減小數據從 kernel 層到 user 層的數據拷貝,從而提升效率。
SQLite 不只支持 mmap,並且推薦使用,在大多數平臺是在必定程度上默認打開的。然而早期的 iOS 版本的存在一些 bug,SQLite 在編譯層就關閉了在 iOS 上對 mmap 的支持,而且後知後覺地在16年1月才從新打開。因此若是使用的 SQLite 版本較低,還需註釋掉相關代碼後,從新編譯生成後,才能夠享受上 mmap 的性能。
下圖就是 SQLite 註釋掉相關代碼的 commit
開啓 mmap 後,SQLite 性能將有所提高,但這還不夠。由於它只會對 DB 文件進行了 mmap,而 WAL 文件享受不到這個優化。緣由以下:
開啓 WAL 模式後,寫入的數據會先 append 到 WAL 文件的末尾。待文件增加到必定長度後,SQLite 會進行 checkpoint。這個長度默認爲1000個頁大小,在 iOS 上約爲3.9MB。
而在多句柄下,對 WAL 文件的操做是並行的。一旦某個句柄將 WAL 文件縮短了,而沒有一個通知機制讓其餘句柄進行更新 mmap 的內容。此時其餘句柄若使用 mmap 操做已被縮短的內容,就會形成 crash。而普通的 I/O 接口,則只會返回錯誤,不會形成 crash。所以,SQLite 沒有實現對 WAL 文件的 mmap。
顯然 SQLite 的設計是針對容量較小的設備,尤爲是在十幾年前的那個年代,這樣的設備並不在少數。而隨着硬盤價格日益下降,對於像 iPhone 這樣的設備,幾 MB 的空間已經再也不是須要斤斤計較的了。
另外一方面,文件從新增加,對於文件系統來講,這就意味着須要消耗時間從新尋找合適的文件塊。
權衡二者,咱們能夠改成
數據庫關閉並 checkpoint 成功時,再也不 truncate 或刪除 WAL 文件,只修改 WAL 的文件頭的 Magic Number。下次數據庫打開時, SQLite 會識別到 WAL 文件不可用,從新從頭開始寫入。
爲 WAL 添加 mmap 的支持 有了上面兩個優化,總體性能就會提高很多了。
這裏我沒有貼具體代碼須要改哪些地方,一方面是由於改動點較零散,另外一方面是代碼上的改動並不難。這個優化的工做量主要是在 SQLite 原理和優化點的挖掘上了,你們能夠根據優化方案去嘗試。
不過咱們還有一些簡單易行且效果還不錯的小優化,但願能夠成爲你們打開 SQLite 黑盒的一個契機。
如咱們在多線程優化時所說,對於 iOS app 並無多進程的需求。所以咱們能夠直接註釋掉 os_unix.c 中全部文件鎖相關的操做。也許你會很奇怪,雖然沒有文件鎖的需求,但這個操做耗時也很短,是否有必要特地優化呢?其實並不全然。耗時多少是比出來。
SQLite 中有 cache 機制。被加載進內存的 page,使用完畢後不會馬上釋放。而是在必定範圍內經過 LRU 的算法更新 page cache。這就意味着,若是 cache 設置得當,大部分讀操做不會讀取新的 page。然而由於文件鎖的存在,原本只需在內存層面進行的讀操做,不得不進行至少一次 I/O 操做。而咱們知道,I/O 操做是遠遠慢於內存操做的。
SQLite 會對申請的內存進行統計,而這些統計的數據都是放到同一個全局變量裏進行計算的。這就意味着統計先後,都是須要加線程鎖,防止出現多線程問題的。
如下 SQLite 內存申請的函數能夠看到,當內存統計打開時,會跑代碼的第二個 if,malloc 的先後被鎖保護了起來。
其實這裏內存申請的量不大,並非很是耗時的操做,但卻很頻繁。多線程併發時,各線程很容易互相阻塞。由於耗時很短,因此被阻塞的時間也很短暫。彷佛不會有太大問題。但頻繁地阻塞卻意味着線程不斷地切換,這是個很影響性能的操做,尤爲對於單核設備。
所以,若是不須要內存統計的特性,能夠經過 sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)進行關閉。這個修改雖然不須要改動源碼,但若是不查看源碼,恐怕是比較難發現的。
總的來講,移動客戶端數據庫雖然不如後臺數據庫那麼複雜,但也存在着很多可挖掘的技術點。
此次也只嘗試了對 SQLite 原有的方案進行優化,而市面上還有許多優秀的數據庫,如 LevelDB、RocksDB、Realm 等,它們採用了和 SQLite 不一樣的實現原理。後續咱們將借鑑它們的優化經驗,嘗試更深刻的優化。
以上就是我今天的分享,謝謝你們。
Q1 :前一陣微信提示我微信數據文件發現有損壞,這個是什麼緣由呢?
這個是數據庫損壞,SQLite 是以B樹結構存儲的,若是某一個節點發生損壞,可能致使沒法讀取數據。損壞的緣由多種多樣,如斷電、文件系統錯誤、硬盤損壞等。據我所知不少產品都出現了相似問題。
你看到的那個是微信的損壞監測和修復邏輯,咱們作了自研的工具進行修復。這塊咱們後續也會分享 db 損壞的監測、保護、修復方案的
Q2 :請問 sqlite 有時候會出 signal 11的錯誤,多是什麼緣由致使的
signal 11 就是 SQLITE_CORRUPT,上面提到的數據庫損壞的其中一種。另外一種是26 SQLITE_NOTADB
Q3 :請問微信在全文索引上有實踐嗎?有沒有本身作本地的搜索索引
SQLite 是支持有全文索引的支持的,咱們要作的是提供一個好的,支持中文的分詞器。
Q4 :請問微信在 db 文件修復上有什麼心得呢?
看來你們對 db 文件損壞很關注啊。SQLite 提供了 PRAGMA integrity_check 的工具檢測損壞 和 DUMP 工具導出損壞 db。但從實踐來看,效果並不理想。咱們採用了按 BTree 結構遍歷修復的方式,之後有機會能夠分享給你們
Q5 :目前有沒有已有的優化過的 sqlite 框架可供使用呢?
iOS上SQLite 的框架彷佛只有 FMDB 和 CoreData,坦白說兩個都不是很好。咱們是本身封裝的 WCDB 框架。
Q6 :微信的 orm 是怎麼搞的
經過封裝和規範來處理 ORM
Q7 :請問下多句柄怎麼開啓,是修改 sqlite 源碼後再編譯的嗎?
這個最開始有提到了
開啓句柄多線程支持的配置 PRAGMA SQLITE_THREADSAFE=2
確保同一個句柄同一時間只有一個線程在操做
Q8 :微信是怎麼分析它的鎖競爭的?
最重要的是讀懂源碼。輔助手段能夠有 SQLite 官方的 Technical/Design Document 和 Instrument 工具
Q9 :請問有沒有對能耗的監測和優化經驗?
檢測相關的咱們有卡頓監控系統,能夠到咱們的公衆號 WeMobileDev 上了解
Q10 :請問 sqlite 優化後有性能對比數據嗎,差異有多大?
性能數據我以咱們的卡頓系統爲準,多線程併發優化使得卡頓率從4.08%降至0.19,I/O 優化使得讀卡頓從1.50%降至0.20%,寫卡頓從1.18%降至0.21%
Q11 :iOS 客戶端用操做數據庫須要每次先 open,執行完了再 close,每次都這樣,仍是 app 只須要開關一次比較好呢?
經常使用的 db 沒有必要常常開關,db 佔用的內存並不高,能夠權衡一下
Q12 :微信對於本地空間不足會有一個強提醒,這是出於什麼考慮?不一樣機型有不一樣的策略嗎?
空間不足是個硬傷,所謂巧婦難爲無米之炊。如16GB 的 iPhone,其實很影響正常使用了。不一樣機型會作細化
Q13 :請問 sqlite 多線程機制,大概能應付多大量級的數據庫操做(基本無卡頓),微信有這方面的測試體驗嗎,而後是使用了底層代碼修改多線程機制後,有大概的提高量級嗎?
優化的效果咱們是以卡頓系統檢測到的爲準的。可否減小用戶感知到的卡頓,優化用戶體驗纔是重點,而不在於能承受多大的量級
Q14 :微信對於數據庫升級有沒有特別優化的地方?或者說不一樣版本的跳版本升級
不知道這個問題指的是 SQLite 的升級仍是表結構的升級。前者的話,暫時沒看到 SQLite 新版本有比較大的特性值得咱們跟進。後者能夠用 alter table 在封裝層支持升級,性能損耗不大
Q15 :請問微信的 SQLite 有沒有開啓加密?若是有,性能是否有提高空間?
iOS 版本目前沒有開啓加密
Q16 :微信 sqllite 數據庫用的內存數據庫嗎?那和文件數據庫導入導出怎麼控制的?
沒有使用內存數據庫
Q17 :能夠問一下,目前作 iOS 版,沒有針對 android 版麼?
此次分享的大部份內容,對Android也是通用的,舉一反三便可。
Q18 :請問下,句柄開幾個比較合適?讀寫分離開來對性能是否會有提高呢?
咱們是按需生成新句柄的,並設了上限,若超過上限會有報警。若是同一時間併發量太大的話,其實更多要考慮業務層是否適用得當。至於業務層的使用,若能作細化那天然是更好
更多精彩內容歡迎關注bugly的微信公衆帳號: