天天看點

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

作者:johnwhe

長久以來SQLite DB都有損壞問題,從Android、iOS等移動系統,到Windows、Linux 等桌面系統都會出現。由于微信所有消息都儲存在DB,服務端不保留備份,一旦損壞将導緻使用者消息被清空,顯然不能接受。

我們即将開源的移動資料庫元件 WCDB (WeChat Database),緻力于解決 DB 損壞導緻資料丢失的問題。

之前一篇文章《微信 SQLite 資料庫修複實踐》介紹了微信對SQLite資料庫修複以及降低損壞率的實踐, 這次再深入介紹一下微信資料庫修複的具體方案和發展曆程。

具體來說,微信需要一套滿足以下條件的DB恢複方案:

恢複成功率高。 由于牽涉到使用者核心資料,“姑且一試”的方案是不夠的,雖說 100% 成功率不太現實,但 90% 甚至 99% 以上的成功率才是我們想要的。

支援加密DB。 Android 端微信用戶端使用的是加密 SQLCipher DB,加密會改變資訊 的排布,往往對密文一個位元組的改動就能使解密後一大片資料變得面目全非。這對于資料恢複 不是什麼好消息,我們的方案必須應對這種情況。

能處理超大的資料量。 經過統計分析,個别重度使用者DB大小已經超過2GB,恢複方案 必須在如此大的資料量下面保證不掉鍊子。

不影響體驗。 統計發現隻有萬分之一不到的使用者會發生DB損壞,如果恢複方案 需要事先準備(比如備份),它必須對使用者不可見,不能為了極個别犧牲全體使用者的體驗。

經過多年的不斷改進,微信先後采用出三套不同的DB恢複方案,離上面的目标已經越來越近了。

Google 一下SQLite DB恢複,不難搜到使用<code>.dump</code>指令恢複DB的方法。<code>.dump</code>指令的作用是将 整個資料庫的内容輸出為很多 SQL 語句,隻要對空 DB 執行這些語句就能得到一個一樣的 DB。

<code>.dump</code>指令原理很簡單:每個SQLite DB都有一個<code>sqlite_master</code>表,裡面儲存着全部table 和index的資訊(table本身的資訊,不包括裡面的資料哦),周遊它就可以得到所有表的名稱和 <code>CREATE TABLE ...</code>的SQL語句,輸出<code>CREATE TABLE</code>語句,接着使用<code>SELECT * FROM ...</code> 通過表名周遊整個表,每讀出一行就輸出一個<code>INSERT</code>語句,周遊完後就把整個 DB dump 出來了。 這樣的操作,和普通查表是一樣的,遇到損壞一樣會傳回<code>SQLITE_CORRUPT</code>,我們忽略掉損壞錯誤, 繼續周遊下個表,最終可以把所有沒損壞的表以及損壞了的表的前半部分讀取出來。将dump 出來的SQL語句逐行執行,最終可以得到一個等效的新DB。由于直接跑在SQLite上層,是以天然 就支援加密SQLCipher,不需要額外處理。

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖:dump輸出樣例)

這個方案不需要任何準備,隻有壞DB的使用者要花好幾分鐘跑恢複,大部分使用者是不感覺的。 資料量大小,主要影響恢複需要的臨時空間:先要儲存dump 出來的SQL的空間,這個 大概一倍DB大小,還要另外一倍 DB大小來建立 DB恢複。至于我們最關心的成功率呢?上線後,成功率約為30%。這個成功率的定義是至少恢複了一條記錄,也就是說一大半使用者 一條都恢複不成功!

研究一下就發現,恢複失敗的使用者,原因都是<code>sqlite_master</code>表讀不出來,特别是第一頁損壞, 會導緻後續所有内容無法讀出,那就完全不能恢複了。恢複率這麼低的尴尬狀況維持了好久, 其他方案才漸漸露出水面。

損壞的資料無法修複,最直覺的解決方案就是備份,于是備份恢複方案被提上日程了。備份恢複這個 方案思路簡單,SQLite 也有不少備份機制可以使用,具體是:

拷貝: 不能再直白的方式。由于SQLite DB本身是檔案(主DB + journal 或 WAL), 直接把檔案複制就能達到備份的目的。

Dump: 上一個恢複方案用到的指令的本來目的。在DB完好的時候執行.dump, 把 DB所有内容輸出為 SQL語句,達到備份目的,恢複的時候執行SQL即可。

