hudi clustering 資料聚集(三 zorder使用)

努力爬呀爬發表於2021-11-13

目前最新的 hudi 版本為 0.9,暫時還不支援 zorder 功能,但 master 分支已經合入了(RFC-28),所以可以自己編譯 master 分支,提前體驗下 zorder 效果。

環境

1、直接下載 master 分支進行編譯,本地使用 spark3,所以使用編譯命令:

mvn clean package -DskipTests -Dspark3

2、啟動 spark-shell,需要指定編譯出來的 jar 路徑:

spark-shell --jars /<path-to-hudi>/packaging/hudi-spark-bundle/target/hudi-spark3-bundle_2.12-0.10.0-SNAPSHOT.jar  --conf 'spark.serializer=org.apache.spark.serializer.KryoSerializer'

zorder commit 程式碼簡略分析

相關配置

HoodieClusteringConfig.java 中新增了 zorder 相關的配置,主要包括:

  • 該功能的使能(預設關閉)
  • 該功能使用的曲線型別(目前只實現 z-order,後續會實現 hilbert)
  • 曲線生成方式(包括 direct 和 sample,預設為 direct)
  • 資料跳過功能(預設開啟)。

相關依賴

1、該配置在 HoodieClusteringConfig 定義,所以該功能的執行需要依賴 clustering ,會在聚集操作後對資料進行重新排序、寫入。

2、該功能會生成自己的索引,索引記錄的位置在 .hooie/.zindex 下,在 HoodieTableMetaClient.java 中定義: public static final String ZINDEX_NAME = ".zindex";

3、該功能的索引列,由 hoodie.clustering.plan.strategy.sort.columns 決定,可指定多列,不同列用英文逗號分割,具體可參考 updateOptimizeOperationStatistics 函式。

相關限制

1、該功能目前支援 spark,暫時沒有提供 Flink 和 Java 的實現。

2、在 zindex 中,只記錄了最大值、最小值 和 null 值個數,具體可參見 saveStatisticsInfo 函式。

3、該功能支援的資料型別,具體可參見 createZIndexedDataFrameByMapValue 函式。

zvalue實現

1、direct 的 zvalue 生成: ZCurveOptimizeHelper.java

2、sample 的 zvalue 生成: RangeSample.scala

spark-shell 程式碼

import org.apache.hudi.QuickstartUtils._
import scala.collection.JavaConversions._
import org.apache.spark.sql.SaveMode._
import org.apache.hudi.DataSourceReadOptions._
import org.apache.hudi.DataSourceWriteOptions._
import org.apache.hudi.config.HoodieWriteConfig._
import org.apache.hudi.config.HoodieClusteringConfig._

val t1 = "t1"
val t2 = "t2"
val basePath = "file:///tmp/hudi_data/"
val dataGen = new DataGenerator(Array("2020/03/11"))

// 生成資料
var a = 0;
var ups = new Array[java.util.ArrayList[String]](8)
var ups = new Array[java.util.List[String]](8)
for (a <- 0 to 7) {
    ups(a) = convertToStringList(dataGen.generateInserts(10000));
}

for (a <- 0 to 7) {
    val df = spark.read.json(spark.sparkContext.parallelize(ups(a), 1));
    
    df.write.format("org.apache.hudi").
        options(getQuickstartWriteConfigs).
        option(PRECOMBINE_FIELD_OPT_KEY, "ts").
        option(RECORDKEY_FIELD_OPT_KEY, "uuid").
        option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
        option(TABLE_NAME, t1).
        // 每次寫入的資料都生成一個新的檔案		
        option("hoodie.parquet.small.file.limit", "0").
        mode(Append).
        save(basePath+t1);

    df.write.format("org.apache.hudi").
        options(getQuickstartWriteConfigs).
        option(PRECOMBINE_FIELD_OPT_KEY, "ts").
        option(RECORDKEY_FIELD_OPT_KEY, "uuid").
        option(PARTITIONPATH_FIELD_OPT_KEY, "partitionpath").
        option(TABLE_NAME, t2).
        // 每次寫入的資料都生成一個新的檔案		
        option("hoodie.parquet.small.file.limit", "0").
        // 每次操作之後都會進行clustering操作
        option("hoodie.clustering.inline", "true").
        // 每4次提交就做一次clustering操作
        option("hoodie.clustering.inline.max.commits", "8").
        // 指定生成檔案最大大小
        option("hoodie.clustering.plan.strategy.target.file.max.bytes", "1400000").
        // 指定小檔案大小限制,當檔案小於該值時,可用於被 clustering 操作
        option("hoodie.clustering.plan.strategy.small.file.limit", "1400000").
        // 指定排序的列
        option("hoodie.clustering.plan.strategy.sort.columns", "begin_lat,end_lat").
        // 使能zorder
        option(LAYOUT_OPTIMIZE_ENABLE.key(), true).
        mode(Append).
        save(basePath+t2);
}

// 建立臨時檢視
spark.read.format("hudi").load(basePath+t1).createOrReplaceTempView("t1_table")
spark.read.format("hudi").load(basePath+t2).createOrReplaceTempView("t2_table")

這裡建立了2張表t1和t2,其中t1是普通的表,t2是使用了zorder排序的表。

共生成8組資料,總共80000條資料,生成對應8個資料檔案(t2表修改檔案的最大最小值,使其在資料合併之後仍然是8個檔案,對應的配置是)hoodie.clustering.plan.strategy.target.file.max.bytes 和 hoodie.clustering.plan.strategy.small.file.limit。

