干货BitSailConnector开发详解系列一Source
编者荐语
还不错的文章
以下文章来源于字节跳动数据平台,作者BitSail团队

字节跳动数据平台
字节跳动数据平台团队,赋能字节跳动各业务线,降低数据应用的门槛为始,以建立数据驱动的智能化企业,赋能各行业的数字化转型,创造更大的社会价值为终。对内支持字节绝大多数业务线,对外发布了火山引擎品牌下的数据智能产品,服务行业企业客户。


BitSail 是字节跳动自研的数据集成产品,支持多种异构数据源间的数据同步,并提供离线、实时、全量、增量场景下全域数据集成解决方案。
本系列聚焦 BitSail Connector 开发模块,为大家带来详细全面的开发方法与场景示例,本篇将主要介绍 Source 接口部分。

文 | 浩宇 来自字节跳动数据平台BitSail团队
持续关注,本开发详解将分为四篇呈现。
- 开发详解系列一:Source(本篇)
- 开发详解系列二:SourceSplitCoordinator
- 开发详解系列三:SourceReader
- 开发详解系列四:Sink、Writer


本文将主要介绍 Source 接口部分:
- Source:参与数据读取组件的生命周期管理,主要负责和框架的交互,构架作业,不参与作业真正的执行。
- SourceSplit:数据读取分片,大数据处理框架的核心目的就是将大规模的数据拆分成为多个合理的Split并行处理。
- State:作业状态快照,当开启checkpoint之后,会保存当前执行状态。
一、Source
数据读取组件的生命周期管理,主要负责和框架的交互,构架作业,它不参与作业真正的执行。
以 RocketMQSource 为例:Source方法需要实现 Source 和 ParallelismComputable 接口。

