Apache Hudi 从入门到放弃(2) —— MOR表的文件结构分析

写在开始

  • 本篇带大家分析一下Hudi中MOR表的文件结构
  • 刚开始看Hudi一周,有什么不对的地方欢迎大家指出

事前准备

建表

-- 先准备一张Hudi MOR表
CREATE TABLE hudi_test_dijie(
 id bigint,
 dt string,
 ts TIMESTAMP(3),
 PRIMARY KEY(id) NOT ENFORCED
)
PARTITIONED BY (`dt`)
WITH (
'connector'= 'hudi',
'path'= 'hdfs:///hudi/hudi_test_dijie',
'table.type'= 'MERGE_ON_READ',
'read.streaming.enabled'= 'true',  
-- 'read.streaming.start-commit'= '20210401134557',
'read.streaming.check-interval'= '4'
);
-- 再准备一张有数据的Kafka表
CREATE TABLE kafka_test (
  id bigint,
  dt string,
  ts as localtimestamp
) WITH (
  'connector' = 'kafka',
  'topic' = 'a_dijie_test',
  'properties.bootstrap.servers' = 'localhost:9092',
  'properties.group.id' = 'testGroup',
  'scan.startup.mode' = 'earliest-offset',
  'format' = 'json'
);

插入数据

-- 我的任务开了checkpoint,并且周期是一秒;写入Hudi请开启checkpoint
-- 我分别在15点、18点、19点执行了这段SQL,并且在19点执行的时候,使用了MiniBatch模式
-- set table.exec.mini-batch.enabled=true;
-- set table.exec.mini-batch.allow-latency=5s;
-- set table.exec.mini-batch.size=5000;
set table.dynamic-table-options.enabled=true;
insert into hudi_test_dijie select * from kafka_test /*+ options ('properties.group.id' = 'test_d') */;

数据下载

# 使用Hdfs 命令下载目录下所有数据
# 注意这里的目录用的是建Hudi表时指定的`path`,而非{DAWEHOUSE}/databases/table  
hdfs dfs -get /hudi/hudi_test_dijie

工具下载

以下适用于Mac电脑

# 用于查看avro文件
brew install avro-tools
# 用于查看parquet文件
brew install parquet-tools

非Mac电脑可以直接下载对应的Jar包,下载路径

  • avro:http://archive.apache.org/dist/avro/avro-1.9.2/java/avro-tools-1.9.2.jar
  • parquet:http://logservice-resource.oss-cn-shanghai.aliyuncs.com/tools/parquet-tools-1.6.0rc3-SNAPSHOT.jar?spm=5176.doc52798.2.7.H3s2kL&file=parquet-tools-1.6.0rc3-SNAPSHOT.jar

开始分析

先看看表下面都有什么文件

目录下有两个文件夹,分别是.hoodie2021-05-01

首先.hoodie目录对应着表的元数据信息,包括表的版本管理(Timeline)、归档目录(存放过时的instant也就是版本)、回滚记录(.rollback)文件等等;因为我们也没有手动触发Hudi自身的savepoint操作,没有savepoint文件,一会儿会简单说一下这个文件的产生和作用

2021-05-01对应着分区路径,因为我Kafka中dt字段的数据都是指定为2021-05-01,所以只有该天的分区,分区里面存放着Base File(*.parquet)和Log File(*.log.*)以及分区信息

接下来,我们分成表元数据表数据两部分来进行分析

表元数据

之前介绍Hudi的时候和大家说过,Hudi有个Timeline的概念,它包含了在不同Instant时间所有对表的操作,一个Instant由以下3种东西构成

  • Instant action :对表执行的操作类型
  • Instant time :Instant时间通常是一个时间戳(比如:20210501182722),该时间戳按操作开始时间的顺序单调增加
  • state :Instant的状态

Hudi保证在时间轴上执行的操作的原子性和基于Instant时间的时间轴一致性。

