之前笔者在介绍 Flink 1.11 Hive Streaming 新特性时提到过,Flink SQL 的 FileSystem Connector 为了与 Flink-Hive 集成的大环境适配,做了很多改进,而其中最为明显的就是分区提交(partition commit)机制。

本文先通过源码简单过一下分区提交机制的两个要素——即触发(trigger)和策略(policy)的实现,然后用合并小文件的实例说一下自定义分区提交策略的方法。

PartitionCommitTrigger

在最新的 Flink SQL 中,FileSystem Connector 原生支持数据分区,并且写入时采用标准 Hive 分区格式,如下所示。

path└── datetime=2019-08-25    └── hour=11        ├── part-0.parquet        ├── part-1.parquet    └── hour=12        ├── part-0.parquet└── datetime=2019-08-26    └── hour=6        ├── part-0.parquet

那么,已经写入的分区数据何时才能对下游可见呢?这就涉及到如何触发分区提交的问题。根据官方文档,触发参数有以下两个:

  • sink.partition-commit.trigger:可选 process-time(根据处理时间触发)和 partition-time(根据从事件时间中提取的分区时间触发)。

  • sink.partition-commit.delay:分区提交的时延。如果 trigger 是 process-time,则以分区创建时的系统时间戳为准,经过此时延后提交;如果 trigger 是 partition-time,则以分区创建时本身携带的事件时间戳为准,当水印时间戳经过此时延后提交。

可见,process-time trigger 无法应对处理过程中出现的抖动,一旦数据迟到或者程序失败重启,数据就不能按照事件时间被归入正确的分区了。所以在实际应用中,我们几乎总是选用 partition-time trigger,并自己生成水印。当然我们也需要通过 partition.time-extractor.*一系列参数来指定抽取分区时间的规则(PartitionTimeExtractor),官方文档说得很清楚,不再赘述。

在源码中,PartitionCommitTrigger 的类图如下。

下面以分区时间触发的 PartitionTimeCommitTrigger 为例,简单看看它的思路。直接上该类的完整代码。

public class PartitionTimeCommitTigger implements PartitionCommitTrigger {private static final ListStateDescriptor<List<String>> PENDING_PARTITIONS_STATE_DESC =new ListStateDescriptor<>("pending-partitions",new ListSerializer<>(StringSerializer.INSTANCE));private static final ListStateDescriptor<Map<Long, Long>> WATERMARKS_STATE_DESC =new ListStateDescriptor<>("checkpoint-id-to-watermark",new MapSerializer<>(LongSerializer.INSTANCE, LongSerializer.INSTANCE));private final ListState<List<String>> pendingPartitionsState;private final Set<String> pendingPartitions;private final ListState<Map<Long, Long>> watermarksState;private final TreeMap<Long, Long> watermarks;private final PartitionTimeExtractor extractor;private final long commitDelay;private final List<String> partitionKeys;public PartitionTimeCommitTigger(boolean isRestored,OperatorStateStore stateStore,Configuration conf,ClassLoader cl,List<String> partitionKeys) throws Exception {this.pendingPartitionsState = stateStore.getListState(PENDING_PARTITIONS_STATE_DESC);this.pendingPartitions = new HashSet<>();if (isRestored) {pendingPartitions.addAll(pendingPartitionsState.get().iterator().next());}this.partitionKeys = partitionKeys;this.commitDelay = conf.get(SINK_PARTITION_COMMIT_DELAY).toMillis();this.extractor = PartitionTimeExtractor.create(cl,conf.get(PARTITION_TIME_EXTRACTOR_KIND),conf.get(PARTITION_TIME_EXTRACTOR_CLASS),conf.get(PARTITION_TIME_EXTRACTOR_TIMESTAMP_PATTERN));this.watermarksState = stateStore.getListState(WATERMARKS_STATE_DESC);this.watermarks = new TreeMap<>();if (isRestored) {watermarks.putAll(watermarksState.get().iterator().next());}}@Overridepublic void addPartition(String partition) {if (!StringUtils.isNullOrWhitespaceOnly(partition)) {this.pendingPartitions.add(partition);}}@Overridepublic List<String> committablePartitions(long checkpointId) {if (!watermarks.containsKey(checkpointId)) {throw new IllegalArgumentException(String.format("Checkpoint(%d) has not been snapshot. The watermark information is: %s.",checkpointId, watermarks));}long watermark = watermarks.get(checkpointId);watermarks.headMap(checkpointId, true).clear();List<String> needCommit = new ArrayList<>();Iterator<String> iter = pendingPartitions.iterator();while (iter.hasNext()) {String partition = iter.next();LocalDateTime partTime = extractor.extract(partitionKeys, extractPartitionValues(new Path(partition)));if (watermark > toMills(partTime) + commitDelay) {needCommit.add(partition);iter.remove();}}return needCommit;}@Overridepublic void snapshotState(long checkpointId, long watermark) throws Exception {pendingPartitionsState.clear();pendingPartitionsState.add(new ArrayList<>(pendingPartitions));watermarks.put(checkpointId, watermark);watermarksState.clear();watermarksState.add(new HashMap<>(watermarks));}@Overridepublic List<String> endInput() {ArrayList<String> partitions = new ArrayList<>(pendingPartitions);pendingPartitions.clear();return partitions;}
}

