MJDK 如何實現壓縮速率的 5 倍提升?

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 等。測試數據類別如下:

Silesia corpus 測試集說明

3.2 MJDK 優化方案

通過 3.1 章節,我們知道 Java 原生的 java.util.zip.* 類庫中的數據壓縮/解壓縮能力最終是調用 zlib 庫實現的,因此 JDK 的壓縮性能提升問題就可轉換爲對 JDK 使用的 zlib 庫的優化。

3.2.1 優化思路

除原生 zlib 外,同樣使用 deflate 算法的壓縮庫有Intel ISA-LIntel IPPZopfli,直接基於 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] 數據壓縮技術:在不丟失有用信息的前提下,通過相應的算法縮減信源數據冗餘,從而提高數據存儲、傳輸和處理效率的技術。
  • [2] 無損壓縮:利用數據的統計冗餘進行壓縮,常見的無損壓縮編碼方法有 Huffman編碼,算術編碼,LZ 編碼(字典壓縮)等。數據統計冗餘度的理論限制爲2:1到5:1,所以無損壓縮的壓縮比一般比較低。這類方法廣泛應用於文本數據、程序等需要精確存儲數據的壓縮,
  • [3] 有損壓縮:利用了人類視覺、聽覺對圖像、聲音中的某些頻率成分不敏感的特性,允許壓縮的過程中損失一定的信息,以此換來更大的壓縮比。廣泛應用於語音、圖像和視頻數據的壓縮。 -[4] zlib:zlib 是基於 DEFLATE 算法實現的,一套完全開源、通用的無損數據壓縮庫。也是目前應用最廣泛的壓縮庫。在網絡傳輸、操作系統、圖像處理等領域均有大量使用。比如:
    • Linux kernel:使用zlib以實作網路協定的壓縮、檔案系統的壓縮以及開機時解壓縮自身的核心。
    • libpng—:用於PNG圖形格式的一個實現,對bitmap數據規定了 DEFLATE 作爲流壓縮方法。
    • HTTP協議:使用 zlib 對 HTTP 響應頭數據進行壓縮/解壓縮。
    • OpenSSHOpenSSL:以 zlib 達到最佳化加密網路傳輸。
    • SubversionGitCVS版本控制系統,使用 zlib 來壓縮和遠端倉庫的通訊流量。
    • dpkgRPM等包管理軟件:以 zlib 解壓縮 RPM 或者其他封包。
  • [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年貨】等關鍵詞,可查看美團技術團隊歷年技術文章合集。

| 本文系美團技術團隊出品,著作權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明“內容轉載自美團技術團隊”。本文未經許可,不得進行商業性轉載或者使用。任何商用行爲,請發送郵件至[email protected]申請授權。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章