执行的关键操作包括

  • COMMITS :一次提交表示将一组记录原子写入到表中。

  • CLEANS :删除表中不再需要的旧文件版本的后台活动。

  • DELTA_COMMIT :增量提交是指将一批记录原子写入到MOR表中,其中部分或全部数据都将只写入到日志中。

  • COMPACTION :合并日志、小文件的后台操作。合并其实是Timeline上的特殊提交。

  • ROLLBACK :表示COMMITS/DELTA_COMMIT不成功并且进行回滚,删除在写入过程中产生的所有不完整的文件。

  • SAVEPOINT :由hudi-CLI手动触发,对Instant进行备份,当后续出现提交错误时,便可ROLLBACK至指定SAVEPOINT

任何给定的Instant都可以处于以下状态之一

  • REQUESTED :表示已调度但尚未开始的操作。

  • INFLIGHT :表示当前正在执行该操作。

  • COMPLETED :表示在Timeline上完成了该操作。

以上是我对官网首页文档的翻译,认真看完了这些,再来看.hoodie下的文件会更清晰

我们按照时间顺序,依次打开有文件内容的文件
首先我们看一下./20210501150108.deltacommit文件中的内容

{
  "partitionToWriteStats": {
    "2021-05-01": [
      {
        "fileId": "817c5634-926f-41e9-a4b1-5c52ae4ce81b",
        "path": "2021-05-01/.817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0",
        "prevCommit": "20210501150108",
        "numWrites": 150159,
        "numDeletes": 0,
        "numUpdateWrites": 72544,
        "numInserts": 77615,
        "totalWriteBytes": 18470596,
        "totalWriteErrors": 0,
        "tempPath": null,
        "partitionPath": "2021-05-01",
        "totalLogRecords": 0,
        "totalLogFilesCompacted": 0,
        "totalLogSizeCompacted": 0,
        "totalUpdatedRecordsCompacted": 0,
        "totalLogBlocks": 0,
        "totalCorruptLogBlock": 0,
        "totalRollbackBlocks": 0,
        "fileSizeInBytes": 18470596,
        "minEventTime": null,
        "maxEventTime": null,
        "logVersion": 1,
        "logOffset": 0,
        "baseFile": "",
        "logFiles": [
          ".817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
        ]
      }
    ]
  },
  "compacted": false,
  "extraMetadata": {
    "schema": "{\"type\":\"record\",\"name\":\"record\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"dt\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"ts\",\"type\":[\"null\",{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}],\"default\":null}]}"
  },
  "operationType": null,
  "totalScanTime": 0,
  "totalCompactedRecordsUpdated": 0,
  "totalLogFilesCompacted": 0,
  "totalLogFilesSize": 0,
  "minAndMaxEventTime": {
    "Optional.empty": {
      "val": null,
      "present": false
    }
  },
  "totalCreateTime": 0,
  "totalUpsertTime": 2616,
  "totalRecordsDeleted": 0,
  "totalLogRecordsCompacted": 0,
  "writePartitionPaths": [
    "2021-05-01"
  ],
  "fileIdAndRelativePaths": {
    "817c5634-926f-41e9-a4b1-5c52ae4ce81b": "2021-05-01/.817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
  }
}
  • partitionToWriteStats
    • 因为我只写入了一个分区,所以这里只有2021-05-01这一个对象,实际上,如果写入了多个分区,那么这里会有多个对象,每个对象的Key都是分区名
    • fileId:在每个分区内,文件被组织为File Group,由文件Id唯一标识。每个File Group包含多个File Slice,其中每个Slice包含在某个Commit或Compcation Instant时间生成的Base File(*.parquet)以及Log Files(*.log*),该文件包含自生成基本文件以来对基本文件的插入和更新。
    • path:对应着本次写入的文件路径,因为我们是MOR的表,所以写入的是日志文件
    • prevCommit:上一次的成功Commit,因为本次Commit是这个表的第一次Commit,所以指向的Instant是自己
    • 下面有一部分是统计信息,不详细说明
    • baseFile:基本文件,经过上一次Compaction后的文件,对于MOR表来说,每次读取的时候,通过将baseFilelogFiles合并,就会读取到实时的数据
    • logFiles:日志文件,MOR表写数据时,数据首先写进日志文件,之后会通过一次Compaction进行合并
  • compacted:是否合并
  • extraMetadata:元数据信息
  • writePartitionPaths:写入的分区路径
  • fileIdAndRelativePaths
    • 因为只写入了一个分区,所以只有一个File Id,对应的是本次写入的数据的相对路径

