paimon sink 源碼 之 paimon table 創建

在學習 paimon sink 的過程中本來只想快速梳理下 paimon 的 sink 時對 DataStream 操作的拓撲, 但是過程中發現 paimon 會有很多概念,并且這些概念都做了很好的抽象,一口吃不了大胖子,慢慢的邊啃邊理解吧。這篇記錄在學習過程中的 paimon table,包含 paimon table 的創建和 table 本身。

paimon table 創建

在 sql 中 table 的創建少不了 catalog, catalog 的創建又離不開 flink 的 CatalogFactory。

Paimon 對于 flink CatalogFactory 的實現

org.apache.flink.table.factories.CatalogFactory

Paimon 對于 flink CatalogFactory 實現類的不同點

  1. IDENTIFIER 不同
  • org.apache.paimon.flink.FlinkGenericCatalogFactory:IDENTIFIER = "paimon"
    CREATE CATALOG catalogName WITH ( 'type'='paimon', ....)
    
  • org.apache.paimon.flink.FlinkCatalogFactory: IDENTIFIER = "paimon-generic";
    CREATE CATALOG catalogName WITH ( 'type'='paimon-generic', ....)
    
  1. 核心方法 CatalogFactory#createCatalog 的實現不一樣
  • FlinkGenericCatalogFactory#createCatalog
    1. 創建 flink hive connector 的 HiveCatalogFactory
    2. 然后創建 flink hive connector 的 HiveCatalog
    3. new paimon 的 FlinkCatalog
    4. 返回 new FlinkGenericCatalog (paimonCatalog, hiveCatalog)
  • FlinkCatalogFactory#createCatalog 直接 new paimon 的 FlinkCatalog
  • 所以 FlinkGenericCatalogFactory 創建出來的是 FlinkGenericCatalog
    FlinkCatalogFactory 創建出來的是 FlinkCatalog
    FlinkGenericCatalog 和 FlinkCatalog 都實現了 flink Catalog, 那么他們的區別是什么呢?

Paimon 對于 Flink Catalog 的實現

org.apache.flink.table.catalog.Catalog

從剛剛 createCatalog 方法中可以看到他們區別是 FlinkGenericCatalog 不僅僅含有 paimon 的 FlinkCatalog 還包含 flink hive connector 的 HiveCatalog,從 FlinkGenericCatalog 的實現來看,很多操作都會同時操作兩個 catalog, 其中 HiveCatalog 是對 hive HMS 進行請求操作,FlinkCatalog 是對 paimon 進行操作,方法例舉如下。

public CatalogBaseTable getTable(ObjectPath tablePath) {
     try {
         return paimon.getTable(tablePath); //  paimon 的 FlinkCatalog
     } catch (TableNotExistException e) {
         return flink.getTable(tablePath);  // hive connector 的 HiveCatalog
     }
}

public void createTable(ObjectPath tablePath, CatalogBaseTable table, boolean ignoreIfExists) {
     String connector = table.getOptions().get(CONNECTOR.key());
     if (FlinkCatalogFactory.IDENTIFIER.equals(connector)) { //是 paimon 表
         paimon.createTable(tablePath, table, ignoreIfExists); // 就用 paimon 的 FlinkCatalog 進行操作
     } else {
         flink.createTable(tablePath, table, ignoreIfExists); // 否則就用  hive connector 的 HiveCatalog
     }
}

public List<String> listTables(String databaseName) {
     // flink list tables contains all paimon tables // 都包含?那他是怎么把 paimon 表同步到 HMS 的
     return flink.listTables(databaseName); // 為什么這里只用  hive connector 的 HiveCatalog 就行?
}

所以看起來 FlinkGenericCatalog 有如下特點

  1. 是可以自動識別是否為 paimon 表,優先用 paimon catalog 去嘗試。
  2. 這個 catalog 可以兼容普通的 hive 表和 paimon 表

對于 listTables 等一些操作為什么只要用 HiveCatalog 就行?
既然都包含,那么 paimon 的 FlinkCatalog 是怎么把表同步到 HMS 的?

Paimon 的 FlinkCatalog

上面得知 FlinkCatalog 是 flink Catalog 的一個實現

FlinkCatalog extends org.apache.flink.table.catalog.AbstractCatalog {
  private final org.apache.paimon.catalog.Catalog catalog;
  public List<String> listTables(String databaseName) {
      return catalog.listTables(databaseName);
    } 
 ... ...
}
  • 從上得知 FlinkCatalog 其實只是一個包裝 真正進行 Catalog 操作的還是 org.apache.paimon.catalog.Catalog 需要注意它的包名不是 flink 的 org.apache.flink.table.catalog.Catalog
    文中我有意的補充包路徑或者強調是 paimon 的什么什么或者是 flink 的什么什么就是想要做區分,在 Paimon 中好多類名看起來很容易誤以為是 flink 的類,其實不是。。。。
  • 所以要看 Paimon 的 org.apache.paimon.flink.FlinkCatalog 還得看 paimon 的 org.apache.paimon.catalog.Catalog

