當前位置:
首頁 > 最新 > 微信移動端資料庫組件WCDB系列(二)—資料庫修復三板斧

微信移動端資料庫組件WCDB系列(二)—資料庫修復三板斧

前言

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

我們即將開源的移動資料庫組件 WCDB (WeChat Database),致力於解決 DB 損壞導致數據丟失的問題。

我們的需求

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

恢復成功率高。由於牽涉到用戶核心數據,「姑且一試」的方案是不夠的,雖說 100% 成功率不太現實,但 90% 甚至 99% 以上的成功率才是我們想要的。

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

能處理超大的數據量。經過統計分析,個別重度用戶DB大小已經超過2GB,恢復方案 必須在如此大的數據量下面保證不掉鏈子。

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

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

官方的Dump恢復方案

Google 一下SQLite DB恢復,不難搜到使用命令恢復DB的方法。命令的作用是將 整個資料庫的內容輸出為很多 SQL 語句,只要對空 DB 執行這些語句就能得到一個一樣的 DB。

命令原理很簡單:每個SQLite DB都有一個表,裡面保存著全部table 和index的信息(table本身的信息,不包括裡面的數據哦),遍歷它就可以得到所有表的名稱和的SQL語句,輸出語句,接著使用通過表名遍歷整個表,每讀出一行就輸出一個語句,遍歷完後就把整個DB dump出來了。 這樣的操作,和普通查表是一樣的,遇到損壞一樣會返回,我們忽略掉損壞錯誤, 繼續遍歷下個表,最終可以把所有沒損壞的表以及損壞了的表的前半部分讀取出來。將dump 出來的SQL語句逐行執行,最終可以得到一個等效的新DB。由於直接跑在SQLite上層,所以天然 就支持加密SQLCipher,不需要額外處理。

(圖:dump輸出樣例)

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

研究一下就發現,恢復失敗的用戶,原因都是表讀不出來,特別是第一頁損壞, 會導致後續所有內容無法讀出,那就完全不能恢復了。恢復率這麼低的尷尬狀況維持了好久, 其他方案才漸漸露出水面。

備份恢復方案

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

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

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

Backup API:SQLite自身提供的一套備份機制,按 Page 為單位複製到新 DB, 支持熱備份。

這麼多的方案孰優孰劣?作為一個移動APP,我們關心的無非就是備份大小、備份性能、 恢復性能幾個指標。微信作為一個重度DB使用者,備份大小和備份性能是主要關注點, 原本用戶就可能有2GB 大的 DB,如果備份數據本身也有2GB 大小,用戶想必不會接受; 性能則主要影響體驗和備份成功率,作為用戶不感知的功能,佔用太多系統資源造成卡頓 是不行的,備份耗時越久,被系統殺死等意外事件發生的概率也越高。

對以上方案做簡單測試後,備份方案也就基本定下了。測試用的DB大小約50MB, 數據條目數大約為10萬條

(圖:備選方案性能對比)

可以看出,比較折中的選擇是Dump + 壓縮,備份大小具有明顯優勢,備份性能尚可, 恢復性能較差但由於需要恢復的場景較少,算是可以接受的短板。

微信在Dump + gzip方案上再加以優化,由於格式化SQL語句輸出耗時較長,因此使用了自定義 的二進位格式承載Dump輸出。第二耗時的壓縮操作則放到別的線程同時進行,在雙核以上的環境 基本可以做到無額外時間消耗。由於數據保密需要,二進位Dump數據也做了加密處理。 採用自定義二進位格式還有一個好處是,恢復的時候不需要重複的編譯SQL語句,編譯一次就可以 插入整個表的數據了,恢復性能也有一定提升。優化後的方案比原始的Dump + 壓縮,每秒備份行數提升了 150%,每秒恢復行數也提升了 40%

(圖: 性能優化效果)

即使優化後的方案,對於特大DB備份也是耗時耗電,對於移動APP來說,可能未必有這樣的機會 做這樣重度的操作,或者頻繁備份會導致卡頓,這也是需要開發者衡量的。比如Android微信會 選擇在充電並滅屏時進行DB備份,若備份過程中退出以上狀態,備份會中止,等待下次機會。

備份方案上線後,恢復成功率達到72%,但有部分重度用戶DB損壞時,由於備份耗時太久, 始終沒有成功,而對DB數據丟失更為敏感的也恰恰是這些用戶,於是新方案應運而生。

解析B-tree恢復方案(RepairKit)

備份方案的高消耗迫使我們從另外的方案考慮,於是我們再次把注意力放在之前的Dump方案。 Dump 方案本質上是嘗試從壞DB里讀出信息,這個嘗試一般來說會出現兩種結果:

DB的基本格式仍然健在,但個別數據損壞,讀到損壞的地方SQLite返回

錯誤, 但已讀到的數據得以恢復。

基本格式丟失(文件頭或損壞),獲取有哪些表的時候就返回, 根本沒法恢復。

第一種可以算是預期行為,畢竟沒有損壞的數據能部分恢復。從之前的數據看, 不少用戶遇到的是第二種情況,這種有沒挽救的餘地呢?

要回答這個問題,先得搞清楚是什麼。它是一個每個SQLite DB都有的特殊的表, 無論是查看官方文檔Database File Format,還是執行SQL語句,都可得知這個系統表保存以下信息: 表名、類型(table/index)、 創建此表/索引的SQL語句,以及表的RootPage。的表名、表結構都是固定的, 由文件格式定義,RootPage 固定為 page 1。

(圖:sqlite_master表)

正常情況下,SQLite 引擎打開DB後首次使用,需要先遍歷,並將裡面保存的SQL語句再解析一遍, 保存在內存中供後續編譯SQL語句時使用。假如損壞了無法解析,「Dump恢復」這種走正常SQLite 流程的方法,自然會卡在第一步了。為了讓受損的DB也能打開,需要想辦法繞過SQLite引擎的邏輯。 由於SQLite引擎初始化邏輯比較複雜,為了避免副作用,沒有採用hack的方式復用其邏輯,而是決定仿造一個只可以 讀取數據的最小化系統。

雖然仿造最小化系統可以跳過很多正確性校驗,但里保存的信息對恢復來說也是十分重要的, 特別是RootPage,因為它是表對應的B-tree結構的根節點所在地,沒有了它我們甚至不知道從哪裡開始解析對應的表。

信息量比較小,而且只有改變了表結構的時候(例如執行了、等語句)才會改變,因此對它進行備份成本是非常低的,一般手機典型只需要幾毫秒到數十毫秒即可完成,一致性也容易保證, 只需要執行了上述語句的時候重新備份一次即可。有了備份,我們的邏輯可以在讀取DB自帶的失敗的時候 使用備份的信息來代替。

DB初始化的問題除了文件頭和完整性外,還有加密。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解析邏輯

即可實現。

(圖:最小化系統)

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

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

(圖: 恢復方案組合)

上面說的三種修復方法,原理上只涉及到SQLite文件格式以及基本的文件系統,是跨平台的。 實際操作上,各個平台可以利用各自的特性做策略上的調整,比如 Android 系統使用在充電滅屏狀態下備份。

我們的組件

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 WeMobileDev 的精彩文章:

TAG:WeMobileDev |

您可能感興趣

強化雲端資料安全,傳 Google 擬採區塊鏈技術
警惕你的雲端資料,以色列公司開發竊取雲端資料間諜工具
Google 的雲端資料庫Cloud SQL:開始支持 PostgreSQL
微軟海底雲端資料中心加裝鏡頭,直播海底看魚游