注意到该类中维护了两对必要的信息:

  • pendingPartitions/pendingPartitionsState:等待提交的分区以及对应的状态;

  • watermarks/watermarksState:<检查点 ID, 水印时间戳>的映射关系(用 TreeMap 存储以保证有序)以及对应的状态。

这也说明开启检查点是分区提交机制的前提。snapshotState() 方法用于将这些信息保存到状态中。这样在程序 failover 时,也能够保证分区数据的完整和正确。

那么 PartitionTimeCommitTigger 是如何知道该提交哪些分区的呢?来看 committablePartitions() 方法:

  1. 检查 checkpoint ID 是否合法;

  2. 取出当前 checkpoint ID 对应的水印,并调用 TreeMap的headMap() 和 clear() 方法删掉早于当前 checkpoint ID 的水印数据(没用了);

  3. 遍历等待提交的分区,调用之前定义的 PartitionTimeExtractor(比如${year}-${month}-${day} ${hour}:00:00)抽取分区时间。如果水印时间已经超过了分区时间加上上述 sink.partition-commit.delay 参数,说明可以提交,并返回它们。

PartitionCommitTrigger 的逻辑会在负责真正提交分区的 StreamingFileCommitter 组件中用到(注意 StreamingFileCommitter 的并行度固定为 1,之前有人问过这件事)。StreamingFileCommitter 和 StreamingFileWriter(即 SQL 版 StreamingFileSink)的细节相对比较复杂,本文不表,之后会详细说明。

PartitionCommitPolicy

PartitionCommitTrigger 解决了分区何时对下游可见的问题,而 PartitionCommitPolicy 解决的是对下游可见的标志问题。根据官方文档,我们可以通过 sink.partition-commit.policy.kind 参数进行配置,一共有三种提交策略(可以组合使用):

  • metastore:向 Hive Metastore 更新分区信息(仅在使用 HiveCatalog 时有效);

  • success-file:向分区目录下写一个表示成功的文件,文件名可以通过 sink.partition-commit.success-file.name 参数自定义,默认为_SUCCESS;

  • custom:自定义的提交策略,需要通过 sink.partition-commit.policy.class 参数来指定策略的类名。

PartitionCommitPolicy 的内部实现就简单多了,类图如下。策略的具体逻辑通过覆写 commit() 方法实现。

两个默认实现 MetastoreCommitPolicy 和 SuccessFileCommitPolicy 如下,都非常容易理解。

