一花一世界,一樹一菩提:編碼壓縮探索與實踐

前言

你可曾面臨這種問題,高併發條件下數據必須99%cache命中才能知足性能需求?html

你可曾面對這種場景,對cache集羣不斷擴容是否讓你感到厭煩?java

那麼是否有一種辦法在提升緩存命中率的同時,又能提升緩存集羣鍵個數,甚至於提升整個集羣的吞吐?web

本文將從實踐出發,對編碼方式及壓縮算法進行介紹,並分享了在真實項目中使用緩存壓縮對性能的影響。redis

(想自學習編程的小夥伴請搜索圈T社區,更多行業相關資訊更有行業相關免費視頻教程。徹底免費哦!)算法

探索篇

提升緩存命中率有不少種方法:選擇好的緩存淘汰算法。當然有一些本地緩存支持LFU或者更新的Tiny LFU算法。不過對於分佈式緩存而言,memcached和redis底層都使用了LRU算法,並不支持淘汰算法的替換。編程

由此,咱們想到第二個思路:提高集羣item數量。api

在不擴容、不拆分item的前提下,便理所應當的想到了對item進行壓縮存儲。緩存

那麼接下來須要肯定的就是具體的壓縮方案:即對何種數據進行壓縮纔是最有效的?採用何種壓縮算法纔是最適合咱們的系統的?併發

編碼方式

不一樣的壓縮算法之間區別最大的即在於編碼方式。所謂編碼其本質即把信息進行某種方式的轉換,以便讓信息可以裝載進目標載體。從信息論的角度來講,編碼的本意並不是改變信息的熵密度,而只是改變信息的表現形式。app

遊程編碼:最易理解的編碼方式

該算法的實現是用當前數據元素以及該元素連續出現的次數來取代字符串中連續出現的數據部分。
在這裏插入圖片描述
aaaaaaaaaabbbaxxxxyyyzyx

字符串長度爲24,使用遊程算法,咱們用較短的字符串後加一個計數值來替換遊程對象。

a10b3a1x4y3z1y1x1

經過遊程編碼編碼後的字符串長度爲17,只有原先的71%,但仍有優化的餘地,好比對於只出現一次的字符,不在後面追加1.

a10b3ax4y3zyx

這樣編碼得來的字符串只有13,只有原先的51%

熵編碼

熵編碼被人所熟知的一種便是Huffman編碼,它的原理是根據字符在原始串中出現的機率。經過構造一顆二叉樹來爲每一個字符產生對應的碼字,又稱最佳編碼。

算術編碼

算術編碼一樣是一種熵編碼,它一樣是基於字符在原始串中出現的機率。所不一樣的是Huffman編碼對每一個字符產生碼字。而算術編碼一般將原始串編碼爲一個介於0~1之間的小數。
好比說對於原始串ARBER,咱們得出每一個字符出現的機率表:

Symbol Times P Interval
A 1 0.2 0 - 0.2
B 1 0.2 0.2 - 0.4
E 1 0.2 0.4 - 0.6
R 2 0.4 0.6 - 1.0

經過這張機率表,咱們能夠將0~1這個區間按照機率劃分給不一樣的字符。
在這裏插入圖片描述
編碼器將當前的區間分紅若干子區間,每一個子區間的長度與當前上下文下可能出現的對應符號的機率成正比。當前要編碼的符號對應的子區間成爲在下一步編碼中的初始區間。
在這裏插入圖片描述
ARBER爲例,由上圖能夠獲得如下類推流:

A的區間是(0,0.2),這個區間成爲下一步編碼的初始區間。R的區間是(0.6,1.0),那麼在初始區間(0,0.2)中取相對的(0,6,1.0)區間即(0,12,0.2)作這一步編碼結果,也便是下一步編碼的子區間。以此類推,最終獲得了(0.14432,0.14456)做爲結果區間。

這裏咱們只是介紹算術編碼的思想,使用十進制也是爲了方便理解。在實際算法實現中,機率以及區間的表示都是使用二進制的小數去表示。極可能不是一精確的小數值,這必定程度上也會影響算法的編碼效率。

