- 1:
Fixed Bucket mode
:属于固定Bucket数量模式,也就是需要我们手工指定Bucket的数量。我们在建表时默认使用的就是这种模式,Bucket参数的值默认为1。我们只需要给Bucket设置一个大于0的数值即可。但是需要注意:Bucket数量过大会导致小文件过多,影响读取性能;Bucket数量过小会影响写入性能。一般情况下,每个Bucket中的数据量推荐为1G左右。 - 2:
Dynamic Bucket mode
:属于动态Bucket数量模式,也就是说Bucket的数量是动态变化的。此时我们需要在建表时指定'bucket' = '-1'
,此时会由Paimon动态维护索引,将每个Bucket中的数据条数控制在2000000(2百万)以下,这个数值是由dynamic-bucket.target-row-num
这个参数控制的。但是需要注意,目前这种模式属于实验性质,暂时不建议在生产环境下使用。
下面我们通过一个案例来具体感受一下Bucket。
创建package:tech.xuwei.paimon.bucket
创建object:BucketDemo
代码如下:
package tech.xuwei.paimon.bucket
import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
/**
- 验证Bucket特性
- Created by xuwei
*/
object BucketDemo {
def main(args: Array[String]): Unit = {
//创建执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
val tEnv = StreamTableEnvironment.create(env)
//创建Paimon类型的Catalog
tEnv.executeSql(
“”"
|CREATE CATALOG paimon_catalog WITH (
| ‘type’=‘paimon’,
| ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
|)
|“”".stripMargin)
tEnv.executeSql(“USE CATALOG paimon_catalog”)
//创建Paimon表
tEnv.executeSql(
“”"
|CREATE TABLE IF NOT EXISTS bucket_test (
| word STRING,
| cnt BIGINT,
| PRIMARY KEY (word) NOT ENFORCED
|)WITH(
| ‘bucket’ = ‘2’ – 手工指定bucket的值,默认为1
|)
|“”".stripMargin)
//查看最完整的建表语句
tEnv.executeSql(“SHOW CREATE TABLE bucket_test”).print()
//向表中添加数据
tEnv.executeSql(
“”"
|INSERT INTO bucket_test(word,cnt)
|VALUES(‘a’,1) , (‘b’,2) , (‘c’,1) , (‘d’,3)
|“”".stripMargin)
}
}
执行代码,可以看到输出的完整建表语句信息:
CREATE TABLE paimon_catalog
.default
.bucket_test
(
word
VARCHAR(2147483647) NOT NULL,
cnt
BIGINT,
CONSTRAINT 53dc473d-3e56-4e13-ac8b-a4f3c9abb72b
PRIMARY KEY (word
) NOT ENFORCED
) WITH (
‘bucket’ = ‘2’,
‘path’ = ‘hdfs://bigdata01:9000/paimon/default.db/bucket_test’
)
接下来我们到hdfs中查看一个这个表的Bucket信息:
[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/bucket_test
Found 5 items
drwxr-xr-x - yehua supergroup 0 2028-11-29 17:54 /paimon/default.db/bucket_test/bucket-0
drwxr-xr-x - yehua supergroup 0 2028-11-29 17:54 /paimon/default.db/bucket_test/bucket-1
drwxr-xr-x - yehua supergroup 0 2028-11-29 17:54 /paimon/default.db/bucket_test/manifest
drwxr-xr-x - yehua supergroup 0 2028-11-29 17:54 /paimon/default.db/bucket_test/schema
drwxr-xr-x - yehua supergroup 0 2028-11-29 17:54 /paimon/default.db/bucket_test/snapshot
此时可以看到,这个表中包含2个bucket目录,这两个bucket目录中存储的就是这个表中的所有数据。
bucket目录内部都是一些data数据文件,里面就是真实的数据内容了:
[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/bucket_test/bucket-0
Found 1 items
-rw-r–r-- 3 yehua supergroup 545 2028-11-29 17:54 /paimon/default.db/bucket_test/bucket-0/data-8475e141-1725-489f-bd09-13606fd2302f-0.orc
咱们之前创建的表没有手工指定bucket,那么bucket默认为1,可以到表wc_sink_sql里面看一下,这个表里面存储了多条数据,但是只有1个bucket:
[root@bigdata04 ~]# hdfs dfs -ls /paimon/default.db/wc_sink_sql
Found 4 items
drwxr-xr-x - yehua supergroup 0 2028-11-29 11:53 /paimon/default.db/wc_sink_sql/bucket-0
drwxr-xr-x - yehua supergroup 0 2028-11-29 11:53 /paimon/default.db/wc_sink_sql/manifest
drwxr-xr-x - yehua supergroup 0 2028-11-29 11:44 /paimon/default.db/wc_sink_sql/schema
drwxr-xr-x - yehua supergroup 0 2028-11-29 11:53 /paimon/default.db/wc_sink_sql/snapshot
所以在实际工作中,设置合适的bucket数量就是非常重要的了。
但是在不同的时期,bucket的数量大概率是需要调整的,因为合适的bucket数量不可能是一成不变的,随着数据量的增加,bucket的数量也需要增大。
官方支持后期手工调整表中的bucket数量,但是想要对bucket中的数据进行重新分布,则只能通过离线流程来完成,也就是说需要跑一个离线任务来重新对bucket中的数据进行分布。
3.2.1.2 Changelog Producers
Changelog Producers:可以翻译为变更日志生产者。
Paimon表中存储数据的时候,除了存储数据本身,还可以选择存储数据的变更日志,也就是Changelog。
Changelog 主要应用在流读场景,在构建实时数据仓库的过程中,我们需要通过流读取上游的数据写入到下游,完成数仓各层之间的数据同步,让整个数仓的数据实时地流动起来。
如果上游数据来源于MySQL的 Binlog 日志,这样是可以直接提供完整的 Changelog 以供流来读取的。
但是针对湖仓一体架构,数仓分层是在Paimon里面实现的,数据会以表格的形式存储在文件系统中。
如果下游的Flink任务要通过流读取Paimon表中的数据,则需要Paimon的存储系统帮助生成 Changelog,以便下游流读。
此时就需要我们在建表时指定参数changelog-producer
来决定在何时以何种方式生成Changelog。
如果不指定Changelog Producer则不会向Paimon表中写入数据的时候生成 Changelog,那么下游任务需要在流读时生成一个Changelog Normalize物化节点来产生Changelog。
这种方式的成本相对比较高,并且官方也不建议这样使用,因为下游任务会在状态中维护一份全量的数据,也就是说每条数据都需要保存在状态中便于任务在执行时生成Changelog。
可能大家在这会有一个疑问,为什么一定需要Changelog呢?
因为通过Changelog可以记录数据的中间变化,针对某些计算逻辑,我们需要知道数据之前的历史值是什么,这样才能得到正确的结果。
例如:我们接收到的数据中包含了相同主键的多条 INSERT 数据,这样会导致下游的流聚合任务有问题,因为相同主键的多条数据应该被认为是更新,而不是重复累加计算。
Paimon 支持的 Changelog Produers
主要包括这几种:None、Input、Lookup和Full Compaction
。
下面我们来详细分析一下:
(1)None
如果不指定changelog-producer
,默认就是 none
,此时存储数据的时候不会存储数据的Changelog,后期读取数据时会动态生成Changelog,成本较高,不建议使用。
看这个图,此时这个数据源可以是任意类型的数据源,假设数据源依次产生了+I,+U,-D
类型的数据,其实这里面缺少了-U
类型的数据,我们通过Paimon 的SinkWriter组件将这些数据写入到了Paimon表中。
注意:此时这个Paimon表中配置的changelog-producer
参数的值为none
。
此时在向Paimon表中写入数据的时候,这个表只会存储数据本身,不会存储数据的Changelog。
当我们再通过一个任务从这个Paimon表中读取数据的时候,这个任务只能读取到+I、+U和-D
类型的数据,但是这个任务会产生一个Changelog Normalize
物化节点来自己生成数据的Changelog,但是这个操作是非常昂贵的,因为它需要在状态中维护数据的所有历史变化情况来生成数据的Changelog。最终是可以获取到完整的+I、-U、+U、-D
类型的数据的。
下面我们来通过一个案例具体演示一下建表语句中指定changelog-producer=none
时的效果。
创建package:tech.xuwei.paimon.changelogproducer.none
创建object:FlinkDataStreamWriteToPaimonForNone
这个Object负责向Paimon表中模拟写入数据。
代码如下:
package tech.xuwei.paimon.changelogproducer.none
import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.typeinfo.Types
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.{DataTypes, Schema}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.types.{Row, RowKind}
/**
- 使用Flink DataStream API向Paimon表中写入数据
- Created by xuwei
*/
object FlinkDataStreamWriteToPaimonForNone {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
val tEnv = StreamTableEnvironment.create(env)
//手工构造一个Changelog DataStream 数据流
val dataStream = env.fromElements(
Row.ofKind(RowKind.INSERT, “jack”, Int.box(10))//+I
//Row.ofKind(RowKind.UPDATE_AFTER, “jack”, Int.box(11))//+U
//Row.ofKind(RowKind.DELETE, “jack”, Int.box(11))//-D
)(Types.ROW_NAMED(Array(“name”, “age”),Types.STRING,Types.INT))
//将DataStream转换为Table
val schema = Schema.newBuilder()
.column(“name”, DataTypes.STRING().notNull())//主键非空
.column(“age”, DataTypes.INT())
.primaryKey(“name”)//指定主键
.build()
val table = tEnv.fromChangelogStream(dataStream,schema,ChangelogMode.all())
//创建Paimon类型的Catalog
tEnv.executeSql(
“”"
|CREATE CATALOG paimon_catalog WITH (
| ‘type’=‘paimon’,
| ‘warehouse’=‘hdfs://bigdata01:9000/paimon’
|)
|“”".stripMargin)
tEnv.executeSql(“USE CATALOG paimon_catalog”)
//注册临时表
tEnv.createTemporaryView(“t1”,table)
//创建Paimon类型的表
tEnv.executeSql(
“”"
|-- 注意:这里的表名使用反引号进行转义,否则会导致SQL DDL语句解析失败。
|CREATE TABLE IF NOT EXISTS changelog_none
(
| name STRING,
| age INT,
| PRIMARY KEY (name) NOT ENFORCED
|) WITH (
| ‘changelog-producer’ = ‘none’ – 注意:值为none时这一行配置可以省略不写
|)
|“”".stripMargin)
//向Paimon表中写入数据
tEnv.executeSql(
“”"
|INSERT INTO changelog_none
|SELECT name,age FROM t1
|“”".stripMargin)
}
}
注意:在执行代码的时候通过修改env.fromElements(...)
中的注释来实现实时产生多种类型数据的效果。
接下来创建Object:FlinkDataStreamReadFromPaimonForNone
这个Object负责从Paimon表中实时读取数据。
注意:为了便于在本地观察Flink读取数据任务中自动生成的Changelog Normalize
物化节点,所以我们需要在代码中开启本地WebUI功能。
先在pom.xml
中引入相关的依赖:
代码如下:
package tech.xuwei.paimon.changelogproducer.none
import java.time.ZoneId
import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.configuration.{Configuration, RestOptions}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
/**
- 使用Flink DataStream API从Paimon表中读取数据
- Created by xuwei
*/
object FlinkDataStreamReadFromPaimonForNone {
def main(args: Array[String]): Unit = {
val conf = new Configuration()
//指定WebUI界面的访问端口,默认就是8081
conf.setString(RestOptions.BIND_PORT,“8081”)
//为了便于在本地通过页面观察任务执行情况,所以开启本地WebUI功能
val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf)
env.setRuntimeMode(RuntimeExecutionMode.STREAMING)
//禁用Chain,把多个算子拆分开单独执行,便于在开发和测试阶段观察,正式执行时不需要禁用Chain
env.disableOperatorChaining()
val tEnv = StreamTableEnvironment.create(env)
//创建Paimon类型的Catalog
tEnv.executeSql(
“”