至此,一个deltacommit文件了解的基本上差不多了;接下来的几个deltacommit内容都差不多,而我们是通过Flink写入的Hudi表,又自动设置了经过5次deltacommit之后,就会合并文件,所以我们来看一次文件合并产生的Instant中的操作是什么样的

ll 20210501150617.*
-rw-r--r--@ 1 dijie  staff  1772  5  1 19:32 20210501150617.commit
-rw-r--r--@ 1 dijie  staff     0  5  1 19:32 20210501150617.compaction.inflight
-rw-r--r--@ 1 dijie  staff  1490  5  1 19:32 20210501150617.compaction.requested

有两个文件有数据有内容,我们按照Instant State顺序来看
先看20210501150617.compaction.requested文件

{
  "operations": {
    "array": [
      {
        "baseInstantTime": {
          "string": "20210501150108"
        },
        "deltaFilePaths": {
          "array": [
            ".817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
          ]
        },
        "dataFilePath": null,
        "fileId": {
          "string": "817c5634-926f-41e9-a4b1-5c52ae4ce81b"
        },
        "partitionPath": {
          "string": "2021-05-01"
        },
        "metrics": {
          "map": {
            "TOTAL_LOG_FILES": 1,
            "TOTAL_IO_READ_MB": 1461,
            "TOTAL_LOG_FILES_SIZE": 1532984788,
            "TOTAL_IO_WRITE_MB": 120,
            "TOTAL_IO_MB": 1581
          }
        },
        "bootstrapFilePath": null
      }
    ]
  },
  "extraMetadata": null,
  "version": {
    "int": 2
  }
}
  • baseInstantTime:记录了是基于哪一次的Instant
  • deltaFilePaths:Log File路径,在这次Compaction完成之后,对应的Log File也会被清理,因为已经被合并Compaction至Base File中
  • dataFilePath:Base File路径,因为这是第一次合并,尚未有Base File生成,所以还是Null
  • fileId:文件Id
  • partitionPath:分区路径
  • metrics:指标值,一眼就能看明白都是什么信息
  • bootstrapFilePath:引导文件,之后会详解什么是引导,这里就不展开说了,主要是做存量表迁移使用
  • extraMetadata:元数据
  • version:版本,这里为何是2我不太理解,之后在看Compaction的时候再去研究

再看20210501150617.commit文件