public class MetastoreCommitPolicy implements PartitionCommitPolicy {private static final Logger LOG = LoggerFactory.getLogger(MetastoreCommitPolicy.class);private TableMetaStore metaStore;public void setMetastore(TableMetaStore metaStore) {this.metaStore = metaStore;}@Overridepublic void commit(Context context) throws Exception {LinkedHashMap<String, String> partitionSpec = context.partitionSpec();metaStore.createOrAlterPartition(partitionSpec, context.partitionPath());LOG.info("Committed partition {} to metastore", partitionSpec);}
}
public class SuccessFileCommitPolicy implements PartitionCommitPolicy {private static final Logger LOG = LoggerFactory.getLogger(SuccessFileCommitPolicy.class);private final String fileName;private final FileSystem fileSystem;public SuccessFileCommitPolicy(String fileName, FileSystem fileSystem) {this.fileName = fileName;this.fileSystem = fileSystem;}@Overridepublic void commit(Context context) throws Exception {fileSystem.create(new Path(context.partitionPath(), fileName),FileSystem.WriteMode.OVERWRITE).close();LOG.info("Committed partition {} with success file", context.partitionSpec());}
}

Customize PartitionCommitPolicy

还记得之前做过的 Hive Streaming 实验么?

由上图可见,在写入比较频繁或者并行度比较大时,每个分区内都会出现很多细碎的小文件,这是我们不乐意看到的。下面尝试自定义 PartitionCommitPolicy,实现在分区提交时将它们顺便合并在一起(存储格式为 Parquet)。

Parquet 格式与普通的TextFile等行存储格式不同,它是自描述(自带 schema 和 metadata)的列存储,数据结构按照 Google Dremel 的标准格式来组织,与 Protobuf 相同。所以,我们应该先检测写入文件的 schema,再按照 schema 分别读取它们,并拼合在一起。

下面贴出合并分区内所有小文件的完整策略 ParquetFileMergingCommitPolicy。为了保证依赖不冲突,Parquet 相关的组件全部采用 Flink shade 过的版本。窃以为代码写得还算工整易懂,所以偷懒不写注释了。

package me.lmagics.flinkexp.hiveintegration.util;import org.apache.flink.hive.shaded.parquet.example.data.Group;
import org.apache.flink.hive.shaded.parquet.hadoop.ParquetFileReader;
import org.apache.flink.hive.shaded.parquet.hadoop.ParquetFileWriter.Mode;
import org.apache.flink.hive.shaded.parquet.hadoop.ParquetReader;
import org.apache.flink.hive.shaded.parquet.hadoop.ParquetWriter;
import org.apache.flink.hive.shaded.parquet.hadoop.example.ExampleParquetWriter;
import org.apache.flink.hive.shaded.parquet.hadoop.example.GroupReadSupport;
import org.apache.flink.hive.shaded.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.flink.hive.shaded.parquet.hadoop.metadata.ParquetMetadata;
import org.apache.flink.hive.shaded.parquet.hadoop.util.HadoopInputFile;
import org.apache.flink.hive.shaded.parquet.schema.MessageType;
import org.apache.flink.table.filesystem.PartitionCommitPolicy;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class ParquetFileMergingCommitPolicy implements PartitionCommitPolicy {private static final Logger LOGGER = LoggerFactory.getLogger(ParquetFileMergingCommitPolicy.class);@Overridepublic void commit(Context context) throws Exception {Configuration conf = new Configuration();FileSystem fs = FileSystem.get(conf);String partitionPath = context.partitionPath().getPath();List<Path> files = listAllFiles(fs, new Path(partitionPath), "part-");LOGGER.info("{} files in path {}", files.size(), partitionPath);MessageType schema = getParquetSchema(files, conf);if (schema == null) {return;}LOGGER.info("Fetched parquet schema: {}", schema.toString());Path result = merge(partitionPath, schema, files, fs);LOGGER.info("Files merged into {}", result.toString());}private List<Path> listAllFiles(FileSystem fs, Path dir, String prefix) throws IOException {List<Path> result = new ArrayList<>();RemoteIterator<LocatedFileStatus> dirIterator = fs.listFiles(dir, false);while (dirIterator.hasNext()) {LocatedFileStatus fileStatus = dirIterator.next();Path filePath = fileStatus.getPath();if (fileStatus.isFile() && filePath.getName().startsWith(prefix)) {result.add(filePath);}}return result;}private MessageType getParquetSchema(List<Path> files, Configuration conf) throws IOException {if (files.size() == 0) {return null;}HadoopInputFile inputFile = HadoopInputFile.fromPath(files.get(0), conf);ParquetFileReader reader = ParquetFileReader.open(inputFile);ParquetMetadata metadata = reader.getFooter();MessageType schema = metadata.getFileMetaData().getSchema();reader.close();return schema;}private Path merge(String partitionPath, MessageType schema, List<Path> files, FileSystem fs) throws IOException {Path mergeDest = new Path(partitionPath + "/result-" + System.currentTimeMillis() + ".parquet");ParquetWriter<Group> writer = ExampleParquetWriter.builder(mergeDest).withType(schema).withConf(fs.getConf()).withWriteMode(Mode.CREATE).withCompressionCodec(CompressionCodecName.SNAPPY).build();for (Path file : files) {ParquetReader<Group> reader = ParquetReader.builder(new GroupReadSupport(), file).withConf(fs.getConf()).build();Group data;while((data = reader.read()) != null) {writer.write(data);}reader.close();}writer.close();for (Path file : files) {fs.delete(file, false);}return mergeDest;}
}

