MJDK 是基於 OpenJDK 構建的美團 JDK 發行版。本文主要介紹 MJDK 是如何在保障 java.util.zip.* API 及壓縮格式兼容性的前提下,實現壓縮/解壓縮速率提升 5-10 倍的效果。希望相關的經驗能夠幫助到更多的技術同學。
1 前言
數據壓縮技術 [1] 因可有效降低數據存儲及傳輸成本,在計算機領域有非常廣泛的應用(包括網絡傳輸、文件傳輸、數據庫、操作系統等場景)。主流壓縮技術按其原理可劃分爲無損壓縮 [2] 、有損壓縮 [3] 兩類,工作中我們最常用的壓縮工具 zip 和 gzip ,壓縮函數庫 zlib,都是無損壓縮技術的應用。Java 應用中對壓縮庫的使用包括:處理 HTTP 請求時對 body 的壓縮/解壓縮操作、使用消息隊列服務時對大消息體(如>1M)的壓縮/解壓縮、數據庫寫入前及讀取後對大字段的壓縮/解壓縮操作等。常見於監控、廣告等涉及大數據傳輸/存儲的業務場景。
美團基礎研發平臺曾經開發過一種基於 Intel 的 isa-l 庫優化的 gzip 壓縮工具及 zlib [4] 壓縮庫(又稱:mzlib [5] 庫),優化後的壓縮速度可提升 10 倍,解壓縮速度能提升 2 倍,並已在鏡像分發、圖片處理等場景長期穩定使用。遺憾的是,受限於 JDK [6] 對壓縮庫調用的底層設計,公司 Java8 服務一直無法使用優化後的 mzlib 庫,也無法享受壓縮/解壓縮速率提升帶來的收益。爲了充分發揮 mzlib 的性能優勢爲業務賦能,在 MJDK 的最新版本中,我們改造並集成了 mzlib 庫,完成了JDK中 java.util.zip.* 原生類庫的優化,可實現在保障 API 及壓縮格式兼容性的前提下,將內存數據壓縮速率提升 5-10 倍的效果。本文主要介紹該特性的技術原理,希望相關的經驗給大家帶來一些啓發或者幫助。
2 數據壓縮技術
計算機領域的數據壓縮技術的發展大致可分爲以下三個階段:
詳細時間節點如下:
- 20世紀50~80年代,香農創立信息論,爲數據壓縮技術奠定了理論基礎。期間出現多種經典算法,如 Huffman 編碼、LZ 系列編碼等。
- 1989年,Phil Katz推出文件歸檔軟件 PKZIP(zip 前身),並公開文件歸檔格式 zip 及其使用的數據壓縮算法 deflate(Huffman 與 LZ77 的組合算法)的所有技術參數。
- 1990年,Info-ZIP 小組基於公開的 deflate 算法編寫了可移植的、免費的、開源實現 zip 和 unzip,極大地擴展了 .zip 格式的使用。
- 1992年,Info-ZIP 小組基於 zip 的 deflate 算法代碼,推出了文件壓縮工具 gzip(GUN zip),用於替代 Unix 下的 compress(有專利糾紛)。通常 gzip 會與歸檔工具 tar 結合使用來生成壓縮的歸檔格式,文件擴展名爲 .tar.gz。
- 1995年,Info-ZIP 小組成員Jean-loup Gailly 和 Mark Adler 基於 gzip 源碼中的 deflate 算法實現,推出了壓縮庫:zlib 。通過庫函數調用的方式,爲其他場景(如PNG壓縮)提供通用的壓縮/解壓縮能力。同年,在 RFC 中發佈了 DEFLATE、ZLIB、GZIP 三種數據壓縮格式。其中 DEFLATE 是原始壓縮數據流格式,ZLIB、GZIP 則是在前者的基礎上包裝數據頭及校驗邏輯等。此後隨着 zip、gzip 工具及 zlib 庫的廣泛應用,DEFLATE 成爲互聯網時代數據壓縮格式的事實標準。
- 2010年後,各大型互聯網公司陸續開源了新的壓縮算法及實現,如:LZFSE(Apple)、Brotli(Google)、Zstandard(Facebook)等,在壓縮速度和壓縮比方面均有不同程度的提升。常見的壓縮庫如下(需要注意的是:由於壓縮算法協議的差異,這些函數庫不能交叉使用,數據壓縮/解壓縮必須使用同一種算法操作):
3 壓縮技術在 Java 中的應用及優化思路
前面我們介紹了壓縮技術的基礎知識,本章節主要介紹 MJDK8_mzlib 版本實現壓縮速率 5 倍提升的技術原理。分兩部分進行闡述:第一部分,介紹原生 JDK 中壓縮/解壓縮 API 的底層原理;第二部分,分享 MJDK 的優化思路。
3.1 Java 語言中壓縮/解壓縮 API 實現原理
Java 語言中,我們可以使用 JDK 原生壓縮類庫(java.util.zip.*)或第三方 Jar 包提供的壓縮類庫兩種方式來實現數據壓縮/解壓縮,其底層原理是通過 JNI (Java Native Interface) 機制,調用 JDK 源碼或第三方 Jar 包中提供的共享庫函數。詳細對比如下:
其中在使用方式上,兩者區別可參考如下代碼。
(1)JDK 原生壓縮類庫(zlib 壓縮庫)
zip 文件壓縮/解壓縮代碼 demo(Java)
public class ZipUtil {
//壓縮
public void compress(File file, File zipFile) {
byte[] buffer = new byte[1024];
try {
InputStream input = new FileInputStream(file);
ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
zipOut.putNextEntry(new ZipEntry(file.getName()));
int length = 0;
while ((length = input.read(buffer)) != -1) {
zipOut.write(buffer, 0, length);
}
input.close();
zipOut.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//解壓縮
public void uncompress(File file, File outFile) {
byte[] buffer = new byte[1024];
try {
ZipInputStream input = new ZipInputStream(new FileInputStream(file));
OutputStream output = new FileOutputStream(outFile);
if (!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdir();
}
if (!outFile.exists()) {
outFile.createNewFile();
}
int length = 0;
while ((length = input.read(buffer)) != -1) {
output.write(buffer, 0, length);
}
input.close();
output.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
gzip 文件壓縮/解壓縮代碼 demo(Java)
public class GZipUtil {
public void compress(File file, File outFile) {
byte[] buffer = new byte[1024];
try {
InputStream input = new FileInputStream(file);
GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream(outFile));
int length = 0;
while ((length = input.read(buffer)) != -1) {
gzip.write(buffer, 0, length);
}
input.close();
gzip.finish();
gzip.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void uncompress(File file, File outFile) {
try {
FileOutputStream out = new FileOutputStream(outFile);
GZIPInputStream ungzip = new GZIPInputStream(new FileInputStream(file));
byte[] buffer = new byte[1024];
int n;
while ((n = ungzip.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
ungzip.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
(2)第三方壓縮類庫(此處以Google推出的snappy壓縮庫舉例,其他第三方類庫原理基本類似)分成兩步。
第一步:pom文件中添加依賴Jar包(C語言)
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
<version>1.1.8.4</version>
</dependency>
第二步:第二步,調用接口進行壓縮/解壓縮操作(C語言)
public class SnappyDemo {
public static void main(String[] args) {
String input = "Hello snappy-java! Snappy-java is a JNI-based wrapper of "
+ "Snappy, a fast compresser/decompresser.";
byte[] compressed = new byte[0];
try {
compressed = Snappy.compress(input.getBytes("UTF-8"));
byte[] uncompressed = Snappy.uncompress(compressed);
String result = new String(uncompressed, "UTF-8");
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
綜上所述,JDK 中默認使用的壓縮庫是 zlib,雖然業務可以通過第三方 Jar 包的方式使用其他的壓縮庫算法,但是因爲 Snappy 等算法的壓縮數據格式與 zlib 支持的 DEFLATE、ZLIB、GZIP 不同,混合使用會有兼容性問題。
除此之外, zlib 庫(1995年推出)本身的迭代速度非常緩慢(原因:應用範圍廣且穩定、無商業組織維護),這裏使用測試集 Silesia corpus 測試了 OpenJDK 7u76(2014 年發行)、8u45(2015 年發行)、8u312(2022 年發行)中內置壓縮類庫的性能,從圖表中可看出,三者在壓縮耗時、壓縮比兩方面均未有明顯的優化效果,難以滿足業務日益增長的壓縮性能需求場景。因此,我們選擇在 MJDK 中集成 zlib 優化,實現既兼容原生接口實現,又能提升壓縮性能的效果。
Silesia corpus是壓縮方法性能基準測試集,提供一套涵蓋現時使用的典型資料類別的檔案資料。文件的大小在6 MB 到51 MB 之間,文件格式包括 text、exe、html、picture、database、bin data 等。測試數據類別如下:
3.2 MJDK 優化方案
通過 3.1 章節,我們知道 Java 原生的 java.util.zip.* 類庫中的數據壓縮/解壓縮能力最終是調用 zlib 庫實現的,因此 JDK 的壓縮性能提升問題就可轉換爲對 JDK 使用的 zlib 庫的優化。
3.2.1 優化思路
除原生 zlib 外,同樣使用 deflate 算法的壓縮庫有Intel ISA-L、Intel IPP、Zopfli,直接基於 zlib 源碼優化的項目有 zlib-cloudflare,它們與 zlib 間的對比如下:
綜上,我們選擇基於 Intel 開源的 ISA-L(原理是使用 intel sse/avx/avx2/avx256 的擴展指令,並行運算多個流來提升底層函數的執行性能) 來完成 zlib 的改造優化。
1. zlib 改造流程(重點在 API 的兼容性改造)
優化後的 mzlib 庫在線上穩定運行 3 年以上,壓縮速率提升在 5 倍以上,有效解決了上文提到基礎研發平臺曾在鏡像構建、圖片處理等場景面臨過壓縮/解壓縮耗時較高的問題。
2. JDK 層面變更
3.2.2 優化效果
測試說明
- 測試集:Silesia corpus
- 測試內容:GZip 壓縮/解壓縮文件、Zip 壓縮/解壓縮文件
測試結論
- 兼容性測試(通過):改造後的 Java 類庫的 Zip、Gzip 壓縮/解壓縮接口可正常使用,與原生 JDK 中的接口交叉進行壓縮/解壓縮操作驗證通過。
- 性能測試(通過):在同一基準 update 版本下,MJDK8_mzlib 數據壓縮耗時比 OpenJDK8 降低 5-10 倍,壓縮比無較大波動(增加 3% 左右)。
目前,美團內部的文檔協同服務已使用該 MJDK 版本,進行用戶協同編輯記錄數據(> 6M)的壓縮存儲,驗證了該功能在線上的穩定運行,壓縮性能提升在 5 倍以上。
4 本文作者
豔梅,來自美團基礎研發平臺。
5 參考文獻
- [1] Comparison of Brotli, Deflate, Zopfli, LZMA, LZHAM and Bzip2 Compression Algorithms
- [2] zip、gzip、zlib的區別
註釋
- [1] 數據壓縮技術:在不丟失有用信息的前提下,通過相應的算法縮減信源數據冗餘,從而提高數據存儲、傳輸和處理效率的技術。
- [2] 無損壓縮:利用數據的統計冗餘進行壓縮,常見的無損壓縮編碼方法有 Huffman編碼,算術編碼,LZ 編碼(字典壓縮)等。數據統計冗餘度的理論限制爲2:1到5:1,所以無損壓縮的壓縮比一般比較低。這類方法廣泛應用於文本數據、程序等需要精確存儲數據的壓縮,
- [3] 有損壓縮:利用了人類視覺、聽覺對圖像、聲音中的某些頻率成分不敏感的特性,允許壓縮的過程中損失一定的信息,以此換來更大的壓縮比。廣泛應用於語音、圖像和視頻數據的壓縮。 -[4] zlib:zlib 是基於 DEFLATE 算法實現的,一套完全開源、通用的無損數據壓縮庫。也是目前應用最廣泛的壓縮庫。在網絡傳輸、操作系統、圖像處理等領域均有大量使用。比如:
- [5] mzlib:美團基於 Intel 的 isa-l 庫優化的 zlib 壓縮庫。
- [6] JDK:Java Development Kit,是 Sun 公司針對Java開發人員發佈的免費軟件開發工具包,是 Java 開發的核心組件之一,包含了 Java 編譯器、Java 虛擬機、Java 類庫等開發工具和資源。
- [7] JNI (Java Native Interface) :JNI是一個本地編程接口。它允許在 Java 虛擬機中運行的 Java 代碼與用其他編程語言(如 C、C++ 和彙編)編寫的應用程序和庫進行互操作。
| 在美團公衆號菜單欄對話框回覆【2022年貨】、【2021年貨】、【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可查看美團技術團隊歷年技術文章合集。