Backup API: SQLite自身提供的一套備份機制,按 Page 為機關複制到新 DB, 支援熱備份。這麼多的方案孰優孰劣?作為一個移動APP,我們關心的無非就是 備份大小、備份性能、 恢複性能 幾個名額。微信作為一個重度DB使用者,備份大小和備份性能是主要關注點, 原本使用者就可能有2GB 大的 DB,如果備份資料本身也有2GB 大小,使用者想必不會接受; 性能則主要影響體驗和備份成功率,作為使用者不感覺的功能,占用太多系統資源造成卡頓 是不行的,備份耗時越久,被系統殺死等意外事件發生的機率也越高。對以上方案做簡單測試後,備份方案也就基本定下了。測試用的DB大小約 50MB, 資料條目數大約為 10萬條:

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖:備選方案性能對比)可以看出,比較折中的選擇是 Dump + 壓縮,備份大小具有明顯優勢,備份性能尚可, 恢複性能較差但由于需要恢複的場景較少,算是可以接受的短闆。微信在Dump + gzip方案上再加以優化,由于格式化SQL語句輸出耗時較長,是以使用了自定義 的二進制格式承載Dump輸出。第二耗時的壓縮操作則放到别的線程同時進行,在雙核以上的環境 基本可以做到無額外時間消耗。由于資料保密需要,二進制Dump資料也做了加密處理。 采用自定義二進制格式還有一個好處是,恢複的時候不需要重複的編譯SQL語句,編譯一次就可以 插入整個表的資料了,恢複性能也有一定提升。優化後的方案比原始的Dump + 壓縮, 每秒備份行數提升了 150%,每秒恢複行數也提升了 40%。

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖: 性能優化效果)即使優化後的方案,對于特大DB備份也是耗時耗電,對于移動APP來說,可能未必有這樣的機會 做這樣重度的操作,或者頻繁備份會導緻卡頓,這也是需要開發者衡量的。比如Android微信會 選擇在 充電并滅屏 時進行DB備份,若備份過程中退出以上狀态,備份會中止,等待下次機會。備份方案上線後,恢複成功率達到72%,但有部分重度使用者DB損壞時,由于備份耗時太久, 始終沒有成功,而對DB資料丢失更為敏感的也恰恰是這些使用者,于是新方案應運而生。解析B-tree恢複方案(RepairKit)備份方案的高消耗迫使我們從另外的方案考慮,于是我們再次把注意力放在之前的Dump方案。 Dump 方案本質上是嘗試從壞DB裡讀出資訊,這個嘗試一般來說會出現兩種結果:

DB的基本格式仍然健在,但個别資料損壞,讀到損壞的地方SQLite傳回<code>SQLITE_CORRUPT</code>錯誤, 但已讀到的資料得以恢複。

基本格式丢失(檔案頭或<code>sqlite_master</code>損壞),擷取有哪些表的時候就傳回SQLITE_CORRUPT, 根本沒法恢複。第一種可以算是預期行為,畢竟沒有損壞的資料能 部分恢複。從之前的資料看, 不少使用者遇到的是第二種情況,這種有沒挽救的餘地呢?要回答這個問題,先得搞清楚<code>sqlite_master</code>是什麼。它是一個每個SQLite DB都有的特殊的表, 無論是檢視官方文檔Database File Format,還是執行SQL語句 <code>SELECT * FROM sqlite_master;</code>,都可得知這個系統表儲存以下資訊: 表名、類型(table/index)、 建立此表/索引的SQL語句,以及表的RootPage。<code>sqlite_master</code>的表名、表結構都是固定的, 由檔案格式定義,RootPage 固定為 page 1。

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖:sqlite_master表)正常情況下,SQLite 引擎打開DB後首次使用,需要先周遊<code>sqlite_master</code>,并将裡面儲存的SQL語句再解析一遍, 儲存在記憶體中供後續編譯SQL語句時使用。假如<code>sqlite_master</code>損壞了無法解析,“Dump恢複”這種走正常SQLite 流程的方法,自然會卡在第一步了。為了讓<code>sqlite_master</code>受損的DB也能打開,需要想辦法繞過SQLite引擎的邏輯。 由于SQLite引擎初始化邏輯比較複雜,為了避免副作用,沒有采用hack的方式複用其邏輯,而是決定仿造一個隻可以 讀取資料的最小化系統。雖然仿造最小化系統可以跳過很多正确性校驗,但<code>sqlite_master</code>裡儲存的資訊對恢複來說也是十分重要的, 特别是RootPage,因為它是表對應的B-tree結構的根節點所在地,沒有了它我們甚至不知道從哪裡開始解析對應的表。<code>sqlite_master</code>資訊量比較小,而且隻有改變了表結構的時候(例如執行了<code>CREATE TABLE、ALTER TABLE</code>等語句)才會改變,是以對它進行備份成本是非常低的,一般手機典型隻需要幾毫秒到數十毫秒即可完成,一緻性也容易保證, 隻需要執行了上述語句的時候重新備份一次即可。有了備份,我們的邏輯可以在讀取DB自帶的<code>sqlite_master</code>失敗的時候 使用備份的資訊來代替。DB初始化的問題除了檔案頭和<code>sqlite_master</code>完整性外,還有加密。SQLCipher加密資料庫,對應的恢複邏輯還需要加上 解密邏輯。按照SQLCipher的實作,加密DB 是按page 進行包括頭部的完整加密,所用的密鑰是根據使用者輸入的原始密碼和 建立DB 時随機生成的 salt 運算後得出的。可以猜想得到,如果儲存salt錯了,将沒有辦法得出之前加密用的密鑰, 導緻所有page都無法讀出了。由于salt 是建立DB時随機生成,後續不再修改,将它納入到備份的範圍内即可。到此,初始化必須的資料就保證了,可以仿造讀取邏輯了。我們正常使用的讀取DB的方法(包括dump方式恢複), 都是通過執行SQL語句實作的,這牽涉到SQLite系統最複雜的子系統——SQL執行引擎。我們的恢複任務隻需要周遊B-tree所有節點, 讀出資料即可完成,不需要複雜的查詢邏輯,是以最複雜的SQL引擎可以省略。同時,因為我們的系統是隻讀的, 寫入恢複資料到新 DB 隻要直接調用 SQLite 接口即可,因而可以省略同樣比較複雜的B-tree平衡、Journal和同步等邏輯。 最後恢複用的最小系統隻需要:

