文章目录
一. 问题描述
如题:多个任务同时写出到同一个hdfs(或hive)数据源时,会出现丢数据的情况。
主要的原因是:
chunjun sink数据到某个源时,会先在这个源下创建临时文件夹,(且所有任务的文件夹名称都是.data),然后写数据到这个临时文件夹,数据写完之后,会将文件移出到目标文件夹,并将临时文件夹删除。
当有多个任务同时往一个源写数据时,可能会存在的情况就是,其中有一个任务先完成了,此时这个任务会将.data文件夹下的所有文件移动到目标目录,并删除.data文件夹,此时其他未完成的任务无法在.data目录下继续写数据,导致数据丢失。
二. 问题分析
解决问题的方式比较明确,就是让每个任务产生的临时文件夹名字都不一样,这样每个任务运行过程中,不会相互影响,数据也就不会丢失。
我们先看下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之前会对文件夹进行预操作:
- 初始化变量:输出路径、临时目录
- 删除临时文件夹(追加模式)或删除输出路径下文件夹(覆盖模式)
- 检查输出文件夹:目标文件夹的维护(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关于任务消费时临时文件夹的处理,以下简单描述一些关键细节:
-
连接器元数据解析发生在jobmanager解析任务拓扑结构的过程中
-
jobmanager生成作业的执行计划后,分配作业给taskmanager,其中包括解析好的连接器元数据发送给taskmanager
-
taskmanager拿着jobmanager发送的执行计划,初始化连接器实例,并开始运行。
-
jobmanager在发送执行计划之前,会有一个initializeGlobal的操作,比如根据连接器元数据初始化一些参数,创建数据源临时文件夹,清理上一次失败任务的临时文件夹等
-
jobmanager在所有taskmanager执行完成之后,会有一个finalizeGlobal的操作,比如将临时文件夹移到目标目录。
三. 解决方案
从第一个节 “问题描述” 我们知道想要解决多任务同时写到一个数据源时数据丢失的问题,不能将临时文件夹统一命名为.data,而是一个任务对应一个文件夹名称,这样临时文件夹就做到了任务级别的隔离。
那怎么使得每个任务生成不同的文件夹名称呢,这里通过连接器with参数传递给连接器,我们知道jobmanager会解析连接器元数据,之后再发送给taskmanager,所以在jobmanager的initializeGlobal、finalizeGlobal,taskmanager的数据写入都能拿到此值。
1. 具体代码思路
1.1. 新增with参数
BaseFileOptions:
public 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;