{
  "partitionToWriteStats": {
    "2021-05-01": [
      {
        "fileId": "817c5634-926f-41e9-a4b1-5c52ae4ce81b",
        "path": "2021-05-01/817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet",
        "prevCommit": "null",
        "numWrites": 84455,
        "numDeletes": 0,
        "numUpdateWrites": 0,
        "numInserts": 84455,
        "totalWriteBytes": 1096720,
        "totalWriteErrors": 0,
        "tempPath": null,
        "partitionPath": "2021-05-01",
        "totalLogRecords": 12277396,
        "totalLogFilesCompacted": 1,
        "totalLogSizeCompacted": 1532984788,
        "totalUpdatedRecordsCompacted": 84455,
        "totalLogBlocks": 123,
        "totalCorruptLogBlock": 0,
        "totalRollbackBlocks": 0,
        "fileSizeInBytes": 1096720,
        "minEventTime": null,
        "maxEventTime": null
      }
    ]
  },
  "compacted": true,
  "extraMetadata": {
    "schema": "{\"type\":\"record\",\"name\":\"record\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"dt\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"ts\",\"type\":[\"null\",{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}],\"default\":null}]}"
  },
  "operationType": "UNKNOWN",
  "totalScanTime": 32727,
  "totalCompactedRecordsUpdated": 84455,
  "totalLogFilesCompacted": 1,
  "totalLogFilesSize": 1532984788,
  "minAndMaxEventTime": {
    "Optional.empty": {
      "val": null,
      "present": false
    }
  },
  "totalCreateTime": 0,
  "totalUpsertTime": 0,
  "totalRecordsDeleted": 0,
  "totalLogRecordsCompacted": 12277396,
  "writePartitionPaths": [
    "2021-05-01"
  ],
  "fileIdAndRelativePaths": {
    "817c5634-926f-41e9-a4b1-5c52ae4ce81b": "2021-05-01/817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet"
  }
}

重复字段就不说了,我们来看点不一样的

  • partitionToWriteStats
    • path:因为我们是Compaction操作,所以路径已经变成了Base File而不是之前的Log File
    • prevCommit:变成Null,因为我们已经产生了Base File,之前的Commit其实可以看错无效Commit
  • compacted:本次是一次Compaction操作
  • fileIdAndRelativePaths:变成了本次Compaction之后的路径

在完成Compaction没多久之后,我取消了Insert任务,并在18点多点的时候又执行了代码,我们继续观察时间轴上的变化

-rw-r--r--@  1 dijie  staff  1772  5  1 19:32 20210501150617.commit
-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501150617.compaction.inflight
-rw-r--r--@  1 dijie  staff  1490  5  1 19:32 20210501150617.compaction.requested
-rw-r--r--@  1 dijie  staff  1962  5  1 19:32 20210501182405.deltacommit
-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501182405.deltacommit.inflight
-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501182405.deltacommit.requested
-rw-r--r--@  1 dijie  staff  1550  5  1 19:32 20210501182406.rollback
-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501182406.rollback.inflight

时间轴一下子从20210501150617跳到了20210501182405,并且在20210501182406发生了一次rollback

时间轴跳跃能够理解,为什么会发生一次回滚呢?我们先来看文件内容
因为20210501182405.deltacommit的内容变化不大,所以我们直接来看20210501182406.rollback这个文件

avro-tools tojson 20210501182406.rollback
{
  "startRollbackTime": "20210501182406",
  "timeTakenInMillis": 25,
  "totalFilesDeleted": 0,
  "commitsRollback": [
    "20210501150618"
  ],
  "partitionMetadata": {
    "2021-05-01": {
      "partitionPath": "2021-05-01",
      "successDeleteFiles": [],
      "failedDeleteFiles": [],
      "rollbackLogFiles": {
        "map": {}
      },
      "writtenLogFiles": {
        "map": {}
      }
    }
  },
  "version": {
    "int": 1
  },
  "instantsRollback": [
    {
      "commitTime": "20210501150618",
      "action": "deltacommit"
    }
  ]
}
  • startRollbackTime:进行Rollback的时间
  • timeTakenInMillis:花费时间
  • totalFilesDeleted:总共删除的文件,这里有些同学可能会有疑问,既然发生了Rollback,那么应该把当时Instant所产生的文件都删除,为何这里的删除文件个数是0呢?我们接着往下看
  • commitsRollback:回滚的Instant,你会发现20210501150618在文件路径下并不存在,这又是为什么?20210501150618这个时间点上,我应该刚好把任务关闭,所以在目录下20210501150618这个Instant并没有完成,所以有20210501150618.inflight20210501150618.requested两个文件,而没有20210501150618.deltacommit文件;而在我又把任务启动,并且触发了第一次Flink的Checkpoint的时候,发现了有未完成的deltacommit,所以进行了回滚操作,并且删除了20210501150618.inflight20210501150618.requested;而且,我们看到本次ROllback和20210501182405.deltacommit只差了1秒,这很明显是在Checkpoint时,发现了有未完成的Commit,所以进行了一次回滚;而我在上一次停止任务的原因是没有数据了,所以上一个字段中的总删除文件个数为0
  • partitionMetadata:分区元数据,一眼能看出描述的是什么,就不再赘述
  • version:不知道为啥是1,之后看看源码
  • instantsRollback:是个数组,所有当前被Rollback的Commit都会被记录在案
    • commitTime:Commit时间
    • action:Commit类型,MOR表是deltacommit,COW表/Compaction是commit