從實用效果上,算術編碼的壓縮比通常要好於Huffman。但Huffman的性能要優於算術編碼,二者都有自適應的算法,沒必要依賴全文進行機率統計,但畢竟算術編碼仍是須要更大的計算量。

字典編碼

字典編碼是指用符號代替一串字符,在編碼中僅僅把字符串當作是一個號碼,而不去管它來表示什麼意義。

LZ77編碼

LZ77是Abraham Lempel與Jacob Ziv在1977年以及1978年發表的論文中的一種無損壓縮編碼。

它經過使用編碼器或者解碼器中已經出現過的相應匹配數據信息替換當前數據從而實現壓縮功能。這個匹配信息使用稱爲長度-距離對的一對數據進行編碼。

編碼器和解碼器都必須保存必定數量的最近的數據,如最近2 KB、4 KB或者32 KB的數據。保存這些數據的結構叫做滑動窗口,由於這樣因此LZ77有時也稱做滑動窗口壓縮。

關於LZ77編碼,還有一點須要瞭解的是它如何表示被壓縮數據。它並不是是對單個字符進行編碼,而是對匹配的字符串進行編碼。形式爲(p,l,c).p表示匹配在滑動窗口的相對起始下標,l表示匹配到滑動窗口的字符串的長度,c表示第一個未匹配的字符。

ABABCBABABCAC舉例,使用長度爲8byte的滑動窗口,長度爲4byte的前向緩衝區。
在這裏插入圖片描述

ANS編碼

ANS是前兩類編碼算法戰爭的終結者。它在2014年被提出來,隨後很快就獲得了大量應用。本質上屬於算術編碼,但它成功地找到了一個用近似機率表示的表格,將原來的機率計算轉換爲查表。因此它是一個達到Huffman編碼效率的算術編碼方法。FSE(Finite State Entropy)是ANS最爲著名的實現。

實際上,大多數壓縮算法的實現每每不會只基於一種特定的編碼方式,而是將多種編碼方式組合起來使用。關於壓縮算法和編碼方式的關係,能夠簡單將其考慮成排序算法和各類實現類比起來。

無損壓縮算法實現

壓縮算法能夠按照特定的編碼機制用比未經編碼少的數據位元(或者其它信息相關的單位)表示信息。如下所討論的壓縮算法都是無損壓縮算法,本質並不會減小信息熵。

不一樣的壓縮算法針對不一樣的測試集,在壓縮比和吞吐率方向上有不一樣的變相。爲了儘量減少這種不一樣測試集帶來的偏差。Silesia壓縮語料庫提供了text, exe, pdf, html等常見的格式內容。所以,諸多的壓縮算法都將Silesia語料庫做爲測試基準來測試其性能。

但須要注意的是:通用的標準並不必定適合你的數據。必定要根據本身的數據進行實測後選擇壓縮算法。

Deflate

Deflate基於LZ77和Huffman編碼方式的變形,由於歷史緣由,在不少地方獲得了應用,好比zip,Gzip,png等處。

Gzip

一種廣爲人知的壓縮算法。其提供了不一樣的壓縮級別,使用壓縮比來交換吞吐率。但通常來講,Gzip的壓縮比較高,響應的吞吐率較差。咱們知道在http中,Accept-Encoding經常選擇使用Gzip用來提升傳輸效率。

LZ4

lz4最大特色是其吞吐率特別高,提供了單核500MB/s的壓縮速度,解壓速度更是上GB/s,已經基本達到了多核系統的內存速度的上限。

Snappy

Google在2011年開源的通用壓縮算法,使用C++實現。號稱注重吞吐率但實際上跟lz4還有較大的差距。官方的意思大概是相比於Gzip有很大提高。

Zstandard

在這裏插入圖片描述
Facebook推出的實時壓縮算法,全稱爲Zstandard,使用C實現,基於FSE熵編碼。最大的特色是對小數據使用了「字典訓練」的模式,在使用字典的條件下,其壓縮比和吞吐率會有很大幅度的提升。它一樣提供了22種壓縮級別,官方默認的壓縮級別爲3,但實測在最低壓縮級別下,其表現已經十分優秀。