Paimon 的 Catalog

org.apache.paimon.catalog.Catalog

Paimon 的 Catalog 是如何創建的

  • 直接說結論吧還是通過 flink SPI 機制根據配置、IDENTIFIER 和 org.apache.paimon.catalog.CatalogFactory 實現類去先找到 factory 然后在 factory 進行創建 paimon org.apache.paimon.catalog.Catalog 的具體實現
  • 看一眼 org.apache.paimon.catalog.CatalogFactory 類圖


    org.apache.paimon.catalog.CatalogFactory
  • Paimon Catalog 創建示例
    // 創建 org.apache.paimon.catalog.FileSystemCatalog
    CREATE CATALOG my_catalog WITH (
        'type' = 'paimon',
        'warehouse' = 'hdfs:///path/to/warehouse'
    );
    
    // 創建 org.apache.paimon.hive.HiveCatalog
    CREATE CATALOG my_hive WITH (
    'type' = 'paimon',
    'metastore' = 'hive',
    -- 'uri' = 'thrift://<hive-metastore-host-name>:<port>', default use 'hive.metastore.uris' in HiveConf
    -- 'hive-conf-dir' = '...', this is recommended in the kerberos environment
    -- 'hadoop-conf-dir' = '...', this is recommended in the kerberos environment
    -- 'warehouse' = 'hdfs:///path/to/warehouse', default use 'hive.metastore.warehouse.dir' in HiveConf
    );
    
    // 創建 org.apache.paimon.jdbc.JdbcCatalog
    CREATE CATALOG my_jdbc WITH (
    'type' = 'paimon',
    'metastore' = 'jdbc',
    'uri' = 'jdbc:mysql://<host>:<port>/<databaseName>',
    'jdbc.user' = '...', 
    'jdbc.password' = '...', 
    'catalog-key'='jdbc',
    'warehouse' = 'hdfs:///path/to/warehouse'
    );
    
  • 可能大部分場景用的是 org.apache.paimon.hive.HiveCatalog 所以來看看這個的實現,剛好也可以解答 paimon 的 FlinkGenericCatalog 是怎么把表同步到 HMS 的,在 創建 FlinkGenericCatalog 時會創建 Paimon 的 org.apache.paimon.catalog.Catalog 會注入核心參數 'metastore'='hive', 使得 FlinkGenericCatalog 中的 Paimon Catalog 最終是 org.apache.paimon.hive.HiveCatalog
    options.set(CatalogOptions.METASTORE, "hive");
    org.apache.paimon.catalog.CatalogFactory.createCatalog(
                                CatalogContext.create(options, new FlinkFileIOLoader()), cl),
    

Paimon 的 org.apache.paimon.hive.HiveCatalog

  • HiveCatalog 維護了一個 hive client 會對表的變更進行同步
HiveCatalog extends AbstractCatalog implements Catalog{
      private final IMetaStoreClient client;  // hive client
      protected void alterTableImpl(Identifier identifier, List<SchemaChange> changes) {
        final SchemaManager schemaManager = schemaManager(identifier);
        // first commit changes to underlying files
        TableSchema schema = schemaManager.commitChanges(changes);
        try {
            // sync to hive hms 表變更同步到 hive
            Table table = client.getTable(identifier.getDatabaseName(), identifier.getObjectName());
            updateHmsTablePars(table, schema);
            updateHmsTable(table, identifier, schema);
            client.alter_table(
                    identifier.getDatabaseName(), identifier.getObjectName(), table, true);
        } catch (Exception te) {
            schemaManager.deleteSchema(schema.id());
            throw new RuntimeException(te);
        }
    }
}

到這里就說明了 paimon table 的創建前揍, 從 Flink 的 CatalogFactory 到 Flink 的 Catalog, 再到 Paimon 基于 Flink 的 Catalog, Paimon 基于 Flink 的 Catalog 是一個殼子實際是用的 Paimon 的 Catalog, Paimon 的 Catalog 又是通過 Paimon 的 CatalogFactory 創建而來。接下來看看主角 Paimon 的 Catalog 創建的 Paimon Table, Table 的創建是 org.apache.paimon.catalog.AbstractCatalog 實現的