接着时间轴继续往下看,已经讲过的文件类型就不再看了,我们来看看这个

-rw-r--r--@  1 dijie  staff  1468  5  1 19:32 20210501192800.clean
-rw-r--r--@  1 dijie  staff  1450  5  1 19:32 20210501192800.clean.inflight
-rw-r--r--@  1 dijie  staff  1450  5  1 19:32 20210501192800.clean.requested

发现3个文件都有值,我们按照Instant的State,依次来看
先看.requested文件

avro-tools tojson 20210501192800.clean.requested
{
  "earliestInstantToRetain": {
    "org.apache.hudi.avro.model.HoodieActionInstant": {
      "timestamp": "20210501182405",
      "action": "deltacommit",
      "state": "COMPLETED"
    }
  },
  "policy": "KEEP_LATEST_COMMITS",
  "filesToBeDeletedPerPartition": {
    "map": {}
  },
  "version": {
    "int": 2
  },
  "filePathsToBeDeletedPerPartition": {
    "map": {
      "2021-05-01": [
        {
          "filePath": {
            "string": "hdfs://hacluster/hudi/hudi_test_dijie/2021-05-01/.817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
          },
          "isBootstrapBaseFile": {
            "boolean": false
          }
        }
      ]
    }
  }
}
  • earliestInstantToRetain:最早的需要保留的Instant,比这个Instant对应的时间点要小的Instant,都需要被清理;因为Flink Sink的默认清理保留Commit Instant个数为10,所以找到了20210501182405这个Instant
  • polucy:清理策略,保留最新的Commit,另一种是KEEP_LATEST_FILE_VERSIONS保留最新的File版本
  • filesToBeDeletedPerPartition:应该是旧的保留字段,现在已经被filePathsToBeDeletedPerPartition替代
  • filePathsToBeDeletedPerPartition:每个分区中要被删除的文件

再看.inflight文件,因为和.requested内容一致,就不再赘述
最后来看20210501192800.clean

{
  "startCleanTime": "20210501192800",
  "timeTakenInMillis": 21,
  "totalFilesDeleted": 1,
  "earliestCommitToRetain": "20210501182405",
  "partitionMetadata": {
    "2021-05-01": {
      "partitionPath": "2021-05-01",
      "policy": "KEEP_LATEST_COMMITS",
      "deletePathPatterns": [
        ".817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
      ],
      "successDeleteFiles": [
        ".817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150108.log.1_1-4-0"
      ],
      "failedDeleteFiles": []
    }
  },
  "version": {
    "int": 2
  },
  "bootstrapPartitionMetadata": {
    "map": {}
  }
}

文件内容比较简单,基本上见字识意

看到这里,基本上时间轴上的文件内容都被我们浏览过了一遍,我们会发现有一个Instant只有.inflight.requested,而且我也是在这个时间点关闭的任务,所以,等我将任务再次启动之后,可以预见的是,将会再次触发一次Rollback

-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501192902.deltacommit.inflight
-rw-r--r--@  1 dijie  staff     0  5  1 19:32 20210501192902.deltacommit.requested

看完了时间轴,我们再看一下元数据目录的另外几个文件夹
首先来看.aux下面的内容

