Paimon 跟 Spark 是否也能玩得來

大資料技術前線發表於2023-12-29

來源:安瑞哥是碼農


上篇跟上上篇文章,我們聊了用 Flink API 讀寫(主要是寫) Paimon 表的一些情況,記錄了這個過程中我認為體驗不好的地方,同時也順便測試對比了一下,用 Flink API 在同一場景下寫 Hudi 跟 Paimon 表的效率差異(有興趣的同學可自行檢視歷史文章)


我們都知道,Paimon 是屬於當下被大家所熟知的,號稱4大資料湖技術中(Hudi、Iceberg、Delta lake、Paimon) 唯一一個 Flink 的“近親”,跟 Flink 的配合自然會更加緊密。


但是,作為一款想被更多使用者所接納的技術,自然也就少不了對 Spark 這位江湖元老的支援,至少官方文件是這麼赫然寫著的。


至於 Paimon 跟 Spark 之間配合的默契程度如何,我們們今天就給它“跑起來”,看看實際效果如何?


還是老規矩,我們透過 Spark structured streaming 實時讀取 kafka 資料,然後以流方式寫入 Paimon 表來進行測試。


(PS:本文根據 kafka2.0 + Spark3.2 + Hadoop3.1 + Paimon0.6 展開)



0. 跑前準備


跑步運動員在開跑前,需要先選擇合身的裝備,穿上合腳的跑鞋,那麼這裡也一樣。


從官方文件的描述來看,paimon 只支援 Spark3.1 及以上版本,幸好,我當前使用的是 Spark3.2。


Paimon 跟 Spark 是否也能玩得來

要不怎麼說官方文件有時候過於官方」呢,跟 Flink API 遇到的情況一樣,目前雖然你可以下載到最新 0.7 版本的 paimon-spark 聯合 jar 包。


但是,maven 中央倉庫能提供的,卻依然只有0.6的孵化版本。

Paimon 跟 Spark 是否也能玩得來


至於為什麼我要糾結這個,因為要在我的 Spark 專案中透過 pom 檔案的方式來引入對 paimon 的依賴。

Paimon 跟 Spark 是否也能玩得來


而這,才是一個正規大資料開發的「正規玩法」。



1. 開始編碼


開發環境的基礎依賴解決之後,接下來就要著手編碼了,老規矩,還是參照官方文件一步步來。


Paimon 跟 Spark 是否也能玩得來

瞧這程式碼部分寫的,那叫一個簡潔。


但我告訴你,如果你以為跟它一樣「照貓畫虎就能輕鬆把程式調通,多少會顯得有點天真,以我這麼長時間來的趟坑經驗,它要是不給你設定點障礙,那幾乎是不太可能的事情。

知道你可能很著急,但你先別急,後面我會把遇到的坑一一告訴你。

一番折騰之後,我把調通之後的(資料正常寫入),用 Spark structured streaming 讀 kafka 資料寫 Paimon 表的程式碼提供如下:


package com.anryg.bigdata.streaming.paimon

import com.alibaba.fastjson.{JSONJSONValidator}
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.streaming.OutputMode

/**
  * @DESC: Spark 讀取 Kafka 資料寫 Paimon
  * @Auther: Anryg
  * @Date: 2023/12/28 11:01
  */