org.apache.paimon.catalog.AbstractCatalog implements Catalog  {
  public Table getTable(Identifier identifier) throws TableNotExistException {
        if (isSystemDatabase(identifier.getDatabaseName())) { // 先忽略
        } else if (isSpecifiedSystemTable(identifier)) { //先忽略
        } else {
            return getDataTable(identifier);
        }
    }
}

private FileStoreTable getDataTable(Identifier identifier) throws TableNotExistException {
        TableSchema tableSchema = getDataTableSchema(identifier);
        return FileStoreTableFactory.create(
                fileIO,
                getDataTableLocation(identifier),
                tableSchema,
                new CatalogEnvironment(
                        Lock.factory(
                                lockFactory().orElse(null), lockContext().orElse(null), identifier),
                        metastoreClientFactory(identifier).orElse(null),
                        lineageMetaFactory));
    }

然后在 FileStoreTableFactory.create 方法中根據是否有主鍵會創建 AppendOnlyFileStoreTable 或者 PrimaryKeyFileStoreTable

    public static FileStoreTable create(
         FileIO fileIO, // 這又是個啥玩意?
         Path tablePath,
         TableSchema tableSchema,
         Options dynamicOptions,
         CatalogEnvironment catalogEnvironment) {
     FileStoreTable table =
             tableSchema.primaryKeys().isEmpty()
                     ? new AppendOnlyFileStoreTable(
                             fileIO, tablePath, tableSchema, catalogEnvironment)
                     : new PrimaryKeyFileStoreTable(
                             fileIO, tablePath, tableSchema, catalogEnvironment);
     return table.copy(dynamicOptions.toMap());
 }

到這里 Paimon Table 就已經創建完成了,Table 提供了表的讀取寫入和表操作的一些抽象,涉及面較多
簡單看看 Table 和寫入相關的一些 方法 混個眼熟,后面了解更多再補充

Paimon Table 之 FileStoreTable

org.apache.paimon.table.Table
  • 看到這個類圖一開始是崩潰了沒想到一個 Table 有這么多花樣
  • 慶幸的是在 paimon sink 中只需要關注 FileStoreTable 的3個實現類 AbstractFileStoreTable、 AppendOnlyFileStoreTable 和 PrimaryKeyFileStoreTable, 其中 AbstractFileStoreTable 是 其他兩個的父類


    org.apache.paimon.table.FileStoreTable
abstract class  AbstractFileStoreTable AbstractFileStoreTable implements FileStoreTable {
    protected final FileIO fileIO;
    @Override 
    public BucketMode bucketMode() { // 分桶模式很重要
        return store().bucketMode(); // store() 方法在子類實現
    }
  ... ...
}
  • 在創建 AbstractFileStoreTable 時需要傳入一個 FileIO FileIO 是個啥先混個眼熟,看類圖是和 數據存儲層交互的一個接口對不同的存儲有不同實現


    org.apache.paimon.fs.FileIO
  • store() 的實現

    • AppendOnlyFileStoreTable FileStore 為 AppendOnlyFileStore<InternalRow>
    • PrimaryKeyFileStoreTable FileStore 為 KeyValueFileStore<KeyValue>
    • FileStore 是數據讀寫的接口


      org.apache.paimon.FileStore
  • bucketMode 的實現

    • BucketMode 來自 FileStoreTable 的 FileStore,所以 PrimaryKeyFileStoreTable 的 BucketMode 是在 KeyValueFileStore 中定義的邏輯為:
      1. 先計算 crossPartitionUpdate
           public boolean crossPartitionUpdate() {
             if (primaryKeys.isEmpty() || partitionKeys.isEmpty()) {
                return false; //如果 primaryKeys 為空 或者  partitionKeys 返回  false
              }
             //如果 primaryKeys 包含所有的 partitionKeys 返回 false ; 如果 主鍵是 ABC, 分區字段是 A 則不支持分區變更,
             // 這個很好理解,因為如果分區變更了 那說明主鍵都變了變成新的記錄了
             // 那如果主鍵是 ABC 分區字段是 AD 呢?A 變了如何支持分區變更??
             return !primaryKeys.containsAll(partitionKeys);
          }
        
        • crossPartitionUpdate = true 的場景就是要有主鍵要有分區并且分區鍵不全是主鍵
        • 如果主鍵是 ABC 分區字段是 AD 呢?A 變了如何支持分區變更??
      2. 如果 bucket =-1 默認為 -1 則看 crossPartitionUpdate 如果可以 crossPartitionUpdate 則為 GLOBAL_DYNAMIC 否則為 DYNAMIC
        如果 bucket !=-1 則為 FIXED 并且此時 crossPartitionUpdate 一點要為 false 會校驗。 這意味著勁量不要去設置 bucket 數目?? 因為設置了 如果 crossPartitionUpdate 為 true 就會報錯,為啥要這樣設計??
           public BucketMode bucketMode() {
                if (options.bucket() == -1) { // 默認為 -1
                  return crossPartitionUpdate ? BucketMode.GLOBAL_DYNAMIC : BucketMode.DYNAMIC;
               } else {
                  checkArgument(!crossPartitionUpdate);
                 return BucketMode.FIXED;
             }
        }
        
  • 對于 AppendOnlyFileStoreTable 的 BucketMode 是在 AppendOnlyFileStore 中定義的邏輯為:options.bucket() == -1 ? BucketMode.UNAWARE : BucketMode.FIXED

  • 所以 AppendOnlyFileStoreTable 的 BucketMode 為 FIXED 或者 UNAWARE

  • 所以 PrimaryKeyFileStoreTable 的 BucketMode 為 DYNAMIC 或者 GLOBAL_DYNAMIC 或者 FIX