tree -a
.
├── .bootstrap
│   ├── .fileids
│   └── .partitions
├── 20210501150617.compaction.requested
├── 20210501182821.compaction.requested
└── 20210501192800.compaction.requested

这3个.requested文件和我们时间轴上的文件是一一对应的,就不展开说了;.bootstrap下存放的是进行引导操作的时候的文件,引导操作是用来将已有的表转化为Hudi表的操作,因为我们并没有执行这个,所以下面也自然没有内容

再来看.temp下的内容

tree -a
.
├── 20210501150617
│   └── 2021-05-01
│       └── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet.marker.CREATE
├── 20210501182821
│   └── 2021-05-01
│       └── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_4-10-0_20210501182821.parquet.marker.MERGE
└── 20210501192800
    └── 2021-05-01
        ├── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_5-10-0_20210501192800.parquet.marker.MERGE
        └── b8561bdf-2434-4f3b-8f78-41c648762f68_0-10-0_20210501192800.parquet.marker.CREATE

这里记录的是每次Base File的一些操作,在我们程序进行中时,如果存在往Base File中追加的操作,那么在这个目录下,也会有*.APPEND文件的产生,现在看不到了是因为我们发生了MERGE的操作

archived目录,存放归档Instant的目录,当不断写入Hudi表时,Timeline上的Instant数量会持续增多,为减少Timeline的操作压力,会在Commit时对Instant进行归档,并将Timeline上对应的Instant删除。
因为我们的Instant个数尚未达到默认值30个,所以并没有产生对应的文件,大家可以自行尝试后自行分析

最后一个元数据文件hoodie.properties

#Properties saved on Sat May 01 15:01:08 CST 2021
#Sat May 01 15:01:08 CST 2021
hoodie.compaction.payload.class=org.apache.hudi.common.model.OverwriteWithLatestAvroPayload
hoodie.table.name=hudi_test_dijie
hoodie.archivelog.folder=archived
hoodie.table.type=MERGE_ON_READ
hoodie.table.version=1
hoodie.timeline.layout.version=1

记录表的类型、Compaction Class名称、归档目录、版本等信息;如果一个Hudi表没有这个文件,那么将无法读取表中内容,这个文件不在建表时被创建,而是当第一次往该表中写入数据,才会创建这个文件;所以如果一个表没有写入过数据,那么无法被读取

至此,Hudi表的所有元数据信息都被我们分析完毕

表数据

先看看都有哪些文件

tree -a
.
├── .817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501150617.log.1_1-4-0
├── .817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501182821.log.1_1-4-0
├── .817c5634-926f-41e9-a4b1-5c52ae4ce81b_20210501192800.log.1_1-4-0
├── .b8561bdf-2434-4f3b-8f78-41c648762f68_20210501192502.log.1_3-4-0
├── .b8561bdf-2434-4f3b-8f78-41c648762f68_20210501192800.log.1_3-4-0
├── .hoodie_partition_metadata
├── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_4-10-0_20210501182821.parquet
├── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_5-10-0_20210501192800.parquet
├── 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet
└── b8561bdf-2434-4f3b-8f78-41c648762f68_0-10-0_20210501192800.parquet

分为3种文件:Log File(.*log.*),分区元数据(.hoodie_partition_metadata),数据文件(.*parquet)

其中Log File是Hudi自己编码的Avro文件,无法用avro-tools进行查看,所以就不在分析了;可以告诉大家的是,并不是实时将数据写入Log File,Hudi先将数据缓存至内存,然后再批量构造成Block后再写入Log File,并且当Log File达到一定大小后,会自动写入新的Log File,有点像Log4j的日志滚动策略;另外,每个Block中包含以下内容

  • Magic Number

  • Block Size

  • Block Version

  • Block Type

  • Block Headers

  • Data Length

  • Data

  • Block Footers

  • Block Size

