??微信 iOS SQLite 源碼優(yōu)化實(shí)踐

轉(zhuǎn)發(fā)自2016-08-18張三華騰訊Bugly

前言

隨著微信 iOS 客戶端業(yè)務(wù)的增長(zhǎng)败徊,在數(shù)據(jù)庫上遇到的性能瓶頸也逐漸凸顯。在微信的卡頓監(jiān)控系統(tǒng)上掏缎,數(shù)據(jù)庫相關(guān)的卡頓不斷上升皱蹦。而在用戶側(cè)也逐漸能感知到這種卡頓,尤其是有大量群聊眷蜈、聯(lián)系人和消息收發(fā)的重度用戶沪哺。

我們?cè)趯?duì) SQLite 進(jìn)行優(yōu)化的過程中發(fā)現(xiàn),靠單純地修改 SQLite 的參數(shù)配置酌儒,已經(jīng)不能徹底解決問題辜妓。因此從6.3.16版本開始,我們合入了 SQLite 的源碼,并開始進(jìn)行源碼層的優(yōu)化嫌拣。

本文將分享在 SQLite 源碼上進(jìn)行的多線程并發(fā)柔袁、I/O 性能優(yōu)化等,并介紹優(yōu)化相關(guān)的 SQLite 原理异逐。

多線程并發(fā)優(yōu)化

1. 背景

由于歷史原因捶索,舊版本的微信一直使用單句柄的方案,即所有線程共有一個(gè) SQLite Handle灰瞻,并用線程鎖避免多線程問題腥例。當(dāng)多線程并發(fā)時(shí),各線程的數(shù)據(jù)庫操作同步順序進(jìn)行酝润,這就導(dǎo)致后來的線程會(huì)被阻塞較長(zhǎng)的時(shí)間燎竖。

2. SQLite 的多句柄方案及 Busy Retry 方案

SQLite 實(shí)際是支持多線程(幾乎)無鎖地并發(fā)操作。只需

開啟配置PRAGMA SQLITE_THREADSAFE=2

確保同一個(gè)句柄同一時(shí)間只有一個(gè)線程在操作

Multi-thread. In this mode, SQLite can be safely used by multiple threads provided that no single database connection is used simultaneously in two or more threads.

倘若再開啟 SQLite 的 WAL 模式(Write-Ahead-Log)要销,多線程的并發(fā)性將得到進(jìn)一步的提升构回。

此時(shí)寫操作會(huì)先 append 到 wal 文件末尾,而不是直接覆蓋舊數(shù)據(jù)疏咐。而讀操作開始時(shí)纤掸,會(huì)記下當(dāng)前的 WAL 文件狀態(tài),并且只訪問在此之前的數(shù)據(jù)浑塞。這就確保了多線程讀與讀借跪、讀與寫之間可以并發(fā)地進(jìn)行。

然而酌壕,阻塞的情況并非不會(huì)發(fā)生掏愁。

當(dāng)多線程寫操作并發(fā)時(shí),后來者還是必須在源碼層等待之前的寫操作完成后才能繼續(xù)卵牍。

SQLite 提供了 Busy Retry 的方案果港,即發(fā)生阻塞時(shí),會(huì)觸發(fā) Busy Handler辽慕,此時(shí)可以讓線程休眠一段時(shí)間后京腥,重新嘗試操作。重試一定次數(shù)依然失敗后溅蛉,則返回SQLITE_BUSY錯(cuò)誤碼。

3. SQLite Busy Retry 方案的不足

Busy Retry 的方案雖然基本能解決問題他宛,但對(duì)性能的壓榨做的不夠極致船侧。在 Retry 過程中,休眠時(shí)間的長(zhǎng)短和重試次數(shù)厅各,是決定性能和操作成功率的關(guān)鍵镜撩。

然而,它們的最優(yōu)值,因不同操作不同場(chǎng)景而不同袁梗。若休眠時(shí)間太短或重試次數(shù)太多宜鸯,會(huì)空耗 CPU 的資源;若休眠時(shí)間過長(zhǎng)遮怜,會(huì)造成等待的時(shí)間太長(zhǎng)淋袖;若重試次數(shù)太少,則會(huì)降低操作的成功率锯梁。