1.1 Source接口
java
public interface Source<T, SplitT extends SourceSplit, StateT extends Serializable> extends Serializable, TypeInfoConverterFactory {
/**
* Run in client side for source initialize;
*/
void configure(ExecutionEnviron execution, BitSailConfiguration readerConfiguration) throws IOException;
/**
* Indicate the Source type.
*/
Boundedness getSourceBoundedness();
/**
* Create Source Reader.
*/
SourceReader<T, SplitT> createReader(SourceReader.Context readerContext);
/**
* Create split coordinator.
*/
SourceSplitCoordinator<SplitT, StateT> createSplitCoordinator(SourceSplitCoordinator.Context<SplitT, StateT> coordinatorContext);
/**
* Get Split serializer for the framework, {@link SplitT} should implement from {@link Serializable}
*/
default BinarySerializer<SplitT> getSplitSerializer() {
return new SimpleBinarySerializer<>();
}
/**
* Get State serializer for the framework, {@link StateT} should implement from {@link Serializable}
*/
default BinarySerializer<StateT> getSplitCoordinatorCheckpointSerializer() {
return new SimpleBinarySerializer<>();
}
/**
* Create type info converter for the source, default value {@link BitSailTypeInfoConverter}
*/
default TypeInfoConverter createTypeInfoConverter() {
return new BitSailTypeInfoConverter();
}
/**
* Get Source' name.
*/
String getReaderName();
}
configure方法
主要去做一些客户端的配置的分发和提取,可以操作运行时环境 ExecutionEnviron 的配置和 readerConfiguration 的配置。
示例:
java
@Override
public void configure(ExecutionEnviron execution, BitSailConfiguration readerConfiguration) {
this.readerConfiguration = readerConfiguration;
this.commonConfiguration = execution.getCommonConfiguration();
}
getSourceBoundedness方法
设置作业的处理方式,是采用流式处理方法、批式处理方法,或者是流批一体的处理方式,在流批一体的场景中,我们需要根据作业的不同类型设置不同的处理方式。
具体对应关系如下:
| Job Type | Boundedness |
|---|---|
| batch | Boundedness.BOUNDEDNESS |
| stream | Boundedness.UNBOUNDEDNESS |
流批一体场景示例:
java
@Override
public Boundedness getSourceBoundedness() {
return Mode.BATCH.equals(Mode.getJobRunMode(commonConfiguration.get(CommonOptions.JOB_TYPE))) ?
Boundedness.BOUNDEDNESS :
Boundedness.UNBOUNDEDNESS;
}
批式场景示例:
java
public Boundedness getSourceBoundedness() {
return Boundedness.BOUNDEDNESS;
}
createTypeInfoConverter方法
用于指定 Source 连接器的类型转换器;我们知道大多数的外部数据系统都存在着自己的类型定义,它们的定义与 BitSail 的类型定义不会完全一致;为了简化类型定义的转换,我们支持了通过配置文件来映射两者之间的关系,进而来简化配置文件的开发。
在行为上表现为对任务描述 Json 文件中 reader 部分的 columns 的解析,对于 columns 中不同字段的 type 会根据上面描述文件从 ClickhouseReaderOptions.COLUMNS 字段中解析到 readerContext.getTypeInfos() 中。
实现:
- BitSailTypeInfoConverter:默认的 TypeInfoConverter,直接对 ReaderOptions.COLUMNS 字段进行字符串的直接解析,COLUMNS 字段中是什么类型,TypeInfoConverter 中就是什么类型。
- FileMappingTypeInfoConverter:会在 BitSail 类型系统转换时去绑定 {readername}-type-converter.yaml 文件,做数据库字段类型和 BitSail 类型的映射。ReaderOptions.COLUMNS 字段在通过这个映射文件转换后才会映射到 TypeInfoConverter 中。
示例:
1. FileMappingTypeInfoConverter
通过 JDBC 方式连接的数据库,包括 MySql、Oracle、SqlServer、Kudu、ClickHouse 等。
这里数据源的特点是以 java.sql.ResultSet 的接口形式返回获取的数据,对于这类数据库,我们往往将 TypeInfoConverter 对象设计为 FileMappingTypeInfoConverter,这个对象会在 BitSail 类型系统转换时去绑定 {readername}-type-converter.yaml 文件,做数据库字段类型和 BitSail 类型的映射。
java
@Override
public TypeInfoConverter createTypeInfoConverter() {
return new FileMappingTypeInfoConverter(getReaderName());
}
对于 {readername}-type-converter.yaml 文件的解析,以 clickhouse-type-converter.yaml 为例。
yaml
# Clickhouse Type to BitSail Type
engine.type.to.bitsail.type.converter:
- source.type: int32
target.type: int
- source.type: float64
target.type: double
- source.type: string
target.type: string
- source.type: date
target.type: date.date
- source.type: null
target.type: void
# BitSail Type to Clickhouse Type
bitsail.type.to.engine.type.converter:
- source.type: int
target.type: int32
- source.type: double
target.type: float64
- source.type: date.date
target.type: date
- source.type: string
target.type: string
这个文件起到的作用是进行 job 描述 json 文件中 reader 部分的 columns 的解析,对于 columns 中不同字段的 type 会根据上面描述文件从 ClickhouseReaderOptions.COLUMNS 字段中解析到 readerContext.getTypeInfos() 中。
json
"reader": {
"class": "com.bytedance.bitsail.connector.clickhouse.source.ClickhouseSource",
"jdbc_url": "jdbc:clickhouse://localhost:8123",
"db_name": "default",
"table_name": "test_ch_table",
"split_field": "id",
"split_config": "{"name": "id", "lower_bound": 0, "upper_bound": "10000", "split_num": 3}",
"sql_filter": "( id % 2 == 0 )",
"columns": [
{
"name": "id",
"type": "int64"
},
{
"name": "int_type",
"type": "int32"
},
{
"name": "double_type",
"type": "float64"
},
{
"name": "string_type",
"type": "string"
},
{
"name": "p_date",
"type": "date"
}
]
}

这种方式不仅仅适用于数据库,也适用于所有需要在类型转换中需要引擎侧和 BitSail 侧进行类型映射的场景。
2. BitSailTypeInfoConverter
通常采用默认的方式进行类型转换,直接对 ReaderOptions.COLUMNS 字段进行字符串的直接解析。
java
@Override
public TypeInfoConverter createTypeInfoConverter() {
return new BitSailTypeInfoConverter();
}
以 Hadoop 为例:
json
"reader": {
"class": "com.bytedance.bitsail.connector.hadoop.source.HadoopSource",
"path_list": "hdfs://127.0.0.1:9000/test_namespace/source/test.json",
"content_type": "json",
"reader_parallelism_num": 1,
"columns": [
{
"name": "id",
"type": "int"
},
{
"name": "string_type",
"type": "string"
},
{
"name": "map_string_string",
"type": "map<string,string>"
},
{
"name": "array_string",
"type": "list<string>"
}
]
}