如果想更深入的了解Log File的写入过程,可以参考org.apache.hudi.common.table.log.HoodieLogFormatWriter这个类

接下来再看.hoodie_partition_metadata文件,顾名思义,存放的是分区元数据

cat .hoodie_partition_metadata
#partition metadata
#Sat May 01 15:01:28 CST 2021
commitTime=20210501150108
partitionDepth=1

比较简单,再让我们看最后的Base File;因为它是个parquet文件,所以我们先看meta再看schema最后看里面的数据

parquet-tools meta 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet > ttt.log
vim ttt.log

因为meta里面的内容太多了一屏幕展示不下,所以丢到了临时文件里面,这里也就不全部贴出来了,放不下

1 file:                   file:/Users/dijie/Downloads/hudi_test_dijie/2021-05-01/817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet
2 creator:                parquet-mr version 1.11.1 (build 765bd5cd7fdef2af1cecd0755000694b992bfadd)

这两行应该是parquet-tools输出的,无视即可,我们将下一行当做真正的第一行

extra:      org.apache.hudi.bloomfilter = /wAAAB4BACd9PmrJ0jr1fDm3LH4fx...

省略号是我自己加的,内容太多不贴出来了;可以看到,在该parquetmeta中,构建了一个Bloomfilter,记录了所有写入该文件的数据的key,当有数据想要写入时,会用来判断数据存在与否,当数据存在时,会读取实文件进行二次判断,以便修正Bloomfilter的误差。
我们再看一下剩下的内容

  4 extra:                  hoodie_min_record_key = 1
  5 extra:                  parquet.avro.schema = {"type":"record","name":"record","fields":[{"name":"_hoodie_commit_time","type":["null","string"],"doc":"","default":null},{"name":"_hoodie_commit_seqno","type":["null","string"],"doc":"",    "default":null},{"name":"_hoodie_record_key","type":["null","string"],"doc":"","default":null},{"name":"_hoodie_partition_path","type":["null","string"],"doc":"","default":null},{"name":"_hoodie_file_name","type":["null","string"],"do    c":"","default":null},{"name":"id","type":"long"},{"name":"dt","type":["null","string"],"default":null},{"name":"ts","type":["null",{"type":"long","logicalType":"timestamp-millis"}],"default":null}]}
  6 extra:                  writer.model.name = avro
  7 extra:                  hoodie_max_record_key = 9999
  8
  9 file schema:            record
 10 --------------------------------------------------------------------------------
 11 _hoodie_commit_time:    OPTIONAL BINARY L:STRING R:0 D:1
 12 _hoodie_commit_seqno:   OPTIONAL BINARY L:STRING R:0 D:1
 13 _hoodie_record_key:     OPTIONAL BINARY L:STRING R:0 D:1
 14 _hoodie_partition_path: OPTIONAL BINARY L:STRING R:0 D:1
 15 _hoodie_file_name:      OPTIONAL BINARY L:STRING R:0 D:1
 16 id:                     REQUIRED INT64 R:0 D:0
 17 dt:                     OPTIONAL BINARY L:STRING R:0 D:1
 18 ts:                     OPTIONAL INT64 L:TIMESTAMP(MILLIS,true) R:0 D:1
 19
 20 row group 1:            RC:84455 TS:3715500 OFFSET:4
 21 --------------------------------------------------------------------------------
 22 _hoodie_commit_time:     BINARY GZIP DO:0 FPO:4 SZ:329/219/0.67 VC:84455 ENC:RLE,BIT_PACKED,PLAIN_DICTIONARY ST:[min: 20210501150617, max: 20210501150617, num_nulls: 0]
 23 _hoodie_commit_seqno:    BINARY GZIP DO:0 FPO:333 SZ:219024/2184907/9.98 VC:84455 ENC:RLE,PLAIN,BIT_PACKED ST:[min: 20210501150617_6_1, max: 20210501150617_6_9999, num_nulls: 0]
 24 _hoodie_record_key:      BINARY GZIP DO:0 FPO:219357 SZ:192644/749171/3.89 VC:84455 ENC:RLE,PLAIN,BIT_PACKED ST:[min: 1, max: 9999, num_nulls: 0]
 25 _hoodie_partition_path:  BINARY GZIP DO:0 FPO:412001 SZ:324/214/0.66 VC:84455 ENC:RLE,BIT_PACKED,PLAIN_DICTIONARY ST:[min: 2021-05-01, max: 2021-05-01, num_nulls: 0]
 26 _hoodie_file_name:       BINARY GZIP DO:0 FPO:412325 SZ:485/347/0.72 VC:84455 ENC:RLE,BIT_PACKED,PLAIN_DICTIONARY ST:[min: 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet, max: 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6    -10-0_20210501150617.parquet, num_nulls: 0]
 27 id:                      INT64 GZIP DO:0 FPO:412810 SZ:166768/675782/4.05 VC:84455 ENC:PLAIN,BIT_PACKED ST:[min: 1, max: 84455, num_nulls: 0]
 28 dt:                      BINARY GZIP DO:0 FPO:579578 SZ:324/214/0.66 VC:84455 ENC:RLE,BIT_PACKED,PLAIN_DICTIONARY ST:[min: 2021-05-01, max: 2021-05-01, num_nulls: 0]
 29 ts:                      INT64 GZIP DO:0 FPO:579902 SZ:80767/104646/1.30 VC:84455 ENC:RLE,BIT_PACKED,PLAIN_DICTIONARY ST:[min: 2021-05-01T15:05:46.010+0000, max: 2021-05-01T15:05:51.844+0000, num_nulls: 0]