我們通過 A/B Test 對(duì)不同的休眠時(shí)間進(jìn)行了測(cè)試即碗,得到了如下的結(jié)果:

可以看到,倘若休眠時(shí)間與重試成功率的關(guān)系陌凳,按照綠色的曲線進(jìn)行分布剥懒,那么 p 點(diǎn)的值也不失為該方案的一個(gè)次優(yōu)解。然而事總不遂人愿合敦,我們需要一個(gè)更好的方案键袱。

4. SQLite 中的線程鎖及進(jìn)程鎖

作為有著十幾年發(fā)展歷史、且被廣泛認(rèn)可的數(shù)據(jù)庫吱雏,SQLite 的任何方案選擇都是有其原因的瘤礁。在完全理解由來之前,切忌盲目自信裸准、直接上手修改展东。因此,首先要了解 SQLite 是如何控制并發(fā)的炒俱。

SQLite 是一個(gè)適配不同平臺(tái)的數(shù)據(jù)庫盐肃,不僅支持多線程并發(fā),還支持多進(jìn)程并發(fā)权悟。它的核心邏輯可以分為兩部分:

Core 層砸王。包括了接口層、編譯器和虛擬機(jī)峦阁。通過接口傳入 SQL 語句谦铃,由編譯器編譯 SQL 生成虛擬機(jī)的操作碼 opcode。而虛擬機(jī)是基于生成的操作碼榔昔,控制 Backend 的行為驹闰。

Backend層。由 B-Tree撒会、Pager嘹朗、OS 三部分組成,實(shí)現(xiàn)了數(shù)據(jù)庫的存取數(shù)據(jù)的主要邏輯诵肛。

在架構(gòu)最底端的 OS 層是對(duì)不同操作系統(tǒng)的系統(tǒng)調(diào)用的抽象層屹培。它實(shí)現(xiàn)了一個(gè) VFS(Virtual File System),將 OS 層的接口在編譯時(shí)映射到對(duì)應(yīng)操作系統(tǒng)的系統(tǒng)調(diào)用。鎖的實(shí)現(xiàn)也是在這里進(jìn)行的褪秀。

SQLite 通過兩個(gè)鎖來控制并發(fā)蓄诽。第一個(gè)鎖對(duì)應(yīng) DB 文件,通過5種狀態(tài)進(jìn)行管理媒吗;第二個(gè)鎖對(duì)應(yīng) WAL 文件仑氛,通過修改一個(gè)16-bit 的 unsigned short int 的每一個(gè) bit 進(jìn)行管理。盡管鎖的邏輯有一些復(fù)雜蝴猪,但此處并不需關(guān)心调衰。這兩種鎖最終都落在 OS 層的sqlite3OsLock、sqlite3OsUnlock和sqlite3OsShmLock上具體實(shí)現(xiàn)自阱。

它們?cè)阪i的實(shí)現(xiàn)比較類似嚎莉。以 lock 操作在 iOS 上的實(shí)現(xiàn)為例:

通過pthread_mutex_lock進(jìn)行線程鎖,防止其他線程介入沛豌。然后比較狀態(tài)量趋箩,若當(dāng)前狀態(tài)不可跳轉(zhuǎn),則返回SQLITE_BUSY

通過fcntl進(jìn)行文件鎖加派,防止其他進(jìn)程介入叫确。若鎖失敗,則返回SQLITE_BUSY

而 SQLite 選擇 Busy Retry 的方案的原因也正是在此---文件鎖沒有線程鎖類似 pthread_cond_signal 的通知機(jī)制芍锦。當(dāng)一個(gè)進(jìn)程的數(shù)據(jù)庫操作結(jié)束時(shí)竹勉,無法通過鎖來第一時(shí)間通知到其他進(jìn)程進(jìn)行重試。因此只能退而求其次娄琉,通過多次休眠來進(jìn)行嘗試次乓。

5. 新的方案

通過上面的各種分析、準(zhǔn)備孽水,終于可以動(dòng)手開始修改了票腰。