Brotli


Google出品的另外一種壓縮算法實現,基於通用的LZ77和Huffman編碼,其比較特殊的地方是使用了二階上下文建模,能夠簡單理解爲根據上下文去判斷下一個字符出現的機率從而實現壓縮。在速度下和Deflate很接近,但卻提供了更高的壓縮比。目前主要的應用方向應該是web內容的壓縮。

另外一個主要的特色的是brotli內置了經常使用詞的字典,已被證實能夠增長壓縮比。

實踐篇

本次實踐的對象是約1.1KB的pb字節對象。系統原先的存儲方式是Java pojo類轉爲pb對象再轉爲pb字節對象存至memcached,讀寫比爲35.這樣一種場景其實也符合大部分互聯網公司的實際業務場景,讀多寫少,開發人員更關心讀的吞吐和CPU的佔用率。

所以,所使用的性能測試方案分爲兩步:在本地使用jmh測試諸多壓縮算法的壓縮比和吞吐率;線上使用memcached測試集羣mock大量讀請求,測試CPU佔用和吞吐。

壓縮比

Method maxRatio minRatio averageRatio
使用zstd進行壓縮 2.43 1.19 1.72
使用同組數據訓練獲得的dict進行zstd壓縮 7.22 1.87 4.58
使用不一樣組數據訓練獲得的dict進行zstd壓縮 5.40 1.70 4.03
使用Gzip進行壓縮 2.49 1.25 1.78
使用Brotli進行壓縮 3.00 1.30 2.00

吞吐率

Benchmark                                   Mode  Cnt   Score    Error   Units
CodecBenchmark.compressWithBrotli           thrpt    3   0.015 ±  0.001  ops/ms
CodecBenchmark.compressWithGzip             thrpt    3   0.773 ±  0.463  ops/ms
CodecBenchmark.compressWithDictZstd         thrpt    3   5.307 ±  1.827  ops/ms
CodecBenchmark.compressWithZstd             thrpt    3   2.740 ±  0.161  ops/ms
CodecBenchmark.decompressWithBrotli         thrpt    3   1.079 ±  0.154  ops/ms
CodecBenchmark.decompressWithGzip           thrpt    3   3.187 ±  0.568  ops/ms
CodecBenchmark.decompressWithDictZstd       thrpt    3   9.501 ±  2.447  ops/ms
CodecBenchmark.decompressWithZstd           thrpt    3   6.482 ±  1.330  ops/ms
CodecBenchmark.fromPbBytes                  thrpt    3  27.149 ± 14.017  ops/ms
CodecBenchmark.toPbBytes                    thrpt    3  20.594 ±  9.530  ops/ms

線上性能測試

操做 CPU平均使用率 memcached QPS
無壓縮讀 431% 15.35k
zstd解壓讀 745% 17.42k
zstd帶字典的解壓讀 695% 20.13k

結果分析

  • zstd使用訓練過的字典在壓縮比和吞吐率變現都十分出色
  • 字典面對與訓練集不一樣的輸入發生了退化現象
  • 使用壓縮後,單個對象體積變小也所以CPU等io的時間縮短。因此反而提升了qps

總結

因此本文寫到這裏,能夠得出結論只有一個,選擇一個文本壓縮算法最有效的方式是實驗,不要輕人別人的測試結果。只能經過實驗,才能得出一個更爲有效的算法以及參數的選擇。

另外,在爲實際生產環境選擇開源項目時還有其餘因素須要考慮。好比在筆者測試的過程當中,不一樣壓縮算法對於語言的支持也不相同。好比zstd就比較好,第三方的java binding庫更新及時,對原生支持較好,封裝的api註釋清晰方便理解。而反觀brotli,官方隨提供了Java binding庫,但只提供瞭解壓,並未提供解壓。性能測試所使用的jbrotli庫已經數年未更新,且須要開發人員本身編譯安裝JNI庫。

相關文章
相關標籤/搜索