BucketMode 翻譯自官網

  • Bucket 是讀寫的最小存儲單元,每個Bucket目錄中包含一棵LSM樹

Fixed Bucket 指的是 BucketMode.FIXED

  • 配置一個大于0的bucket,采用Fixed Bucket模式,根據Math.abs(key_hashcode % numBuckets)記錄計算bucket。重新縮放存儲桶只能通過離線流程完成,請參閱重新縮放存儲桶。桶數過多會導致小文件過多,桶數過少會導致寫入性能較差
  • 根據 bucket 鍵將數據分發到相應的 Bucket 中(默認為主鍵),對于帶有分桶鍵的查詢可以進行桶的 data skipping

Dynamic Bucket 有兩種

  • 配置'bucket' = '-1'。以前寫入過的 key 會落入舊的 bucket,新的 key 會落入新的 bucket,bucket 和 key 的分布取決于數據到達的順序。 Paimon 維護一個索引來確定哪個鍵對應哪個桶。
  • 分桶鍵的查詢不支持桶的 data skipping
  • Paimon會自動擴大桶的數量。
    • Option1: 'dynamic-bucket.target-row-num':控制一個桶的目標行數。
    • Option2: 'dynamic-bucket.initial-buckets': 控制初始化bucket的數量。

動態Bucket僅支持單個寫入作業。請不要啟動多個作業來寫入同一分區(這可能會導致重復數據)。即使您啟用'write-only'并啟動專用的壓縮作業,它也不會起作用。
Dynamic bucket mode can not work with log system

Normal Dynamic Bucket Mode 指的是 BucketMode.DYNAMIC

  • 當您的更新不跨分區(沒有分區,或者主鍵包含所有分區字段)時,BucketMode.DYNAMIC 使用 HASH 索引來維護從鍵到桶的映射,它比固定桶模式需要更多的內存。
    性能:
  1. 一般來說,沒有性能損失,但會有一些額外的內存消耗,一個分區中的1 億個 條目多占用1 GB內存,不再活動的分區不占用內存。
  2. 對于更新率較低的表,建議使用此模式,以顯著提高性能。

Normal Dynamic Bucket Mode支持sort-compact以加快查詢速度。請參閱緊湊排序

Cross Partitions Upsert Dynamic Bucket Mode 指的是 BucketMode.GLOBAL_DYNAMIC

  • 當需要跨分區upsert(主鍵不包含所有分區字段)時,Dynamic Bucket 模式直接維護鍵到分區和桶的映射,使用本地磁盤,并在啟動流寫作業時通過讀取表中所有現有鍵來初始化索引。不同的合并引擎有不同的行為:
    • Deduplicate:刪除舊分區中的數據,并將新數據插入到新分區中。保證數據的唯一性
    • PartialUpdate & Aggregation:將新數據插入舊分區。
    • FirstRow:如果有舊值,則忽略新數據。
      性能:
    1. 對于數據量較大的表,性能會有明顯的損失。而且,初始化需要很長時間。
    2. 如果你的upsert不依賴太舊的數據,可以考慮配置索引TTL來減少索引和初始化時間:
      2.1 'cross-partition-upsert.index-ttl':rocksdb索引和初始化中的TTL,這樣可以避免維護太多索引而導致性能越來越差。但請注意,這也可能會導致數據重復。

FINAL

  • 講述了 Paimon Table 的創建過程。


    Paimon Table 的創建
  • Paimon Table 功能待補充。
  • 介紹了 BucketMode。 這個很重要不同的數據場景選擇合適的 mode, 不同的 mode 數據入 paimon 的 拓撲也不一樣
  • FileStore 功能待補充。
  • Paimon sink 拓撲 是在 DynamicTableSink 中定義的
  • 下篇會介紹 Paimon 的 DynamicTableSink
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容