對於GaussDB來說,在今天引入數據壓縮,究竟能夠給客戶帶來什麼不一樣的價值,是過去一段時間我們一直在思考的問題。 ...
本文作者|華為雲資料庫GaussDB首席架構師 馮柯
【背景介紹】
數據壓縮與關係資料庫的結合,早已不是一個新鮮的話題,當前我們已經看到了各種各樣資料庫壓縮的產品和解決方案。對於GaussDB來說,在今天引入數據壓縮,究竟能夠給客戶帶來什麼不一樣的價值,是過去一段時間我們一直在思考的問題。
為了回答這個問題,我們首先對各種通用壓縮演算法進行了廣泛的測試,從性能最好的LZ4/Snappy,到性能與壓縮率均衡的Zstd/Zlib,再到強調壓縮率的LZMA/BZip。我們發現:即使是性能最好的壓縮演算法,仍然無法做到對一個線上資料庫的性能不產生顯著影響。我們也調研了資料庫領域的各種編碼方法,包括近幾年學術界發佈的一些基於預測和線性擬合的編碼方法,從研究發佈的測試結果及實測來看,資料庫編碼用於解決特定數值分佈的可壓縮性問題,與壓縮演算法的成熟度相比,當前並沒有一種通用的資料庫編碼方法,能夠在大多數真實數據集中的場景下提供穩定的壓縮率。
這是我們對於資料庫壓縮這個領域的一個基本技術判斷。過去的產品實踐也驗證了這一點,我們看到很多商業資料庫和開源資料庫都提供了對於壓縮的支持,絕大多數時候,留給客戶的選擇就是決定要不要在特定的表上開啟壓縮。開啟壓縮意味著空間節省,但同時意味著性能下降,這個看似簡單的選擇恰恰是客戶最難做的。這也是為什麼有了這麼多資料庫壓縮的產品,我們卻很少能看到數據壓縮真正廣泛應用在資料庫線上業務中的根本原因。
這給了我們更多的啟示。我們相信,真正可被應用的資料庫壓縮技術,能夠去兼顧壓縮率與業務影響的平衡,應該是選擇性的。即我們能夠基於技術去判定數據的溫度,並基於這樣的判定,去選擇性地壓縮業務中相對較冷的數據,而不去碰那些相對較熱的數據。
這樣的技術選擇意味著我們無法去滿足所有業務場景,我們要求業務的數據溫度分佈,必須滿足80-20分佈規則。即我們去壓縮那些占用80%存儲需求、但只占用20%計算需求的冷數據,而不去碰那些只占用20%存儲需求、但卻占用80%計算需求的熱數據。幸運的是,我們發現絕大多數對於容量控制有需求的業務,都具備這樣的特征。
【場景及目標選擇】
通過對大量業務場景的分析,我們發現業務對於資料庫壓縮技術的需求是多元化的,有線上交易業務(OLTP)存儲壓縮的場景,有分析業務(OLAP)存儲壓縮的場景,有歷史業務存儲壓縮的場景,也有容災業務傳輸壓縮的場景。不同的場景,對於壓縮技術的訴求,如果從壓縮性能、壓縮率、解壓性能的三維指標去看,從對業務侵入的容忍度去看,是完全不同的。
這意味著如果我們想要打造一個全場景的GaussDB高級壓縮特性,它應該是多個技術的組合,包括不同的壓縮演算法、不同的冷熱判定模型及方法、不同的數據存儲組織等,通過不同的技術組合及應用去滿足不同的場景需求。
這同時意味著我們在不同壓縮適用場景的支持上需要有個優先順序的取捨。我們的答案是選擇去優先支持OLTP存儲壓縮場景,這是我們認為資料庫壓縮技術最有價值的業務領域,當然也是技術挑戰最大的領域。
確定場景之後,接下來是確定技術目標,我們面向這個場景,究竟要打造什麼樣的核心競爭力,這取決於我們對於典型客戶場景的分析。我們識別了兩類典型客戶場景:
場景A:客戶業務來自於IBM小機,單庫容量50TB,遷移到開放平臺後,面臨容量過大和運維視窗過長問題。選擇拆庫意味著分散式改造,對於一個已經穩定運行許多年的存量關鍵業務來說,這種技術選擇風險過高。選擇壓縮可以顯著降低容量風險,但業務最初的設計並沒有考慮冷熱分離(比如基於時間維度建立分區),需要一種零侵入的壓縮技術支持,同時對業務性能影響足夠低。
場景B:客戶業務基於分散式集群部署,單集群容量已經超過1PB,並且仍在快速增長,需要定期擴容。選擇壓縮可以降低擴容頻率,顯著降低業務的軟硬體成本,並減少變更風險。但業務的數據分佈設計是面向擴展性的(比如基於用戶維度建立分區),沒有考慮冷熱分離,因此同樣的,業務需要一種零侵入的壓縮技術支持,同時對性能影響足夠低。
通過對客戶典型場景的需求梳理,我們確定了GaussDB OLTP存儲壓縮的基本設計目標:1)冷熱判定對業務應該是零侵入的,不應對業務的已有數據分佈、邏輯模型有任何依賴;2)對業務影響必須足夠低,我們定義目標低於10%,並挑戰5%;3)提供合理的壓縮率,我們定義目標不低於2:1。基本設計目標的定義,使得我們能夠將後續每個具體場景中的技術選擇都變成一個確定性問題。
【冷熱判定】
確定設計目標後,我們開始進行工程落地。有三個問題需要解決:1)如何實現對數據的冷熱判定;2)如何實現壓縮後數據的存儲組織;3)如何實現有競爭力的壓縮演算法。
對於冷熱判定,首先要確定判定的粒度。數據的冷熱判定可以基於不同粒度實現,行級、塊級或表/分區級,粒度越粗,實現的複雜度越低,但對業務的侵入也越大。基於設計目標,很自然的,我們選擇行級的冷熱判定,這是對業務數據分佈依賴最小的方案,我們需要解決的,是如何控制引入冷熱判定的代價。
我們利用GaussDB存儲引擎已有的機制巧妙地解決了這一問題。具體來說,GaussDB存儲引擎在每行數據的元數據Meta中記錄了最近一次修改該行的事務ID(XID),該信息被用來支持事務的可見性判定,從而實現多版本併發控制(MVCC)。對於特定行來說,如果其XID足夠“老”,老到它對所有當前已經活躍的事務都可見,那麼這時候我們實際上已經不關註XID的具體值,我們可以通過引入一個特定的標誌位(FLG)來記錄這一點,而原來XID中填充的值可以被一個物理時間來代替,這個物理時間就表徵了其所屬行最後一次修改時間的上限(LMT,Last Modified Time)。很顯然,LMT可以用來支持冷熱判定(具體見圖1):
圖1:行級冷熱判定
上述方案的好處是引入LMT並沒有增加額外開銷,對業務的邏輯模型也沒有任何依賴,在大多數時候,如果不是特別嚴格要求,業務可以定義一個簡單的規則來實現冷熱判定,比如:
AFTER 3 MONTHS OF NO MODIFICATION
此時系統會掃描目標表,對於所有滿足當前時間減去LMT超過3個月的行進行壓縮。
註意在上述方案中,我們實際上只識別了行的寫熱點,但並沒有識別行的讀熱點,我們只知道滿足條件的行3個月內未發生任何更新,但我們無法確認這些行在3個月內是否被頻繁讀取。維護行的讀熱點,目前從技術上沒有低成本的解決方案。對於像訂單明細這樣的流水類業務,這個方案可以很好地工作,因為數據的讀和寫呈現出相同的溫度特征,其訪問頻率隨著未修改時間的增加不斷衰減。但對於像手機相冊這樣的收藏類業務,僅識別寫可能是不夠的,因為一個很早建立的收藏關係仍然可能被頻繁訪問。
這意味著,即使系統進行了冷熱判定,我們仍然需要去優化業務可能訪問壓縮數據的場景,我們把這個問題留給了存儲組織和壓縮演算法,對於壓縮演算法來說,我們更關註其解壓性能。
另一個問題是在某些場景下,使用預設的冷熱判定可能是不夠的,比如對於某些類型的交易而言,其產生的訂單明細可能在3個月內確實不會被修改,但會在達到一個特定的觸發條件後被更新(比如解凍擔保交易)。這種場景在實際業務中並不常見,但如果業務確實關註性能,那麼我們支持在預設的冷熱判定規則以外,允許業務自定義規則,比如:
AFTER 3 MONTHS OF NO MODIFICATION ON (order_status = "finished")
此時系統會僅壓縮3個月未修改、且訂單狀態已經完結的數據。
當前我們支持的自定義規則,是任意合法的行表達式,業務可以寫任意複雜的表達式來表徵數據的冷熱判定規則,但表達式中所引用的任何欄位,只能是目標表上的合法欄位。通過這種預設和自定義規則的組合使用,我們提供了業務足夠低的使用門檻和更好的靈活性。
【存儲組織】
當滿足冷熱判定條件的行被壓縮時,我們需要決定如何存儲這些壓縮後的數據,基於設計目標,我們選擇了對業務侵入最小的存儲組織實現——塊內壓縮。
我們知道關係資料庫的存儲組織都是基於固定長度的分塊的,在GaussDB資料庫中,典型的數據塊大小為8KB,選擇更大的數據塊顯然有利於壓縮,但對業務性能會造成更大的影響。所謂塊內壓縮,是指:1)單個塊內所有滿足冷熱判定條件的行,會作為一個整體進行壓縮;2)壓縮後形成的數據就存放在當前的數據塊中,存放區域稱為BCA(Block Compressed Area),它通常位於塊的尾部。
塊內壓縮的設計意味著解壓任何數據只依賴於當前塊,而不需要訪問其它的數據塊,從壓縮率的視角看,這樣的設計並不是最友好的,但它非常有利於控制業務影響。註意在我們前面的討論中,即使業務定義了冷熱判定條件,仍然存在一定的概率會訪問壓縮數據,我們希望這個訪問代價能夠有一個確定性的上限。
圖2給出了塊內壓縮的詳細流程:首先,當壓縮被觸發時,系統掃描數據塊中的所有行,根據指定的冷熱判定條件,識別出R1和R3是冷數據(圖2(a));接著,系統將R1和R3作為一個整體進行壓縮,將壓縮後的數據就存放在該數據塊的BCA中(圖2(b));如果業務後續需要更新R1,那麼系統會為更新後的數據生成一個新的拷貝R4,並標識BCA中的R1已經被刪除(如圖2(c));最後,當系統在該數據塊上需要更多空間時,可以回收BCA中屬於R1的空間(圖2(d))。
圖2:塊內壓縮
在整個設計中有兩點需要註意:1)我們實際上只壓縮了用戶數據Data,並沒有壓縮相應的元數據Meta,後者通常用來支持事務的可見性;2)我們支持將冷數據重新變為熱數據,以消除因為冷熱誤判而帶來的影響。同樣地,從壓縮率的視角,這樣的設計並不是最友好的,但它極大地減少了對業務的侵入。簡單來說,業務對於壓縮數據的訪問,與正常數據完全相同,在功能上沒有任何限制,在事務語義上也沒有任何差別。這是非常重要的原則:我們的OLTP存儲壓縮對於業務是完全透明的,這是當前這個特性,以及後續GaussDB高級壓縮系列所有特性都將遵循的基本原則。
【壓縮演算法】
基於設計目標,如果從壓縮率、壓縮性能、解壓性能的三維指標來看,我們實際上需要的是一個能夠提供合理的壓縮率、合理的壓縮性能、但是極致的解壓性能的壓縮演算法,這是我們壓縮演算法設計的基礎。
我們首先測試了直接使用LZ4進行壓縮,LZ4是目前已知的壓縮性能和解壓性能最好的開源三方庫,從實測結果看,LZ4的壓縮率是偏低的。我們仔細分析了其演算法原理,LZ4是基於LZ77演算法的一種實現,LZ77演算法的思想非常簡單,就是把要壓縮的數據看成一個位元組流,演算法從位元組流的當前位置開始,前向尋找和當前位置相同的匹配字元串,然後用匹配到的字元串的長度以及與當前位置的偏移,用來表示被匹配的字元串,從而達到壓縮的效果。從演算法原理上看,LZ77演算法對於長文本會有比較好的壓縮效果,但是對於結構化數據中大量的短文本以及數值類型,效果就有限,我們實際的測試也驗證了這一點。
接下來,我們將壓縮演算法分為了兩層:第一層,我們按列對一些數值類型進行了編碼,我們選擇了簡單的差值編碼,這種編碼足夠輕量級,解壓特定欄位不需要依賴其它欄位的值;第二層,我們將編碼後的數據再調用LZ4進行壓縮。註意在第一層中,我們實際上是按列編碼、按行存儲,這和業界的一般實現(按列編碼並存儲)有很大不同,按列存儲對壓縮率會更加友好,但是按列存儲意味著同一行的數據會被分散到BCA的不同區域,這種傳統的設計無法支持我們後續希望實現的部分解壓,我們將在結束語中更詳細地說明這一問題。
通過實測,我們發現這種列編碼+通用壓縮的實現方式有效地提升了壓縮率,同時控制了業務影響的明顯增加,但兩層實現之間是松耦合的,這引入了許多額外的開銷。因此我們在仔細權衡之後,決定放棄LZ4,而是完全基於LZ77演算法,重新實現一個緊耦合的壓縮演算法。
這在當時看來是一個非常冒險的嘗試,事實上,在我們之前,還沒有任何資料庫內核團隊,會選擇自己去實現一個通用壓縮演算法。但從最後取得的收益來看,我們實際上是打開了一扇全新的大門。當列編碼與LZ77演算法之間的邊界被打破時,我們引入了一系列的優化創新,考慮篇幅原因,我們無法展現全部技術細節,在這裡,我們只介紹兩個小的優化:
第一個優化是內置行邊界。我們發現,當系統採用兩層壓縮演算法後,我們需要額外地保存每一行數據在編碼後的長度,因為我們需要在LZ77演算法解壓後找到每一行的邊界,這是一個不小的開銷。為了消除這個開銷,我們選擇在LZ77的編碼格式中嵌入一個行邊界的標記,這個標記只占用了1個位,其開銷較現有方案大幅降低。當然,這個標記位被占用後,LZ77前向搜索的最大視窗長度減少了一半,但在我們這個場景中,這並不是什麼問題,因為我們的典型頁面長度只有8KB。
第二個優化是2位元組短編碼。原有LZ4的實現中,為了提高壓縮性能,系統使用3位元組編碼來描述一個匹配,這意味著系統能夠識別的最短匹配為4位元組。但是在結構化數據中,3位元組的匹配是非常普遍的,參考下麵一個例子:
A = 1 … B = 2
其中,A和B是同一行數據中的兩個整數型欄位,它們的值分別為1和2,基於當前的位元組序,該行數據實際在記憶體中存放的形式如下所示:
01 00 00 00 … 02 00 00 00
註意上面標紅的部分,很明顯,這裡面有一個3位元組的匹配,但是它無法被LZ4識別。
我們通過在LZ77演算法中額外引入2位元組短編碼來解決這一問題,2位元組短編碼可以識別最小3位元組的匹配,從而相對LZ4能夠提升壓縮率。當然,引入短編碼會有額外的開銷:1)壓縮性能會有一定程度的下降,因為我們需要建立兩個獨立的HASH表,幸運的是,在我們這個場景中,極致壓縮性能並不是我們追求的目標;2)2位元組編碼減少了表達匹配串與被匹配串之間距離的位寬,這意味著3位元組的匹配必須離得更近才能被識別,在我們這個場景中,這並不是什麼問題,因為相對於這個限制,一個典型數據行的長度已經是足夠小的。
【效果評估】
我們使用標準的TPCC測試來評估啟用OLTP存儲壓縮特性對業務的影響。TPCC模型共包含9張表,其中空間會動態增長的流水錶共有3張,在這3張表中,訂單明細表(Orderline表)的空間增長比其它表多一個數量級,因此我們選擇在這張表上開啟壓縮。基於TPCC的業務語義,每筆訂單一旦完成配送,其訂單狀態就進入完結狀態,完結的訂單不會再被修改,但仍有一定的概率被查詢。基於這個語義,我們選擇冷熱判定原則為只壓縮已經完結的訂單。
我們分別測試了在不開啟壓縮和開啟壓縮狀態下系統的性能值,結果如圖3所示:
圖3:業務影響評估
測試結果表明:在TPCC測試場景下,開啟壓縮與不開啟壓縮相比,系統性能大概降低了1.5%。這是一個非常不錯的結果,這意味著即使在超過百萬tpmC的業務峰值場景中,系統也可以開啟壓縮。我們不知道在此之前,業內是否有其它資料庫產品也能夠達到這一水平。
我們測試了Orderline表的壓縮率,作為更豐富的數據集,我們同時選擇了TPCH模型中的4張表(Lineitem、Orders、Customer、Part表)進行測試。為了便於比較,對於每個數據集,我們同時測試了LZ4、ZLIB和我們的壓縮演算法的壓縮率表現,其中ZLIB是強調壓縮解壓性能和壓縮率均衡的演算法,其壓縮解壓性能較LZ4低了5-10倍。最終結果如圖4所示:
圖4:壓縮率評估
測試結果與我們預期的相符,在數值型欄位較多時,我們的壓縮演算法的壓縮率要高於所有通用壓縮演算法,但在文本型欄位較多時,我們的壓縮演算法的壓縮率會介於LZ類和LZ + Huffman組合類的壓縮演算法之間。
【運維TIPS】
註意我們的壓縮方案實際上是離線的,也就是數據剛生成時必然是熱數據,它們不會觸發壓縮,業務訪問這些數據的性能也不會受任何影響;隨著時間的推移,這些數據的溫度會逐漸降低,最終被獨立的壓縮任務識別為冷數據併進行壓縮。
選擇在業務低峰期運行這些壓縮任務、並控制其資源消耗是運維端需要關註的問題。在這塊我們提供了豐富的運維手段,包括指定運維視窗、壓縮任務的並行度、每個壓縮任務的壓縮數據量等。對於絕大多數業務來說,單位時間內新增的數據量實際是比較有限的,因此業務也可以選擇一個特定的時間段集中完成壓縮任務,比如每個月第一天的凌晨兩點到四點,完成3個月前新增冷數據的壓縮。
業務在決定開啟壓縮之前,可能希望先瞭解開啟壓縮後的收益,並根據收益大小做出決策。為此我們提供了一個壓縮率評估工具,能夠對目標表的數據進行採樣,並使用和實際壓縮過程完全相同的演算法對採樣數據進行壓縮,計算壓縮率,但不會實際生成BCA,不會修改任何數據。
如果業務將壓縮數據遷移到另一個表,可能會導致所有數據從壓縮狀態變為非壓縮狀態,從而導致空間膨脹,這並非我們的方案引入的,而是所有壓縮方案都需要解決的問題。如果冷熱判定規則非常確定,那麼業務可以手動執行壓縮任務使壓縮立即生效;對於耗時較長的大容量壓縮表的遷移,業務仍然可以選擇定期地開啟自動壓縮任務來完成。
最後,對於壓縮的開啟和關閉,我們提供最細粒度的控制,無論是普通表、普通分區表中的單個分區,還是二級分區中的任意單個分區、子分區,業務都可以單獨開啟或關閉壓縮。這使得對於業務本身已經對數據區分了冷熱(比如基於時間分區)的場景,仍然可以和我們的壓縮特性很好地配合。
【結束語】
在OLTP表壓縮這個特性中,我們引入了一系列的技術創新,包括全新的壓縮演算法、細粒度的自動冷熱判定和塊內壓縮支持等,可以在提供合理壓縮率的同時,大幅度降低對業務的影響,我們希望這個特性能夠在支持關鍵線上業務的容量控制中發揮重要價值。
接下來我們還將在降低引入壓縮對業務的影響、部分解壓特性、OLTP索引壓縮等方面持續創新迭代,我們希望能夠有開創性的技術突破來解決相關的問題,為業務創造更大價值。