[BitSail] Connector开发详解系列四:Sink、Writer
更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
Sink Connector
BitSail Sink Connector交互流程介绍
- Sink:数据写入组件的生命周期管理,主要负责和框架的交互,构架作业,它不参与作业真正的执行。
- Writer:负责将接收到的数据写到外部存储。
- WriterCommitter(可选):对数据进行提交操作,来完成两阶段提交的操作;实现exactly-once的语义。
开发者首先需要创建Sink类,实现Sink接口,主要负责数据写入组件的生命周期管理,构架作业。通过configure方法定义writerConfiguration的配置,通过createTypeInfoConverter方法来进行数据类型转换,将内部类型进行转换写到外部系统,同Source部分。之后我们再定义Writer类实现具体的数据写入逻辑,在write方法调用时将BitSail Row类型把数据写到缓存队列中,在flush方法调用时将缓存队列中的数据刷写到目标数据源中。
Sink
数据写入组件的生命周期管理,主要负责和框架的交互,构架作业,它不参与作业真正的执行。
对于每一个Sink任务,我们要实现一个继承Sink接口的类。
Sink接口
public interface Sink<InputT, CommitT extends Serializable, WriterStateT extends Serializable> extends Serializable {
/**
* @return The name of writer operation.
*/
String getWriterName();
/**
* Configure writer with user defined options.
*
* @param commonConfiguration Common options.
* @param writerConfiguration Options for writer.
*/
void configure(BitSailConfiguration commonConfiguration, BitSailConfiguration writerConfiguration) throws Exception;
/**
* Create a writer for processing elements.
*
* @return An initialized writer.
*/
Writer<InputT, CommitT, WriterStateT> createWriter(Writer.Context<WriterStateT> context) throws IOException;
/**
* @return A converter which supports conversion from BitSail { @link TypeInfo}
* and external engine type.
*/
default TypeInfoConverter createTypeInfoConverter() {
return new BitSailTypeInfoConverter();
}
/**
* @return A committer for commit committable objects.
*/
default Optional<WriterCommitter<CommitT>> createCommitter() {
return Optional.empty();
}
/**
* @return A serializer which convert committable object to byte array.
*/
default BinarySerializer<CommitT> getCommittableSerializer() {
return new SimpleBinarySerializer<CommitT>();
}
/**
* @return A serializer which convert state object to byte array.
*/
default BinarySerializer<WriterStateT> getWriteStateSerializer() {
return new SimpleBinarySerializer<WriterStateT>();
}
}
configure方法
负责configuration的初始化,通过commonConfiguration中的配置区分流式任务或者批式任务,向Writer类传递writerConfiguration。
示例
ElasticsearchSink:
public void configure(BitSailConfiguration commonConfiguration, BitSailConfiguration writerConfiguration) {
writerConf = writerConfiguration;
}
createWriter方法
负责生成一个继承自Writer接口的connector Writer类。
createTypeInfoConverter方法
类型转换,将内部类型进行转换写到外部系统,同Source部分。
createCommitter方法
可选方法,书写具体数据提交逻辑,一般用于想要保证数据exactly-once语义的场景,writer在完成数据写入后,committer来完成提交,进而实现二阶段提交,详细可以参考Doris Connector的实现。
Writer
具体的数据写入逻辑
Writer接口
public interface Writer<InputT, CommT, WriterStateT> extends Serializable, Closeable {
/**
* Output an element to target source.
*
* @param element Input data from upstream.
*/
void write(InputT element) throws IOException;
/**
* Flush buffered input data to target source.
*
* @param endOfInput Flag indicates if all input data are delivered.
*/
void flush(boolean endOfInput) throws IOException;
/**
* Prepare commit information before snapshotting when checkpoint is triggerred.
*
* @return Information to commit in this checkpoint.
* @throws IOException Exceptions encountered when preparing committable information.
*/
List<CommT> prepareCommit() throws IOException;
/**
* Do snapshot for at each checkpoint.
*
* @param checkpointId The id of checkpoint when snapshot triggered.
* @return The current state of writer.
* @throws IOException Exceptions encountered when snapshotting.
*/
default List<WriterStateT> snapshotState(long checkpointId) throws IOException {
return Collections.emptyList();
}
/**
* Closing writer when operator is closed.
*
* @throws IOException Exception encountered when closing writer.
*/
default void close() throws IOException {
}
interface Context<WriterStateT> extends Serializable {
TypeInfo<?>[] getTypeInfos();
int getIndexOfSubTaskId();
boolean isRestored();
List<WriterStateT> getRestoreStates();
}
}
构造方法
根据writerConfiguration配置初始化数据源的连接对象。
示例
public RedisWriter(BitSailConfiguration writerConfiguration) {
// initialize ttl
int ttl = writerConfiguration.getUnNecessaryOption(RedisWriterOptions.TTL, -1);
TtlType ttlType;
try {
ttlType = TtlType.valueOf(StringUtils.upperCase(writerConfiguration.get(RedisWriterOptions.TTL_TYPE)));
} catch (IllegalArgumentException e) {
throw BitSailException.asBitSailException(RedisPluginErrorCode.ILLEGAL_VALUE,
String.format("unknown ttl type: %s", writerConfiguration.get(RedisWriterOptions.TTL_TYPE)));
}
int ttlInSeconds = ttl < 0 ? -1 : ttl * ttlType.getContainSeconds();
log.info("ttl is {}(s)", ttlInSeconds);
// initialize commandDescription
String redisDataType = StringUtils.upperCase(writerConfiguration.get(RedisWriterOptions.REDIS_DATA_TYPE));
String additionalKey = writerConfiguration.getUnNecessaryOption(RedisWriterOptions.ADDITIONAL_KEY, "default_redis_key");
this.commandDescription = initJedisCommandDescription(redisDataType, ttlInSeconds, additionalKey);
this.columnSize = writerConfiguration.get(RedisWriterOptions.COLUMNS).size();
// initialize jedis pool
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_TOTAL_CONNECTIONS));
jedisPoolConfig.setMaxIdle(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_IDLE_CONNECTIONS));
jedisPoolConfig.setMinIdle(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MIN_IDLE_CONNECTIONS));
jedisPoolConfig.setMaxWait(Duration.ofMillis(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_WAIT_TIME_IN_MILLIS)));
String redisHost = writerConfiguration.getNecessaryOption(RedisWriterOptions.HOST, RedisPluginErrorCode.REQUIRED_VALUE);
int redisPort = writerConfiguration.getNecessaryOption(RedisWriterOptions.PORT, RedisPluginErrorCode.REQUIRED_VALUE);
String redisPassword = writerConfiguration.get(RedisWriterOptions.PASSWORD);
int timeout = writerConfiguration.get(RedisWriterOptions.CLIENT_TIMEOUT_MS);
if (StringUtils.isEmpty(redisPassword)) {
this.jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout);
} else {
this.jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout, redisPassword);
}
// initialize record queue
int batchSize = writerConfiguration.get(RedisWriterOptions.WRITE_BATCH_INTERVAL);
this.recordQueue = new CircularFifoQueue<>(batchSize);
this.logSampleInterval = writerConfiguration.get(RedisWriterOptions.LOG_SAMPLE_INTERVAL);
this.jedisFetcher = RetryerBuilder.<Jedis>newBuilder()
.retryIfResult(Objects::isNull)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))
.build()
.wrap(jedisPool::getResource);
this.maxAttemptCount = writerConfiguration.get(RedisWriterOptions.MAX_ATTEMPT_COUNT);
this.retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(needRetry -> Objects.equals(needRetry, true))
.retryIfException(e -> !(e instanceof BitSailException))
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(maxAttemptCount))
.build();
}
write方法
该方法调用时会将BitSail Row类型把数据写到缓存队列中,也可以在这里对Row类型数据进行各种格式预处理。直接存储到缓存队列中,或者进行加工处理。如果这里设定了缓存队列的大小,那么在缓存队列写满后要调用flush进行刷写。
示例
redis:将BitSail Row格式的数据直接存储到一定大小的缓存队列中
public void write(Row record) throws IOException {
validate(record);
this.recordQueue.add(record);
if (recordQueue.isAtFullCapacity()) {
flush(false);
}
}
Druid:将BitSail Row格式的数据做格式预处理,转化到StringBuffer中储存起来。
@Override
public void write(final Row element) {
final StringJoiner joiner = new StringJoiner(DEFAULT_FIELD_DELIMITER, "", "");
for (int i = 0; i < element.getArity(); i++) {
final Object v = element.getField(i);
if (v != null) {
joiner.add(v.toString());
}
}
// timestamp column is a required field to add in Druid.
// See https://druid.apache.org/docs/24.0.0/ingestion/data-model.html#primary-timestamp
joiner.add(String.valueOf(processTime));
data.append(joiner);
data.append(DEFAULT_LINE_DELIMITER);
}
flush方法
该方法中主要实现将write方法的缓存中的数据刷写到目标数据源中。
示例
redis:将缓存队列中的BitSail Row格式的数据刷写到目标数据源中。
public void flush(boolean endOfInput) throws IOException {
processorId++;
try (PipelineProcessor processor = genPipelineProcessor(recordQueue.size(), this.complexTypeWithTtl)) {
Row record;
while ((record = recordQueue.poll()) != null) {
String key = (String) record.getField(0);
String value = (String) record.getField(1);
String scoreOrHashKey = value;
if (columnSize == SORTED_SET_OR_HASH_COLUMN_SIZE) {
value = (String) record.getField(2);
// Replace empty key with additionalKey in sorted set and hash.
if (key.length() == 0) {
key = commandDescription.getAdditionalKey();
}
}
if (commandDescription.getJedisCommand() == JedisCommand.ZADD) {
// sorted set
processor.addInitialCommand(new Command(commandDescription, key.getBytes(), parseScoreFromString(scoreOrHashKey), value.getBytes()));
} else if (commandDescription.getJedisCommand() == JedisCommand.HSET) {
// hash
processor.addInitialCommand(new Command(commandDescription, key.getBytes(), scoreOrHashKey.getBytes(), value.getBytes()));
} else if (commandDescription.getJedisCommand() == JedisCommand.HMSET) {
//mhset
if ((record.getArity() - 1) % 2 != 0) {
throw new BitSailException(CONVERT_NOT_SUPPORT, "Inconsistent data entry.");
}
List<byte[]> datas = Arrays.stream(record.getFields())
.collect(Collectors.toList()).stream().map(o -> ((String) o).getBytes())
.collect(Collectors.toList()).subList(1, record.getFields().length);
Map<byte[], byte[]> map = new HashMap<>((record.getArity() - 1) / 2);
for (int index = 0; index < datas.size(); index = index + 2) {
map.put(datas.get(index), datas.get(index + 1));
}
processor.addInitialCommand(new Command(commandDescription, key.getBytes(), map));
} else {
// set and string
processor.addInitialCommand(new Command(commandDescription, key.getBytes(), value.getBytes()));
}
}
retryer.call(processor::run);
} catch (ExecutionException | RetryException e) {
if (e.getCause() instanceof BitSailException) {
throw (BitSailException) e.getCause();
} else if (e.getCause() instanceof RedisUnexpectedException) {
throw (RedisUnexpectedException) e.getCause();
}
throw e;
} catch (IOException e) {
throw new RuntimeException("Error while init jedis client.", e);
}
}
Druid:使用HTTP post方式提交sink作业给数据源。
private HttpURLConnection provideHttpURLConnection(final String coordinatorURL) throws IOException {
final URL url = new URL("http://" + coordinatorURL + DRUID_ENDPOINT);
final HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Accept", "application/json, text/plain, */*");
con.setDoOutput(true);
return con;
}
public void flush(final boolean endOfInput) throws IOException {
final ParallelIndexIOConfig ioConfig = provideDruidIOConfig(data);
final ParallelIndexSupervisorTask indexTask = provideIndexTask(ioConfig);
final String inputJSON = provideInputJSONString(indexTask);
final byte[] input = inputJSON.getBytes();
try (final OutputStream os = httpURLConnection.getOutputStream()) {
os.write(input, 0, input.length);
}
try (final BufferedReader br =
new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), StandardCharsets.UTF_8))) {
final StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
LOG.info("Druid write task has been sent, and the response is {}", response);
}
}
close方法
关闭之前创建的各种目标数据源连接对象。
示例
public void close() throws IOException {
bulkProcessor.close();
restClient.close();
checkErrorAndRethrow();
}
【关于BitSail】:
️ Star 不迷路 https://github.com/bytedance/bitsail
提交问题和建议:https://github.com/bytedance/bitsail/issues
贡献代码:https://github.com/bytedance/bitsail/pulls
BitSail官网:https://bytedance.github.io/bitsail/zh/
订阅邮件列表:bitsail+subscribe@googlegroups.com
加入BitSail技术社群
[BitSail] Connector开发详解系列四:Sink、Writer的更多相关文章
- 干货 | BitSail Connector 开发详解系列一:Source
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 BitSail 是字节跳动自研的数据集成产品,支持多种异构数据源间的数据同步,并提供离线.实时.全量.增量场景下全 ...
- Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节
简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...
- wpf 客户端【JDAgent桌面助手】开发详解(四) popup控件的win8.0的bug
目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...
- Eureka详解系列(四)--Eureka Client部分的源码和配置
简介 按照原定的计划,我将分三个部分来分析 Eureka 的源码: Eureka 的配置体系(已经写完,见Eureka详解系列(三)--探索Eureka强大的配置体系): Eureka Client ...
- Handler详解系列(四)——利用Handler在主线程与子线程之间互发消息,handler详解
MainActivity如下: package cc.c; import android.app.Activity; import android.os.Bundle; import android. ...
- PayPal 开发详解(四):买家付款
1.点击[立即付款] 2.使用[个人账户]登录paypal Personal测试帐号 3.核对商品信息 4.确认信息无误,点击[立即付款],提示付款成功,跳转到商家设置的URL 5.URL中包含pa ...
- 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码
简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...
- Java源码详解系列(十二)--Eureka的使用和源码
eureka 是由 Netflix 团队开发的针对中间层服务的负载均衡器,在微服务项目中被广泛使用.相比 SLB.ALB 等负载均衡器,eureka 的服务注册是无状态的,扩展起来非常方便. 在这个系 ...
- 源码详解系列(六) ------ 全面讲解druid的使用和源码
简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...
- Java源码详解系列(十)--全面分析mybatis的使用、源码和代码生成器(总计5篇博客)
简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...
随机推荐
- 在NestJS应用程序中使用 Unleash 实现功能切换的指南
前言 近年来,软件开发行业迅速发展,功能开关(Feature Toggle)成为了一种常见的开发实践.通过功能开关,可以在运行时动态地启用或禁用应用程序的特定功能,以提供更灵活的软件交付和配置管理.对 ...
- 巅峰对决:英伟达 V100、A100/800、H100/800 GPU 对比
近期,不论是国外的 ChatGPT,还是国内诸多的大模型,让 AIGC 的市场一片爆火.而在 AIGC 的种种智能表现背后,均来自于堪称天文数字的算力支持.以 ChatGPT 为例,据微软高管透露,为 ...
- SNN_文献阅读_Text Classification in Memristor-based Spiking Neural Networks
SNN中局部学习和非局部学习,基于梯度的规则都需要对用于表示单个连续值的脉冲训练窗口上的累积误差进行平均,这种方法在更新权重时考虑了每一个脉冲的影响.在计算速度和空间效率等方面,特别是当代表单个数值的 ...
- 高效使用 PyMongo 进行 MongoDB 查询和插入操作
插入到集合中: 要将记录(在MongoDB中称为文档)插入到集合中,使用insert_one()方法.insert_one()方法的第一个参数是一个包含文档中每个字段的名称和值的字典. import ...
- 车的可用捕获量(3.26leetcode每日打卡)
在一个 8 x 8 的棋盘上,有一个白色车(rook).也可能有空方块,白色的象(bishop)和黑色的卒(pawn).它们分别以字符 "R",".",&quo ...
- Jenkins从Ubuntu迁移至AlmaLinux问题及相关解决记录
相关背景 之前在Ubuntu平台上搭建了Jenkins(在Ubuntu机器上使用war包安装Jenkins),现在由于一些需求,需要将系统迁移到AlmaLinux平台.由于AlmaLinux属于Cen ...
- top命令和ps命令
top 命令和 ps 命令 ps 命令 ps 命令查看系统的瞬时信息.通常使用ps -ef | grep 进程名, -e 代表显示所有进程,-f 表示做一个更为完整的输出.经常使用这个命令获得进程的 ...
- ABAP 生产订单长文本增强 <销售计划 、物料独立需求 长文本带入 计划订单-生产订单 >
计划订单长文本带入生产订单 尝试在生产订单保存后 用 creat_text 函数 去创建长文本,发现前台不显示,查看 文本抬头底表 STXL 发现有值 ,用READ 函数 读取 能读. DATA:td ...
- [AGC038E] Gachapon
Problem Statement Snuke found a random number generator. It generates an integer between $0$ and $N- ...
- PolarCTF-2023冬季个人挑战赛 WP
Crypto 数星星 题目 小明暗恋小红很久了,终于在一个月黑风高的夜晚,决定约她出去数星星.小明数着数着,数出了一串数字,3,6,10,12,15,他觉得这是爱情的关键,思考了整整一晚上,小红很生气 ...