object SparkReadKafka2Paimon {

    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("SparkReadKafka2Paimon")/*.setMaster("local[*]")*/
        conf.set("spark.sql.catalog.paimon","org.apache.paimon.spark.SparkCatalog"//必須設定,指定catalog為paimon
        conf.set("spark.sql.catalog.paimon.warehouse","hdfs://192.168.211.106:8020/tmp/paimon"//必須設定
        val spark = SparkSession.builder().config(conf).getOrCreate()

        val rawDF = spark.readStream
            .format("kafka")
            .option("kafka.bootstrap.servers""192.168.211.107:6667")
            .option("subscribe""test")
            .option("failOnDataLoss",false)
            .option("fetchOffset.numRetries",3)
            .option("startingOffsets","latest")
            .load()

        import spark.implicits._

        val ds = rawDF.selectExpr("CAST(value AS STRING)")
            .map(row => {
                val line = row.getAs[String]("value")
                val rawJson = JSON.parseObject(line)      //原始string是一個json,對其進行解析
                val message = rawJson.getString("message")  //獲取業務資料部分
                val fieldArray = message.split(",")
                fieldArray
            }).filter(_.length == 9).map(array =>(array(0),array(1),array(2),array(3),array(4),array(5),array(6),array(7),array(8)))
            .toDF("client_ip","domain","time","target_ip","rcode","query_type","authority_record","add_msg","dns_ip")

        spark.sql("USE paimon"//指定catalog

        /**跟Hudi不一樣,需要提前建立paimon表*/
        spark.sql(
            """
              |CREATE TABLE IF NOT EXISTS data_from_spark2paimon01(
              |client_ip STRING,
              |domain STRING,
              |`time` STRING,
              |target_ip STRING,
              |rcode STRING,
              |query_type STRING,
              |authority_record STRING,
              |add_msg STRING,
              |dns_ip STRING
              |)
              |TBLPROPERTIES (
              |'primary-key'='client_ip,domain,time,target_ip'
              |)
            "
"".stripMargin)

        ds.writeStream
            .outputMode(OutputMode.Append())
            .option("checkpointLocation""hdfs://192.168.211.106:8020/tmp/offset/test/SparkReadKafka2Paimon01")
            .format("paimon")
            .start("hdfs://192.168.211.106:8020/tmp/paimon/default.db/data_from_spark2paimon01")
            .awaitTermination()

    }
}



2. 幾個需要注意的地方


程式碼交代完,我們集中火力,來看看這個過程中,一共可能碰到哪些讓你“惱火”的事。


2.1 注意點一


如果根據官方文件,在 IDEA 編輯器裡,根據常規思維,寫下了 Spark 讀取 kafka 然後寫入 Paimon 的“常規程式碼”後,一執行,你就會收穫第一個報錯:


Exception in thread "main" org.apache.spark.sql.catalyst.analysis.NoSuchDatabaseException: Database 'paimon' not found
 at org.apache.spark.sql.catalyst.catalog.SessionCatalog.requireDbExists(SessionCatalog.scala:218)
 at org.apache.spark.sql.catalyst.catalog.SessionCatalog.setCurrentDatabase(SessionCatalog.scala:313)
 at org.apache.spark.sql.connector.catalog.CatalogManager.setCurrentNamespace(CatalogManager.scala:104)

它告訴你沒有“paimon”這個資料庫,你以為自己需要新建這個庫嗎?


不,它其實是要你新增這個配置而已:


Paimon 跟 Spark 是否也能玩得來

而這個“paimon”也不是什麼database,它其實是我們需要選擇的“catalog”,這裡的錯誤提示會讓人有誤解。


2.2 注意點二


解決完上面那個問題後,繼續除錯,迎接著你的2個報錯,是這個:

Exception in thread "main" java.lang.NullPointerException: Paimon 'warehouse' path must be set
 at org.apache.paimon.utils.Preconditions.checkNotNull(Preconditions.java:65)
 at org.apache.paimon.catalog.CatalogFactory.warehouse(CatalogFactory.java:55)
 at org.apache.paimon.catalog.CatalogFactory.createCatalog(CatalogFactory.java:83)
 at org.apache.paimon.catalog.CatalogFactory.createCatalog(CatalogFactory.java:66)

提示你需要設定“warehouse”的路徑。


之所以說它讓人惱火呢,原因在於,同一份程式碼,你會發現,這個資料寫入的破地址,它居然讓你配置2遍,咋想的?


你需要在程式碼裡配置這個(當然,也可以在外部的執行引數中指定,但我不太習慣):


Paimon 跟 Spark 是否也能玩得來

這是第一次資料位置的指定,但為了程式能順利跑起來,還需要在後面資料寫入的時候再指定一次,這個我們下面聊。


其實以上這兩個坑,在官方文件是有提示的,只不過需要你在編碼的時候,要學會理解這些設定的含義。


Paimon 跟 Spark 是否也能玩得來

透過我的驗證,這3項配置中,前2個是必須的,否則就會丟擲上面我說的異常,而第3項,目前從我的測試來看,不需要。


2.3 注意點三


第3個,可能會因為沒有設定 paimon catalog,而引發的異常如下:

Exception in thread "main" org.apache.spark.sql.AnalysisException: Hive support is required to CREATE Hive TABLE (AS SELECT);
'CreateTable `default`.`data_from_spark2paimon01`, org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe, Ignore

怎麼樣,是不是看得你一臉懵逼?


怎麼就它喵的提示建立 Hive 表失敗了呢?


原因很簡單,因為這個時候,如果你沒有指定 catalog,那麼這個程式碼中的“create table”建表語句,程式會預設你用的 hive catalog,所以會認為你在建立 hive 表,而當前這個建表語句又不符合 hive 建表語句的規範。


咋解決呢?在程式碼中手動指定 catalog。


Paimon 跟 Spark 是否也能玩得來

其實對於這一點,官網也有提示,只不過,你需要知道在程式碼中如何指定:


Paimon 跟 Spark 是否也能玩得來

前一個是指定 catalog,後一個是指定 database,因為預設就是 default,所以也可以不用寫。


2.4 注意點四


這個坑是我認為這幾個裡面,最坑爹的,為啥這麼說呢?


剛才上面不是提到那個“warehouse”路徑設定問題了嘛,前面是這麼設定的:


Paimon 跟 Spark 是否也能玩得來


我在這裡設定了一個資料儲存的總目錄。


如果程式非得讓我在最後資料寫入的時候,還得指定一次路徑,根據我一個正常的人理解,我就會這麼設定:


Paimon 跟 Spark 是否也能玩得來

在之前總目錄的基礎上,再加一個我要建的表名(data_from_spark2paimon01),以此來儲存這個表要寫進去的資料。


這樣思考按理說沒毛病吧?因為 Hudi 對於資料寫入路徑的設計就是這樣的。


可是呢,人家 Paimon 偏要膈應一些。


結果我一執行,你猜會怎麼著?


Exception in thread "main" java.lang.IllegalArgumentException: Schema file not found in location hdfs://192.168.211.106:8020/tmp/paimon/data_from_spark2paimon01. Please create table first.
 at org.apache.paimon.table.FileStoreTableFactory.lambda$create$0(FileStoreTableFactory.java:61)
 at java.util.Optional.orElseThrow(Optional.java:290)

它告訴你,當前這個目錄下找不到“schema file”。


我就奇了怪了,我指定的這個目錄,不就是讓你去寫資料的嗎,你的 schema file 不應該就寫到這個目錄裡面的嗎?


可它... 偏不!


至此,我們不妨來瞅一眼,這個當初我指定的目錄,到底發生了什麼變化:


Paimon 跟 Spark 是否也能玩得來

看到沒,程式自作主張的在我指定的主目錄下,建立了一個“default.db”子目錄,然後在這個子目錄下,再建立了一個以表名(data_from_spark2paimon01)命名的子子目錄。


接著,在這個表名的下級目錄中,又建立了一個“schema”目錄。


我想,這個“schema”目錄,大抵就是剛才那個丟擲的異常,要尋找的吧


於是,把剛才那個最後指定寫入路徑的地址,給改成了這個:


Paimon 跟 Spark 是否也能玩得來

果然,程式終於能正常調通了。



3. 瞅一眼效果


提交到 yarn 叢集之後,跑了一個多小時,沒有出現什麼么蛾子,執行正常:


Paimon 跟 Spark 是否也能玩得來


Paimon 跟 Spark 是否也能玩得來

再看一眼資料目錄,寫了一共 2G 資料,共486個檔案(後續數量沒有再變少)。


Paimon 跟 Spark 是否也能玩得來


從檔案數量上來看,這個小檔案量有點多(預設設定下)。



最後


從這次透過 Spark 寫 Paimon 的 API 來看,過程雖然經歷了一點小坎坷,但基本上透過仔細閱讀文件內容,再根據丟擲的異常提示,就能較快定位到問題,並迅速解決。


對於這次的測試體驗,總體感覺還可以。


雖然我還是覺得,官方文件的描述,還可以再詳細和友好一些,但比之前那種遇到異常之後,看著報錯資訊一臉懵逼,不知道如何是好要強一些。


最後,想強調一點的就是,Spark structed streaming 就是一個實時的流式計算框架,我都已經在生產上用它好幾年了,請有些同學別再誤會它了,拜託。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027827/viewspace-3002071/,如需轉載,請註明出處,否則將追究法律責任。

相關文章