我們知道,iOS app 是單進(jìn)程的女气,并沒有多進(jìn)程并發(fā)的需求杏慰,這和 SQLite 的設(shè)計(jì)初衷是不相同的。這就給我們的優(yōu)化提供了理論上的基礎(chǔ)炼鞠。在 iOS 這一特定場(chǎng)景下缘滥,我們可以舍棄兼容性,提高并發(fā)性簇搅。

新的方案修改為完域,當(dāng) OS 層進(jìn)行 lock 操作時(shí):

通過pthread_mutex_lock進(jìn)行線程鎖,防止其他線程介入瘩将。然后比較狀態(tài)量,若當(dāng)前狀態(tài)不可跳轉(zhuǎn),則將當(dāng)前期望跳轉(zhuǎn)的狀態(tài)姿现,插入到一個(gè) FIFO 的 Queue 尾部肠仪。最后,線程通過pthread_cond_wait進(jìn)入 休眠狀態(tài)备典,等待其他線程的喚醒异旧。

忽略文件鎖

當(dāng) OS 層的 unlock 操作結(jié)束后:

取出 Queue 頭部的狀態(tài)量,并比較狀態(tài)是否能夠跳轉(zhuǎn)提佣。若能夠跳轉(zhuǎn)吮蛹,則通過pthread_cond_signal_thread_np喚醒對(duì)應(yīng)的線程重試。

pthread_cond_signal_thread_np是 Apple 在 pthread 庫中新增的接口拌屏,與pthread_cond_signal類似潮针,它能喚醒一個(gè)等待條件鎖的線程。不同的是倚喂,pthread_cond_signal_thread_np可以指定一個(gè)特定的線程進(jìn)行喚醒每篷。

新的方案可以在 DB 空閑時(shí)的第一時(shí)間,通知到其他正在等待的線程端圈,最大程度地降低了空等待的時(shí)間焦读,且準(zhǔn)確無誤。此外舱权,由于 Queue 的存在矗晃,當(dāng)主線程被其他線程阻塞時(shí),可以將主線程的操作“插隊(duì)”到 Queue 的頭部宴倍。當(dāng)其他線程發(fā)起喚醒通知時(shí)张症,主線程可以有更高的優(yōu)先級(jí),從而降低用戶可感知的卡頓啊楚。

該方案上線后吠冤,卡頓檢測(cè)系統(tǒng)檢測(cè)到

等待線程鎖的造成的卡頓下降超過90%

SQLITE_BUSY 的發(fā)生次數(shù)下降超過95%

I/O 性能優(yōu)化

保留 WAL 文件大小

如上文多線程優(yōu)化時(shí)提到,開啟 WAL 模式后恭理,寫入的數(shù)據(jù)會(huì)先 append 到 WAL 文件的末尾拯辙。待文件增長(zhǎng)到一定長(zhǎng)度后,SQLite 會(huì)進(jìn)行 checkpoint颜价。這個(gè)長(zhǎng)度默認(rèn)為1000個(gè)頁大小涯保,在 iOS 上約為3.9MB。

同樣的周伦,在數(shù)據(jù)庫關(guān)閉時(shí)夕春,SQLite 也會(huì)進(jìn)行 checkpoint。不同的是专挪,checkpoint 成功之后及志,會(huì)將 WAL 文件長(zhǎng)度刪除或 truncate 到0片排。下次打開數(shù)據(jù)庫,并寫入數(shù)據(jù)時(shí)速侈,WAL 文件需要重新增長(zhǎng)率寡。而對(duì)于文件系統(tǒng)來說,這就意味著需要消耗時(shí)間重新尋找合適的文件塊倚搬。

顯然 SQLite 的設(shè)計(jì)是針對(duì)容量較小的設(shè)備冶共,尤其是在十幾年前的那個(gè)年代,這樣的設(shè)備并不在少數(shù)每界。而隨著硬盤價(jià)格日益降低捅僵,對(duì)于像 iPhone 這樣的設(shè)備,幾 MB 的空間已經(jīng)不再是需要斤斤計(jì)較的了眨层。

因此我們可以修改為:

數(shù)據(jù)庫關(guān)閉并 checkpoint 成功時(shí)庙楚,不再 truncate 或刪除 WAL 文件只修改 WAL 的文件頭的 Magic Number。下次數(shù)據(jù)庫打開時(shí)谐岁,SQLite 會(huì)識(shí)別到 WAL 文件不可用醋奠,重新從頭開始寫入。

保留 WAL 文件大小后伊佃,每個(gè)數(shù)據(jù)庫都會(huì)有這約3.9MB的額外空間占用窜司。如果數(shù)據(jù)庫較多,這些空間還是不可忽略的航揉。因此塞祈,微信中目前只對(duì)讀寫頻繁且檢測(cè)到卡頓的數(shù)據(jù)庫開啟,如聊天記錄數(shù)據(jù)庫帅涂。

mmap 優(yōu)化

mmap 對(duì) I/O 性能的提升無需贅言议薪,尤其是對(duì)于讀操作。SQLite 也在 OS 層封裝了 mmap 的接口媳友,可以無縫地切換 mmap 和普通的I/O接口斯议。只需配置PRAGMA mmap_size=XXX即可開啟 mmap。

There are advantages and disadvantages to using memory-mapped I/O. Advantages include:

Many operations, especially I/O intensive operations, can be much faster since content does need to be copied between kernel space and user space. In some cases, performance can nearly double.

The SQLite library may need less RAM since it shares pages with the operating-system page cache and does not always need its own copy of working pages.

然而醇锚,你在 iOS 上這樣配置恐怕不會(huì)有任何效果哼御。因?yàn)樵缙诘?iOS 版本的存在一些 bug,SQLite 在編譯層就關(guān)閉了在 iOS 上對(duì) mmap 的支持焊唬,并且后知后覺地在16年1月才重新打開恋昼。所以如果使用的 SQLite 版本較低,還需注釋掉相關(guān)代碼后赶促,重新編譯生成后液肌,才可以享受上 mmap 的性能。

開啟 mmap 后鸥滨,SQLite 性能將有所提升嗦哆,但這還不夠谤祖。因?yàn)樗粫?huì)對(duì) DB 文件進(jìn)行了 mmap,而 WAL 文件享受不到這個(gè)優(yōu)化吝秕。

WAL 文件長(zhǎng)度是可能變短的泊脐,而在多句柄下空幻,對(duì) WAL 文件的操作是并行的烁峭。一旦某個(gè)句柄將 WAL 文件縮短了,而沒有一個(gè)通知機(jī)制讓其他句柄進(jìn)行更新 mmap 的內(nèi)容秕铛。此時(shí)其他句柄若使用 mmap 操作已被縮短的內(nèi)容约郁,就會(huì)造成 crash。而普通的 I/O 接口但两,則只會(huì)返回錯(cuò)誤鬓梅,不會(huì)造成 crash。因此谨湘,SQLite 沒有實(shí)現(xiàn)對(duì) WAL 文件的 mmap绽快。

還記得我們上一個(gè)優(yōu)化嗎?沒錯(cuò)紧阔,我們保留了 WAL 文件的大小坊罢。因此它在這個(gè)場(chǎng)景下是不會(huì)縮短的,那么不能 mmap 的條件就被打破了擅耽。實(shí)現(xiàn)上活孩,只需在 WAL 文件打開時(shí),用unixMapfile將其映射到內(nèi)存中乖仇,SQLite 的 OS 層即會(huì)自動(dòng)識(shí)別憾儒,將普通的 I/O 接口切換到 mmap 上。

其他優(yōu)化

禁用文件鎖

如我們?cè)诙嗑€程優(yōu)化時(shí)所說乃沙,對(duì)于 iOS app 并沒有多進(jìn)程的需求起趾。因此我們可以直接注釋掉os_unix.c中所有文件鎖相關(guān)的操作。也許你會(huì)很奇怪警儒,雖然沒有文件鎖的需求训裆,但這個(gè)操作耗時(shí)也很短,是否有必要特意優(yōu)化呢冷蚂?其實(shí)并不全然缭保。耗時(shí)多少是比出來。