别忘了修改分区提交策略相关的参数:

'sink.partition-commit.policy.kind' = 'metastore,success-file,custom', 'sink.partition-commit.policy.class' = 'me.lmagics.flinkexp.hiveintegration.util.ParquetFileMergingCommitPolicy'

重新跑一遍之前的 Hive Streaming 程序,观察日志输出:

20-08-04 22:15:00 INFO  me.lmagics.flinkexp.hiveintegration.util.ParquetFileMergingCommitPolicy       - 14 files in path /user/hive/warehouse/hive_tmp.db/analytics_access_log_hive/ts_date=2020-08-04/ts_hour=22/ts_minute=13
// 如果看官熟悉Protobuf的话,可以发现这里的schema风格是完全一致的20-08-04 22:15:00 INFO  me.lmagics.flinkexp.hiveintegration.util.ParquetFileMergingCommitPolicy       - Fetched parquet schema: message hive_schema {  optional int64 ts;  optional int64 user_id;  optional binary event_type (UTF8);  optional binary from_type (UTF8);  optional binary column_type (UTF8);  optional int64 site_id;  optional int64 groupon_id;  optional int64 partner_id;  optional int64 merchandise_id;}
20-08-04 22:15:04 INFO  me.lmagics.flinkexp.hiveintegration.util.ParquetFileMergingCommitPolicy       - Files merged into /user/hive/warehouse/hive_tmp.db/analytics_access_log_hive/ts_date=2020-08-04/ts_hour=22/ts_minute=13/result-1596550500950.parquet

最后来验证一下,合并成功。

以上。感兴趣的同学也可以动手测试~

原文链接:

https://www.jianshu.com/p/fb7d29abfa14


  福利来了  

Apache Flink 极客挑战赛

万众瞩目的第二届 Apache Flink 极客挑战赛来啦!本次大赛全面升级,重量级助阵嘉宾专业指导,强大的资源配置供你发挥创意,还有 30w 丰厚奖金等你带走~聚焦  Flink 与 AI 技术的应用实践,挑战疫情防控的世界级难题,你准备好了么?

(点击图片可了解更多大赛信息)

戳我报名!