針對 begin_lat 和 end_lat 列進行排序,使用預設的 direct 方式。

使用 inline 方式觸發 clustering,在每 8 次提交進行一次 clustering。

現象及分析

1、在 t1 目錄下,只有對應的 8 個 parquet 資料檔案,在 t2 目錄下,有 16 個 parquet 資料檔案,其中 8 個是原始的資料檔案,另外 8 個是 clustering 後新生成的資料檔案。

2、在 t2 的 .hoodie 下 生成了 .zindex 目錄:

可以使用 parquet-tool.jar 對該檔案進行檢視:

file = 8f06528b-47ae-4b13-b41f-0a5c78851705-0_1-725-722_20211111153247.parquet
begin_lat_minValue = 0.08211371450402716
begin_lat_maxValue = 0.9997316799855066
begin_lat_num_nulls = 0
end_lat_minValue = 0.007866719050410031
end_lat_maxValue = 0.9999245980998445
end_lat_num_nulls = 0

file = 53d66a45-d951-4e14-9344-ff187d12e9a5-0_0-725-721_20211111153247.parquet
begin_lat_minValue = 5.235437913420071E-6
begin_lat_maxValue = 0.9998301829548436
begin_lat_num_nulls = 0
end_lat_minValue = 7.622934875439746E-6
end_lat_maxValue = 0.9999316614851375
end_lat_num_nulls = 0

file = a0663218-7da0-4ac4-8ffc-4616d2d44d1c-0_2-725-723_20211111153247.parquet
begin_lat_minValue = 0.12506873541740904
begin_lat_maxValue = 0.999128795187336
begin_lat_num_nulls = 0
end_lat_minValue = 0.09383249599535315
end_lat_maxValue = 0.49995210011578595
end_lat_num_nulls = 0

file = 70192aaf-4766-441a-8781-ce381a54cf7c-0_3-725-724_20211111153247.parquet
begin_lat_minValue = 0.12503208286353262
begin_lat_maxValue = 0.4999482257584935
begin_lat_num_nulls = 0
end_lat_minValue = 0.25026158207678606
end_lat_maxValue = 0.9998244932648992
end_lat_num_nulls = 0

file = ebc3017f-f93a-4366-bc20-3b9d86d73111-0_7-725-728_20211111153247.parquet
begin_lat_minValue = 0.6250154203734088
begin_lat_maxValue = 0.9999920463384483
begin_lat_num_nulls = 0
end_lat_minValue = 0.5000295773327517
end_lat_maxValue = 0.9999604245270753
end_lat_num_nulls = 0

file = ec3f3bc8-3503-4642-b064-f93fa577ff83-0_6-725-727_20211111153247.parquet
begin_lat_minValue = 0.5000433340037777
begin_lat_maxValue = 0.9999933816913421
begin_lat_num_nulls = 0
end_lat_minValue = 0.3751232553589945
end_lat_maxValue = 0.9997198848519347
end_lat_num_nulls = 0

file = 91f80f48-1837-4ee8-993c-56ffb9669e9e-0_4-725-725_20211111153247.parquet
begin_lat_minValue = 0.25005205774731387
begin_lat_maxValue = 0.9999255937189415
begin_lat_num_nulls = 0
end_lat_minValue = 0.12500497130106802
end_lat_maxValue = 0.9999928793510746
end_lat_num_nulls = 0

file = 73d01fc5-e9b0-4216-afa5-86abffd082e4-0_5-725-726_20211111153247.parquet
begin_lat_minValue = 0.5000222416743727
begin_lat_maxValue = 0.9999855548910661
begin_lat_num_nulls = 0
end_lat_minValue = 0.18757377058575975
end_lat_maxValue = 0.49997508767465637
end_lat_num_nulls = 0

以上只擷取前兩個資料。在該檔案中,會對所有的 parquet 檔案進行統計,統計的資料包括最大值、最小值和null個數,統計的列就是使用 hoodie.clustering.plan.strategy.sort.columns 指定的列。當 spark 進行查詢時,就會使用這些條件來判斷是否要讀取該資料檔案。

3、這裡可以使用 begin_lat 和 end_lat 看下過濾效果:

a、sql("select count(*) from t2_table where begin_lat < 0.2 and end_lat < 0.5").show()

總共80000條資料,查詢了37420條資料,最終得到資料:7951。

b、sql("select count(*) from t2_table where begin_lat < 0.2 and end_lat > 0.8").show()

總共80000條資料,查詢了27473條資料,最終得到資料:3210。

c、sql("select count(*) from t2_table where begin_lat > 0.9 and end_lat < 0.2").show()

總共80000條資料,查詢了49567條資料,最終得到資料:1600。

d、sql("select count(*) from t2_table where begin_lat > 0.95 and end_lat < 0.02").show()

總共80000條資料,查詢了18235條資料,最終得到資料:81。

4、在 t1 表中,由於沒有對 begin_lat 和 end_lat 做任何處理,所以同樣查詢以上4條 sql 時,都會讀取 80000 條資料,而使用 zorder 之後,只分別讀取了 37420,27473,49567,18235 條資料,過濾效果提升明顯。

其實,不使能 zorder 功能,而只使用 clustering 的排序功能,也能做一些過濾,但由於本次實驗中使用的資料分佈較為均勻,所以雖然也可以對兩個欄位做排序,但基本上只會對第一個欄位有較好的過濾效果,有興趣的可以自己嘗試一下。

相關文章