VFS讀取部分的接口(Open/Read/Close),或者直接用stdio的fopen/fread、Posix的open/read也可以

SQLCipher的解密邏輯

B-tree解析邏輯

即可實作。

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖:最小化系統)

Database File Format 較長的描述了SQLite檔案格式, 參照之實作B-tree解析可讀取 SQLite DB。加密 SQLCipher 情況較為複雜,幸好SQLCipher 加密部分可以單獨抽出,直接套用其解密邏輯。

實作了上面的邏輯,就能讀出DB的資料進行恢複了,但還有一個小插曲。我們知道,使用SQLite查詢一個表, 每一行的列數都是一緻的,這是Schema層面保證的。但是在Schema的下面一層——B-tree層,沒有這個保證。 B-tree的每一行(或者說每個entry、每個record)可以有不同的列數,一般來說,SQLite插入一行時, B-tree裡面的列數和實際表的列數是一緻的。但是當對一個表進行了<code>ALTER TABLE ADD COLUMN</code>操作, 整個表都增加了一列,但已經存在的B-tree行實際上沒有做改動,還是維持原來的列數。 當SQLite查詢到<code>ALTER TABLE</code>前的行,缺少的列會自動用預設值補全。恢複的時候,也需要做同樣的判斷和支援, 否則會出現缺列而無法插入到新的DB。

解析B-tree方案上線後,成功率約為78%。這個成功率計算方法為恢複成功的 Page 數除以總 Page 數。 由于是我們自己的系統,可以得知總 Page 數,使用恢複 Page 數比例的計算方法比人數更能反映真實情況。 B-tree解析好處是準備成本較低,不需要經常更新備份,對大部分表比較少的應用備份開銷也小到幾乎可以忽略, 成功恢複後能還原損壞時最新的資料,不受備份時限影響。 壞處是,和Dump一樣,如果損壞到表的中間部分,比如非葉子節點,将導緻後續資料無法讀出。

由于解析B-tree恢複原理和備份恢複不同,失敗場景也有差别,可以兩種手段混合使用覆寫更多損壞場景。 微信的資料庫中,有部分資料是臨時或者可從服務端拉取的,這部分資料可以選擇不修複,有些資料是不可恢複或者 恢複成本高的,就需要修複了。

如果修複過程一路都是成功的,那無疑使用B-tree解析修複效果要好于備份恢複。備份恢複由于存在 時效性,總有部分最新的記錄會丢掉,解析修複由于直接基于損壞DB來操作,不存在時效性問題。 假如損壞部分位于不需要修複的部分,解析修複有可能不發生任何錯誤而完成。

若修複過程遇到錯誤,則很可能是需要修複的B-tree損壞了,這會導緻需要修複的表發生部分或全部缺失。 這個時候再使用備份修複,能挽救一些缺失的部分。

最早的Dump修複,場景已經基本被B-tree解析修複覆寫了,若B-tree修複不成功,Dump恢複也很有可能不會成功。 即便如此,假如上面的所有嘗試都失敗,最後還是會嘗試Dump恢複。

微信移動端資料庫元件 WCDB 系列:資料庫修複三闆斧(二)

(圖: 恢複方案組合)

上面說的三種修複方法,原理上隻涉及到SQLite檔案格式以及基本的檔案系統,是跨平台的。 實際操作上,各個平台可以利用各自的特性做政策上的調整,比如 Android 系統使用 <code>JobScheduler</code> 在充電滅屏狀态下備份。

WCDB - WeChat Database,微信的移動資料庫元件,包含上面幾種修複方案, 以及加密、連接配接池并發、ORM、性能優化等特性,将在近日開源,歡迎關注。

本文來源于:WeMobileDev 微信公衆号

相關推薦

微信移動端資料庫元件WCDB系列:iOS基礎篇(一)

微信移動端資料庫元件WCDB系列:WINQ原理篇(三)

微信移動端資料庫元件WCDB系列: Android 特性篇(四)