可以看到,meta还记录了该文件中的最小Key和最大Key,可以在Bloomfilter过滤之前就过滤掉数据

剩下的就是一些字段信息,字段格式,字段最大值最小值等等,它的schema信息可以说已经包含在了meta之中,所以说我们就直接跳过,来看最终的数据

parquet-tools head -n 2 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet
_hoodie_commit_time = 20210501150617
_hoodie_commit_seqno = 20210501150617_6_1
_hoodie_record_key = 3640
_hoodie_partition_path = 2021-05-01
_hoodie_file_name = 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet
id = 3640
dt = 2021-05-01
ts = 1619881551277

_hoodie_commit_time = 20210501150617
_hoodie_commit_seqno = 20210501150617_6_2
_hoodie_record_key = 3638
_hoodie_partition_path = 2021-05-01
_hoodie_file_name = 817c5634-926f-41e9-a4b1-5c52ae4ce81b_6-10-0_20210501150617.parquet
id = 3638
dt = 2021-05-01
ts = 1619881551295

内容比较简单,就是每条数据对应的字段的值,除此之外还有一些额外的信息,也比较好理解,就不再多说

好了,至此,我们可以说是把MOR表生成的文件都看了一遍;可能有些文件有所遗漏,比如Savepoint产生的文件,但是只有Hudi-cli才能进行这样的操作并生产对应的文件,我并没有尝试,所以也就没有无法去分析

写在最后

  • MOR的表,表元数据里面的信息量还是比较大的,干货比较多,希望大家认真去看
  • 下一次分享将会是COW表的文件分析,敬请期待
  • 有人可能不理解为什么要花这么大的篇幅来了解文件结构和文件中的内容,其实如果了解透彻它们,那么我们在进行Hudi的读写流程分析的时候,才能更好的理解这个框架本身的设计思想
  • 最后,希望大家一定要认真看这篇文章,内容超多;算上标题、贴出来的Hudi文件内容,接近2万5千字,算是很有诚意的一篇文章,我花了2天假期时间来写,希望大家不要辜负
  • 最后,点个赞呗

你可能感兴趣的:(Apache,Hudi,Apache,Hudi,数据湖,大数据,仓湖一体,数据仓库)