SQLite 中有 cache 機(jī)制蝙茶。被加載進(jìn)內(nèi)存的 page艺骂,使用完畢后不會(huì)立刻釋放。而是在一定范圍內(nèi)通過 LRU 的算法更新 page cache隆夯。這就意味著钳恕,如果 cache 設(shè)置得當(dāng)别伏,大部分讀操作不會(huì)讀取新的 page。然而因?yàn)槲募i的存在忧额,本來只需在內(nèi)存層面進(jìn)行的讀操作厘肮,不得不進(jìn)行至少一次 I/O 操作。而我們知道睦番,I/O 操作是遠(yuǎn)遠(yuǎn)慢于內(nèi)存操作的类茂。

禁用內(nèi)存統(tǒng)計(jì)鎖

SQLite 會(huì)對(duì)申請(qǐng)的內(nèi)存進(jìn)行統(tǒng)計(jì),而這些統(tǒng)計(jì)的數(shù)據(jù)都是放到同一個(gè)全局變量里進(jìn)行計(jì)算的托嚣。這就意味著統(tǒng)計(jì)前后巩检,都是需要加線程鎖,防止出現(xiàn)多線程問題的示启。

內(nèi)存申請(qǐng)雖然不是非常耗時(shí)的操作兢哭,但卻很頻繁。多線程并發(fā)時(shí)夫嗓,各線程很容易互相阻塞迟螺。

阻塞雖然也很短暫,但頻繁地切換線程舍咖,卻是個(gè)很影響性能的操作矩父,尤其是單核設(shè)備。

因此谎仲,如果不需要內(nèi)存統(tǒng)計(jì)的特性浙垫,可以通過sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)進(jìn)行關(guān)閉。這個(gè)修改雖然不需要改動(dòng)源碼郑诺,但如果不查看源碼夹姥,恐怕是比較難發(fā)現(xiàn)的。

優(yōu)化上線后辙诞,卡頓監(jiān)控系統(tǒng)監(jiān)測(cè)到

DB 寫操作造成的卡頓下降超過80%

DB 讀操作造成的卡頓下降超過85%

結(jié)語

移動(dòng)客戶端數(shù)據(jù)庫雖然不如后臺(tái)數(shù)據(jù)庫那么復(fù)雜辙售,但也存在著不少可挖掘的技術(shù)點(diǎn)。本次嘗試了僅對(duì) SQLite 原有的方案進(jìn)行優(yōu)化飞涂,而市面上還有許多優(yōu)秀的數(shù)據(jù)庫旦部,如 LevelDB、RocksDB较店、Realm 等士八,它們采用了和 SQLite 不同的實(shí)現(xiàn)原理。后續(xù)我們將借鑒它們的優(yōu)化經(jīng)驗(yàn)梁呈,嘗試更深入的優(yōu)化婚度。

come from

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市官卡,隨后出現(xiàn)的幾起案子蝗茁,更是在濱河造成了極大的恐慌醋虏,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哮翘,死亡現(xiàn)場(chǎng)離奇詭異颈嚼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饭寺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門阻课,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人佩研,你說我怎么就攤上這事柑肴。” “怎么了旬薯?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)适秩。 經(jīng)常有香客問我绊序,道長(zhǎng),這世上最難降的妖魔是什么秽荞? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任骤公,我火速辦了婚禮,結(jié)果婚禮上扬跋,老公的妹妹穿的比我還像新娘阶捆。我一直安慰自己,他們只是感情好钦听,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布洒试。 她就那樣靜靜地躺著,像睡著了一般朴上。 火紅的嫁衣襯著肌膚如雪垒棋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天痪宰,我揣著相機(jī)與錄音叼架,去河邊找鬼。 笑死衣撬,一個(gè)胖子當(dāng)著我的面吹牛乖订,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播具练,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼乍构,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了靠粪?” 一聲冷哼從身側(cè)響起蜡吧,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤毫蚓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后昔善,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體元潘,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年君仆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翩概。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡返咱,死狀恐怖钥庇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咖摹,我是刑警寧澤评姨,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站萤晴,受9級(jí)特大地震影響吐句,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜店读,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一嗦枢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧屯断,春花似錦文虏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至剃氧,卻和暖如春敏储,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朋鞍。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工已添, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滥酥。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓更舞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坎吻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缆蝉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容