createSourceReader方法
书写具体的数据读取逻辑,负责数据读取的组件,在接收到 Split 后会对其进行数据读取,然后将数据传输给下一个算子。
具体传入构造 SourceReader 的参数按需求决定,但是一定要保证所有参数可以序列化。如果不可序列化,将会在 createJobGraph 的时候出错。
示例:
java
public SourceReader<Row, RocketMQSplit> createReader(SourceReader.Context readerContext) {
return new RocketMQSourceReader(
readerConfiguration,
readerContext,
getSourceBoundedness()
);
}
createSplitCoordinator方法
书写具体的数据分片、分片分配逻辑,SplitCoordinator 承担了去创建、管理 Split 的角色。
具体传入构造 SplitCoordinator 的参数按需求决定,但是一定要保证所有参数可以序列化。如果不可序列化,将会在 createJobGraph 的时候出错。
示例:
java
public SourceSplitCoordinator<RocketMQSplit, RocketMQState> createSplitCoordinator(SourceSplitCoordinator.Context<RocketMQSplit, RocketMQState> coordinatorContext) {
return new RocketMQSourceSplitCoordinator(
coordinatorContext,
readerConfiguration,
getSourceBoundedness()
);
}
1.2 ParallelismComputable接口
java
public interface ParallelismComputable extends Serializable {
/**
* give a parallelism advice for reader/writer based on configurations and upstream parallelism advice
*
* @param commonConf common configuration
* @param selfConf reader/writer configuration
* @param upstreamAdvice parallelism advice from upstream (when an operator has no upstream in DAG, its upstream is
* global parallelism)
* @return parallelism advice for the reader/writer
*/
ParallelismAdvice getParallelismAdvice(BitSailConfiguration commonConf,
BitSailConfiguration selfConf,
ParallelismAdvice upstreamAdvice) throws Exception;
}
getParallelismAdvice方法
用于指定下游 reader 的并行数目。一般有以下的方式:可以选 selfConf.get(ClickhouseReaderOptions.READER_PARALLELISM_NUM) 来指定并行度。也可以自定义自己的并行度划分逻辑。
示例:
比如在 RocketMQ 中,我们可以定义每1个 reader 可以处理至多4个队列 DEFAULT_ROCKETMQ_PARALLELISM_THRESHOLD = 4 通过这种自定义的方式获取对应的并行度。
java
public ParallelismAdvice getParallelismAdvice(BitSailConfiguration commonConfiguration,
BitSailConfiguration rocketmqConfiguration,
ParallelismAdvice upstreamAdvice) throws Exception {
String cluster = rocketmqConfiguration.get(RocketMQSourceOptions.CLUSTER);
String topic = rocketmqConfiguration.get(RocketMQSourceOptions.TOPIC);
String consumerGroup = rocketmqConfiguration.get(RocketMQSourceOptions.CONSUMER_GROUP);
DefaultLitePullConsumer consumer = RocketMQUtils.prepareRocketMQConsumer(rocketmqConfiguration, String.format(SOURCE_INSTANCE_NAME_TEMPLATE,
cluster,
topic,
consumerGroup,
UUID.randomUUID()
));
try {
consumer.start();
Collection<MessageQueue> messageQueues = consumer.fetchMessageQueues(topic);
int adviceParallelism = Math.max(CollectionUtils.size(messageQueues) / DEFAULT_ROCKETMQ_PARALLELISM_THRESHOLD, 1);
return ParallelismAdvice.builder()
.adviceParallelism(adviceParallelism)
.enforceDownStreamChain(true)
.build();
} finally {
consumer.shutdown();
}
}
二、SourceSplit
数据源的数据分片格式,需要我们实现 SourceSplit 接口。
end
