本文作者|華為雲資料庫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組合類的壓縮演演算法之間。
注意我們的壓縮方案實際上是離線的,也就是資料剛生成時必然是熱資料,它們不會觸發壓縮,業務存取這些資料的效能也不會受任何影響;隨著時間的推移,這些資料的溫度會逐漸降低,最終被獨立的壓縮任務識別為冷資料並進行壓縮。
選擇在業務低峰期執行這些壓縮任務、並控制其資源消耗是運維端需要關注的問題。在這塊我們提供了豐富的運維手段,包括指定運維視窗、壓縮任務的並行度、每個壓縮任務的壓縮資料量等。對於絕大多數業務來說,單位時間內新增的資料量實際是比較有限的,因此業務也可以選擇一個特定的時間段集中完成壓縮任務,比如每個月第一天的凌晨兩點到四點,完成3個月前新增冷資料的壓縮。
業務在決定開啟壓縮之前,可能希望先了解開啟壓縮後的收益,並根據收益大小做出決策。為此我們提供了一個壓縮率評估工具,能夠對目標表的資料進行取樣,並使用和實際壓縮過程完全相同的演演算法對取樣資料進行壓縮,計算壓縮率,但不會實際生成BCA,不會修改任何資料。
如果業務將壓縮資料遷移到另一個表,可能會導致所有資料從壓縮狀態變為非壓縮狀態,從而導致空間膨脹,這並非我們的方案引入的,而是所有壓縮方案都需要解決的問題。如果冷熱判定規則非常確定,那麼業務可以手動執行壓縮任務使壓縮立即生效;對於耗時較長的大容量壓縮表的遷移,業務仍然可以選擇定期地開啟自動壓縮任務來完成。
最後,對於壓縮的開啟和關閉,我們提供最細粒度的控制,無論是普通表、普通分割區表中的單個分割區,還是二級分割區中的任意單個分割區、子分割區,業務都可以單獨開啟或關閉壓縮。這使得對於業務本身已經對資料區分了冷熱(比如基於時間分割區)的場景,仍然可以和我們的壓縮特性很好地配合。
在OLTP表壓縮這個特性中,我們引入了一系列的技術創新,包括全新的壓縮演演算法、細粒度的自動冷熱判定和塊內壓縮支援等,可以在提供合理壓縮率的同時,大幅度降低對業務的影響,我們希望這個特效能夠在支援關鍵線上業務的容量控制中發揮重要價值。
接下來我們還將在降低引入壓縮對業務的影響、部分解壓特性、OLTP索引壓縮等方面持續創新迭代,我們希望能夠有開創性的技術突破來解決相關的問題,為業務創造更大價值。