【chunjun-bugfix】【flink sql元数据解析原理】多任务同时输出到同一数据源(HDFS、HIVE)时出现数据丢失的问题处理 与flink sql元数据解析原理

一. 问题描述

如题:多个任务同时写出到同一个hdfs(或hive)数据源时,会出现丢数据的情况。

主要的原因是:

chunjun sink数据到某个源时,会先在这个源下创建临时文件夹,(且所有任务的文件夹名称都是.data),然后写数据到这个临时文件夹,数据写完之后,会将文件移出到目标文件夹,并将临时文件夹删除。
 
当有多个任务同时往一个源写数据时,可能会存在的情况就是,其中有一个任务先完成了,此时这个任务会将.data文件夹下的所有文件移动到目标目录,并删除.data文件夹,此时其他未完成的任务无法在.data目录下继续写数据,导致数据丢失。

相关社区pr:[hotfix-#1745][connector][hdfs][hive] Data loss occurs when multiple tasks write to the same hdfs (or hive) data source

 

二. 问题分析

解决问题的方式比较明确,就是让每个任务产生的临时文件夹名字都不一样,这样每个任务运行过程中,不会相互影响,数据也就不会丢失。

我们先看下chunjun在启动、数据消费、任务完成整个过程中,涉及到关于临时文件夹都有哪些操作。

 

1. flink-sql任务运行主要逻辑分析

1.1. jobmanager构建作业的行为

a. 构建作业

构建flink作业时,我们会定义作业的数据流处理逻辑,包括数据源和输出源。此时会定义连接器的元数据例如源schema、连接方式、消费策略、分区策略等。

 
b. 作业提交过程

在作业提交阶段,JobManager 负责解析作业的拓扑结构,包括数据源和数据汇的配置。
在解析拓扑结构时,JobManager会识别连接器元数据的配置信息,并确保连接器被正确初始化和配置。
 
连接器元数据的解析通常在作业初始化阶段完成,以确保提交作业执行时,所有的连接器都已经正确配置和准备好接收和发送数据。

 
c. 对作业进行前置和后置操作

任务提交后,jobmanager分配任务给taskmanager之前,jobmanager也能够对作业输出源做些前置操作。如下类

/**
 * This interface may be implemented by {@link OutputFormat}s to have the master initialize them
 * globally.
 *
 * <p>For example, the {@link FileOutputFormat} implements this behavior for distributed file
 * systems and creates/deletes target directories if necessary.
 */
@Public
public interface InitializeOnMaster {

    /**
     * The method is invoked on the master (JobManager) before the distributed program execution
     * starts.
     *
     * @param parallelism The parallelism with which the format or functions will be run.
     * @throws IOException The initialization may throw exceptions, which may cause the job to
     *     abort.
     */
    void initializeGlobal(int parallelism) throws IOException;
}

此方法执行于JobManager分发任务给taskmanager之前,通常可以对输出源进行例如:创建或删除文件夹的操作。
 

对于所有taskmanager任务运行完成,jobmanager可以对任务进行一些自定义操作。

/**
 * This interface may be implemented by {@link OutputFormat}s to have the master finalize them
 * globally.
 */
@Public
public interface FinalizeOnMaster {

    /**
     * The method is invoked on the master (JobManager) after all (parallel) instances of an
     * OutputFormat finished.
     *
     * @param parallelism The parallelism with which the format or functions was run.
     * @throws IOException The finalization may throw exceptions, which may cause the job to abort.
     */
    void finalizeGlobal(int parallelism) throws IOException;
}

 

1.2. taskmanager执行作业行为

JobManager 在作业提交后负责生成作业的执行计划,并将任务分配给 TaskManagers。一旦任务被分配,TaskManagers 就会执行这些任务,实际处理数据的操作发生在 TaskManagers 上。

1. 连接器实例的创建

JobManager 分配作业(包含连接器信息)给 TaskManager 时,TaskManager
负责实例化连接器。连接器通常是在 TaskManager 的上下文中创建的,以确保连接器可以访问 TaskManager 所在的资源和环境。

 
2. 连接器初始化相关操作

a. 与外部系统建立连接。这可能涉及到网络连接、身份验证和其他与外部系统通信相关的操作。
b. TaskManager会分配适当的资源给连接器实例,包括内存、CPU 等,比如创建线程池等。
c. 状态初始化:某些连接器可能需要维护一些状态信息,例如读取数据的偏移量、写入数据的位置等。这些状态信息通常在连接器初始化时被设置,并在连接器的生命周期内被维护和更新。
d. 错误处理和恢复:进行错误处理和恢复机制的设置,以确保作业的稳定性和可靠性。

 
3. 消费处理数据
 
 

2. chunjun中hdfs连接器运行时关于.data文件夹的行为

知道了flink运行过程中作业的一些行为,我们接下来看hdfs连接器运行时的一些消费行为:

2.1. jobmanager预操作

jobmanager发送执行计划给taskmanager之前会对文件夹进行预操作:

  1. 初始化变量:输出路径、临时目录
  2. 删除临时文件夹(追加模式)或删除输出路径下文件夹(覆盖模式)
  3. 检查输出文件夹:目标文件夹的维护(hive分区目录的创建等)
BaseFileOutputFormat.class

    @Override
    public void initializeGlobal(int parallelism) {
        initVariableFields();
        if (WriteMode.OVERWRITE.name().equalsIgnoreCase(baseFileConfig.getWriteMode())
                && StringUtils.isBlank(baseFileConfig.getSavePointPath())) {
            // not delete the data directory when restoring from checkpoint
            deleteDataDir();
        } else {
            deleteTmpDataDir();
        }
        checkOutputDir();
    }

    protected void initVariableFields() {
        // The file name here is actually the partition name
        if (StringUtils.isNotBlank(baseFileConfig.getFileName())) {
            outputFilePath =
                    baseFileConfig.getPath() + File.separatorChar + baseFileConfig.getFileName();
        } else {
            outputFilePath = baseFileConfig.getPath();
        }
        tmpPath =
                outputFilePath
                        + File.separatorChar
                        + TMP_DIR_NAME
        nextNumForCheckDataSize = baseFileConfig.getNextCheckRows();
        openSource();
    }

 

2.2. taskmanager操作

BaseRichOutputFormat.class
open: 初始化taskmanager指标参数,初始化hdfs相关参数消费,打开fs等;
writeRecord: 写数据,并同步指标到metric;
close:关闭资源等;

任务执行过程中会将数据写到.data文件夹中,直到此taskmanager任务完成数据都在.data文件中。

 

2.3. jobmanager后置操作

当所有的taskmanager任务都执行完成后,jobmanager会将临时文件加的数据移动到输出目录中。

BaseFileOutputFormat
    @Override
    public void finalizeGlobal(int parallelism) {
        initVariableFields();
        moveAllTmpDataFileToDir();
        super.finalizeGlobal(parallelism);
    }

 
至此关于临时文件夹在任务执行过程中所有行为都已完成。

 

2.3. 小结

上述分析我们知道了解了flink任务在提交到完成的一些细节,chunjun关于任务消费时临时文件夹的处理,以下简单描述一些关键细节:

  1. 连接器元数据解析发生在jobmanager解析任务拓扑结构的过程中

  2. jobmanager生成作业的执行计划后,分配作业给taskmanager,其中包括解析好的连接器元数据发送给taskmanager

  3. taskmanager拿着jobmanager发送的执行计划,初始化连接器实例,并开始运行。

  4. jobmanager在发送执行计划之前,会有一个initializeGlobal的操作,比如根据连接器元数据初始化一些参数,创建数据源临时文件夹,清理上一次失败任务的临时文件夹等

  5. jobmanager在所有taskmanager执行完成之后,会有一个finalizeGlobal的操作,比如将临时文件夹移到目标目录。

 

三. 解决方案

从第一个节 “问题描述” 我们知道想要解决多任务同时写到一个数据源时数据丢失的问题,不能将临时文件夹统一命名为.data,而是一个任务对应一个文件夹名称,这样临时文件夹就做到了任务级别的隔离。

那怎么使得每个任务生成不同的文件夹名称呢,这里通过连接器with参数传递给连接器,我们知道jobmanager会解析连接器元数据,之后再发送给taskmanager,所以在jobmanager的initializeGlobal、finalizeGlobal,taskmanager的数据写入都能拿到此值。

1. 具体代码思路

1.1. 新增with参数

BaseFileOptionspublic static final ConfigOption<String> JOB_IDENTIFIER =
            ConfigOptions.key("properties.job-identifier")
                    .stringType()
                    .defaultValue("")
                    .withDescription(
                            "To solve the problem of file loss caused by multiple tasks writing to the same output source at the same time");

新增连接器元数据参数,为了外界能够根据不同的任务传递不同的任务标识。
 

1.2. hdfs连接器参数接收

HdfsDynamicTableFactory

    private HdfsConfig getHdfsConfig(ReadableConfig config) {
        HdfsConfig hdfsConfig = new HdfsConfig();
...
        hdfsConfig.setJobIdentifier(config.get(BaseFileOptions.JOB_IDENTIFIER));
}


@EqualsAndHashCode(callSuper = true)
@Data
public class BaseFileConfig extends CommonConfig {


    private String jobIdentifier = "";
}

jobmanager在解析任务拓扑时会解析连接器参数,当jobmanager发送执行计划给taskmanager时,taskmanager就能拿到这些参数。

 

1.3. 临时文件夹重新命名

BaseFileOutputFormat.java

protected void initVariableFields() {
        tmpPath =
                outputFilePath
                        + File.separatorChar
                        + TMP_DIR_NAME
                        + baseFileConfig.getJobIdentifier();
        log.info("[initVariableFields] get tmpPath: {}", tmpPath);
}
  • jobmanager解析完连接器参数后,就可以初始化临时路径,接着可以创建临时文件夹,以便taskmanager使用。

  • 拿到jobmanager传递的参数之后taskmanager就在此临时文件夹进行数据写入,且当启动多个tm时每个tm拿到的名字都一致时,保证了数据都写到这个文件夹。

 

2. flink sql举例说明

CREATE TABLE source
(
    id int,
    col_boolean boolean,
    col_tinyint tinyint
) WITH (
      'connector' = 'stream-x'
      ,'number-of-rows' = '10000'
);

CREATE TABLE sink
(
    id int,
    col_boolean boolean
) WITH (
      'connector' = 'hdfs-x'
      ,'path' = 'hdfs://ns/user/hive/warehouse/tudou.db/type_txt'
      ,'file-name' = 'pt=1'
      ,'properties.hadoop.user.name' = 'root'
      ,'properties.dfs.ha.namenodes.ns' = 'nn1,nn2'
      ,'properties.fs.defaultFS' = 'hdfs://ns'
      ,'properties.dfs.namenode.rpc-address.ns.nn2' = 'ip:9000'
      ,'properties.dfs.client.failover.proxy.provider.ns' = 'org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider'
      ,'properties.dfs.namenode.rpc-address.ns.nn1' = 'ip:9000'
      ,'properties.dfs.nameservices' = 'ns'
      ,'properties.fs.hdfs.impl.disable.cache' = 'true'
      ,'properties.fs.hdfs.impl' = 'org.apache.hadoop.hdfs.DistributedFileSystem'
      ,'default-fs' = 'hdfs://ns'
      ,'field-delimiter' = ','
      ,'encoding' = 'utf-8'
      ,'max-file-size' = '10485760'
      ,'next-check-rows' = '20000'
      ,'write-mode' = 'overwrite'
      ,'file-type' = 'text'
      -- 为了处理多任务(HDFS、HIVE)同时输出到同一数据源时数据丢失的问题
      -- 每个任务指定唯一的字符标识,且不能变化:比如这个任务失败了,传递的还应该是这个字符,以便清理上次写入的数据
      ,'properties.job-identifier' = 'job_id_tmp'
);

insert into sink
select *
from source u;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值