Flink SQL FileSystem Connector 分区提交与自定义小文件合并策略 ​相关推荐

  1. Flink从入门到精通100篇(十五)-Flink SQL FileSystem Connector 分区提交与自定义小文件合并策略 ​

    前言 本文先通过源码简单过一下分区提交机制的两个要素--即触发(trigger)和策略(policy)的实现,然后用合并小文件的实例说一下自定义分区提交策略的方法. PartitionCommitTr ...

  2. 大数据教程(10.6)自定义inputFormat(小文件合并)

    2019独角兽企业重金招聘Python工程师标准>>> 上一篇文章分析了运营商流量日志解析增强的实现,至此,mapreduce的组件中除了inputFormat全都自定义写过了!博主 ...

  3. Flink SQL Print Connector

    Flink 版本:1.13 1. 简介 Print Connector 可以将每一行数据输出到标准输出流以及标准错误流中.它是专门为如下场景设计的: 在流式作业中进行简单测试. 在生产环境进行调试. ...

  4. flink sql设置并行度_Flink集成Hivestream模式用例

    01 背景 基于前面的文章 Flink集成hive bath模式用例 knowfarhhy,公众号:大数据摘文Flink 集成Hive ,我们继续介绍stream模式下的用例. 02 流模式读取Hiv ...

  5. tfs 文件系统部署_使用SQL Server数据工具和使用自定义工作流文件的TFS部署到多个数据库

    tfs 文件系统部署 In the previous blog post : Deployment to several databases using SQL Server Data Tools a ...

  6. Hive SQL 迁移 Flink SQL 在快手的实践

    摘要:本文整理自快手数据架构工程师张芒,阿里云工程师刘大龙,在 Flink Forward Asia 2022 生产实践专场的分享.本篇内容主要分为四个部分: Flink 流批一体引擎 Flink B ...

  7. Flink SQL 在网易云音乐的产品化实践

    简介:云音乐的性能优化.运维完善实战经验分享. 摘要:本文由网易云音乐数据智能部资深数据平台开发工程师蒋文伟分享,主要介绍 Flink SQL 在云音乐的产品化实践.分享内容如下: 简介 产品功能 性 ...

  8. spark sql合并小文件_Spark SQL小文件问题在OPPO的解决方案

    Spark SQL小文件是指文件大小显著小于hdfs block块大小的的文件.过于繁多的小文件会给HDFS带来很严重的性能瓶颈,对任务的稳定和集群的维护会带来极大的挑战. 一般来说,通过Hive调度 ...

  9. Hive Distribute by 应用之动态分区小文件过多问题优化

    目录 0 问题现象及原因分析 1 问题解决 解决办法 2 由以上问题引出的问题 3 思考 4 小结 0 问题现象及原因分析 现象: [Error 20004]: Fatal error occurre ...

  10. 解决Hive动态分区小文件过多问题

    一.问题描述 为了支撑相应的业务需求,本次生产环境通过Hive SQL来完成动态插入分区表数据的脚本开发.但是,动态分区的插入往往会伴随产生大量的小文件的发生.而小文件产生过多的影响主要分为以下两种情 ...

最新文章

  1. 2021 线性代数 第三章 习题课
  2. 解析postgresql 删除重复数据案例
  3. 根据数据库表字段删除所有相关信息(删库)
  4. 巨坑 之 pip install 和 conda install 的区别 以及 查看 和 修改 虚拟环境下运行路径
  5. mysql的数据类型——待写
  6. shell文本处理工具grep
  7. 主要版本发布后Java开发人员应使用的15种工具
  8. Eclipse : Unresolved inclusion
  9. Oracle_用户管理
  10. Pycharm导入python项目
  11. 简洁大气的网站微信QQ防红跳转代码
  12. 服务器mdf ldf文件,数据库mdf和ldf文件上传到服务器
  13. 通过Gazebo仿真学TurtleBot3(四)——简单的/cmd_vel控制
  14. ios系统有哪些好用的思维导图软件?
  15. MAYA XGen创建毛发时报错找不到过程“XgCreateDescription“的解决方法
  16. ECIF OCRM ACRM关系
  17. 搭建个人家用NAS网络存储服务器
  18. 利用PuTTY配置端口映射,实现外网对服务器的访问
  19. 用classwizard生成某个基类的继承类
  20. html5 3d在线网页,HTML5网页动画 3D旋转展示

热门文章

  1. jQuery特效:实现瀑布流
  2. HTML:让img标签和input标签水平对齐一样高
  3. Java JDK中的跳表实现
  4. 论文笔记_S2D.41_2017-ICCV-使用深度估计与深度卷积神经场,进行单目视觉里程计的尺度恢复
  5. 关于JSF Converter转换器的知识点
  6. SQL Unicode
  7. 【linux】centos6.9通过virtualenv安装python3.5
  8. win10升级后ctrl+shift+f失效了(zend studio)问题解决
  9. 利用Dojo和JSON建立无限级AJAX动态加载的功能模块树
  10. 3D物理引擎JiglibFlash