更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【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接口

  1. public interface Sink<InputT, CommitT extends Serializable, WriterStateT extends Serializable> extends Serializable {
  2. /**
  3. * @return The name of writer operation.
  4. */
  5. String getWriterName();
  6. /**
  7. * Configure writer with user defined options.
  8. *
  9. * @param commonConfiguration Common options.
  10. * @param writerConfiguration Options for writer.
  11. */
  12. void configure(BitSailConfiguration commonConfiguration, BitSailConfiguration writerConfiguration) throws Exception;
  13. /**
  14. * Create a writer for processing elements.
  15. *
  16. * @return An initialized writer.
  17. */
  18. Writer<InputT, CommitT, WriterStateT> createWriter(Writer.Context<WriterStateT> context) throws IOException;
  19. /**
  20. * @return A converter which supports conversion from BitSail { @link TypeInfo}
  21. * and external engine type.
  22. */
  23. default TypeInfoConverter createTypeInfoConverter() {
  24. return new BitSailTypeInfoConverter();
  25. }
  26. /**
  27. * @return A committer for commit committable objects.
  28. */
  29. default Optional<WriterCommitter<CommitT>> createCommitter() {
  30. return Optional.empty();
  31. }
  32. /**
  33. * @return A serializer which convert committable object to byte array.
  34. */
  35. default BinarySerializer<CommitT> getCommittableSerializer() {
  36. return new SimpleBinarySerializer<CommitT>();
  37. }
  38. /**
  39. * @return A serializer which convert state object to byte array.
  40. */
  41. default BinarySerializer<WriterStateT> getWriteStateSerializer() {
  42. return new SimpleBinarySerializer<WriterStateT>();
  43. }
  44. }

configure方法

负责configuration的初始化,通过commonConfiguration中的配置区分流式任务或者批式任务,向Writer类传递writerConfiguration。

示例

ElasticsearchSink:

  1. public void configure(BitSailConfiguration commonConfiguration, BitSailConfiguration writerConfiguration) {
  2. writerConf = writerConfiguration;
  3. }

createWriter方法

负责生成一个继承自Writer接口的connector Writer类。

createTypeInfoConverter方法

类型转换,将内部类型进行转换写到外部系统,同Source部分。

createCommitter方法

可选方法,书写具体数据提交逻辑,一般用于想要保证数据exactly-once语义的场景,writer在完成数据写入后,committer来完成提交,进而实现二阶段提交,详细可以参考Doris Connector的实现。

Writer

具体的数据写入逻辑

Writer接口

  1. public interface Writer<InputT, CommT, WriterStateT> extends Serializable, Closeable {
  2. /**
  3. * Output an element to target source.
  4. *
  5. * @param element Input data from upstream.
  6. */
  7. void write(InputT element) throws IOException;
  8. /**
  9. * Flush buffered input data to target source.
  10. *
  11. * @param endOfInput Flag indicates if all input data are delivered.
  12. */
  13. void flush(boolean endOfInput) throws IOException;
  14. /**
  15. * Prepare commit information before snapshotting when checkpoint is triggerred.
  16. *
  17. * @return Information to commit in this checkpoint.
  18. * @throws IOException Exceptions encountered when preparing committable information.
  19. */
  20. List<CommT> prepareCommit() throws IOException;
  21. /**
  22. * Do snapshot for at each checkpoint.
  23. *
  24. * @param checkpointId The id of checkpoint when snapshot triggered.
  25. * @return The current state of writer.
  26. * @throws IOException Exceptions encountered when snapshotting.
  27. */
  28. default List<WriterStateT> snapshotState(long checkpointId) throws IOException {
  29. return Collections.emptyList();
  30. }
  31. /**
  32. * Closing writer when operator is closed.
  33. *
  34. * @throws IOException Exception encountered when closing writer.
  35. */
  36. default void close() throws IOException {
  37. }
  38. interface Context<WriterStateT> extends Serializable {
  39. TypeInfo<?>[] getTypeInfos();
  40. int getIndexOfSubTaskId();
  41. boolean isRestored();
  42. List<WriterStateT> getRestoreStates();
  43. }
  44. }

构造方法

根据writerConfiguration配置初始化数据源的连接对象。

示例

  1. public RedisWriter(BitSailConfiguration writerConfiguration) {
  2. // initialize ttl
  3. int ttl = writerConfiguration.getUnNecessaryOption(RedisWriterOptions.TTL, -1);
  4. TtlType ttlType;
  5. try {
  6. ttlType = TtlType.valueOf(StringUtils.upperCase(writerConfiguration.get(RedisWriterOptions.TTL_TYPE)));
  7. } catch (IllegalArgumentException e) {
  8. throw BitSailException.asBitSailException(RedisPluginErrorCode.ILLEGAL_VALUE,
  9. String.format("unknown ttl type: %s", writerConfiguration.get(RedisWriterOptions.TTL_TYPE)));
  10. }
  11. int ttlInSeconds = ttl < 0 ? -1 : ttl * ttlType.getContainSeconds();
  12. log.info("ttl is {}(s)", ttlInSeconds);
  13. // initialize commandDescription
  14. String redisDataType = StringUtils.upperCase(writerConfiguration.get(RedisWriterOptions.REDIS_DATA_TYPE));
  15. String additionalKey = writerConfiguration.getUnNecessaryOption(RedisWriterOptions.ADDITIONAL_KEY, "default_redis_key");
  16. this.commandDescription = initJedisCommandDescription(redisDataType, ttlInSeconds, additionalKey);
  17. this.columnSize = writerConfiguration.get(RedisWriterOptions.COLUMNS).size();
  18. // initialize jedis pool
  19. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  20. jedisPoolConfig.setMaxTotal(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_TOTAL_CONNECTIONS));
  21. jedisPoolConfig.setMaxIdle(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_IDLE_CONNECTIONS));
  22. jedisPoolConfig.setMinIdle(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MIN_IDLE_CONNECTIONS));
  23. jedisPoolConfig.setMaxWait(Duration.ofMillis(writerConfiguration.get(RedisWriterOptions.JEDIS_POOL_MAX_WAIT_TIME_IN_MILLIS)));
  24. String redisHost = writerConfiguration.getNecessaryOption(RedisWriterOptions.HOST, RedisPluginErrorCode.REQUIRED_VALUE);
  25. int redisPort = writerConfiguration.getNecessaryOption(RedisWriterOptions.PORT, RedisPluginErrorCode.REQUIRED_VALUE);
  26. String redisPassword = writerConfiguration.get(RedisWriterOptions.PASSWORD);
  27. int timeout = writerConfiguration.get(RedisWriterOptions.CLIENT_TIMEOUT_MS);
  28. if (StringUtils.isEmpty(redisPassword)) {
  29. this.jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout);
  30. } else {
  31. this.jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout, redisPassword);
  32. }
  33. // initialize record queue
  34. int batchSize = writerConfiguration.get(RedisWriterOptions.WRITE_BATCH_INTERVAL);
  35. this.recordQueue = new CircularFifoQueue<>(batchSize);
  36. this.logSampleInterval = writerConfiguration.get(RedisWriterOptions.LOG_SAMPLE_INTERVAL);
  37. this.jedisFetcher = RetryerBuilder.<Jedis>newBuilder()
  38. .retryIfResult(Objects::isNull)
  39. .retryIfRuntimeException()
  40. .withStopStrategy(StopStrategies.stopAfterAttempt(3))
  41. .withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))
  42. .build()
  43. .wrap(jedisPool::getResource);
  44. this.maxAttemptCount = writerConfiguration.get(RedisWriterOptions.MAX_ATTEMPT_COUNT);
  45. this.retryer = RetryerBuilder.<Boolean>newBuilder()
  46. .retryIfResult(needRetry -> Objects.equals(needRetry, true))
  47. .retryIfException(e -> !(e instanceof BitSailException))
  48. .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
  49. .withStopStrategy(StopStrategies.stopAfterAttempt(maxAttemptCount))
  50. .build();
  51. }

write方法

该方法调用时会将BitSail Row类型把数据写到缓存队列中,也可以在这里对Row类型数据进行各种格式预处理。直接存储到缓存队列中,或者进行加工处理。如果这里设定了缓存队列的大小,那么在缓存队列写满后要调用flush进行刷写。

示例

redis:将BitSail Row格式的数据直接存储到一定大小的缓存队列中

  1. public void write(Row record) throws IOException {
  2. validate(record);
  3. this.recordQueue.add(record);
  4. if (recordQueue.isAtFullCapacity()) {
  5. flush(false);
  6. }
  7. }

Druid:将BitSail Row格式的数据做格式预处理,转化到StringBuffer中储存起来。

  1. @Override
  2. public void write(final Row element) {
  3. final StringJoiner joiner = new StringJoiner(DEFAULT_FIELD_DELIMITER, "", "");
  4. for (int i = 0; i < element.getArity(); i++) {
  5. final Object v = element.getField(i);
  6. if (v != null) {
  7. joiner.add(v.toString());
  8. }
  9. }
  10. // timestamp column is a required field to add in Druid.
  11. // See https://druid.apache.org/docs/24.0.0/ingestion/data-model.html#primary-timestamp
  12. joiner.add(String.valueOf(processTime));
  13. data.append(joiner);
  14. data.append(DEFAULT_LINE_DELIMITER);
  15. }

flush方法

该方法中主要实现将write方法的缓存中的数据刷写到目标数据源中。

示例

redis:将缓存队列中的BitSail Row格式的数据刷写到目标数据源中。

  1. public void flush(boolean endOfInput) throws IOException {
  2. processorId++;
  3. try (PipelineProcessor processor = genPipelineProcessor(recordQueue.size(), this.complexTypeWithTtl)) {
  4. Row record;
  5. while ((record = recordQueue.poll()) != null) {
  6. String key = (String) record.getField(0);
  7. String value = (String) record.getField(1);
  8. String scoreOrHashKey = value;
  9. if (columnSize == SORTED_SET_OR_HASH_COLUMN_SIZE) {
  10. value = (String) record.getField(2);
  11. // Replace empty key with additionalKey in sorted set and hash.
  12. if (key.length() == 0) {
  13. key = commandDescription.getAdditionalKey();
  14. }
  15. }
  16. if (commandDescription.getJedisCommand() == JedisCommand.ZADD) {
  17. // sorted set
  18. processor.addInitialCommand(new Command(commandDescription, key.getBytes(), parseScoreFromString(scoreOrHashKey), value.getBytes()));
  19. } else if (commandDescription.getJedisCommand() == JedisCommand.HSET) {
  20. // hash
  21. processor.addInitialCommand(new Command(commandDescription, key.getBytes(), scoreOrHashKey.getBytes(), value.getBytes()));
  22. } else if (commandDescription.getJedisCommand() == JedisCommand.HMSET) {
  23. //mhset
  24. if ((record.getArity() - 1) % 2 != 0) {
  25. throw new BitSailException(CONVERT_NOT_SUPPORT, "Inconsistent data entry.");
  26. }
  27. List<byte[]> datas = Arrays.stream(record.getFields())
  28. .collect(Collectors.toList()).stream().map(o -> ((String) o).getBytes())
  29. .collect(Collectors.toList()).subList(1, record.getFields().length);
  30. Map<byte[], byte[]> map = new HashMap<>((record.getArity() - 1) / 2);
  31. for (int index = 0; index < datas.size(); index = index + 2) {
  32. map.put(datas.get(index), datas.get(index + 1));
  33. }
  34. processor.addInitialCommand(new Command(commandDescription, key.getBytes(), map));
  35. } else {
  36. // set and string
  37. processor.addInitialCommand(new Command(commandDescription, key.getBytes(), value.getBytes()));
  38. }
  39. }
  40. retryer.call(processor::run);
  41. } catch (ExecutionException | RetryException e) {
  42. if (e.getCause() instanceof BitSailException) {
  43. throw (BitSailException) e.getCause();
  44. } else if (e.getCause() instanceof RedisUnexpectedException) {
  45. throw (RedisUnexpectedException) e.getCause();
  46. }
  47. throw e;
  48. } catch (IOException e) {
  49. throw new RuntimeException("Error while init jedis client.", e);
  50. }
  51. }

Druid:使用HTTP post方式提交sink作业给数据源。

  1. private HttpURLConnection provideHttpURLConnection(final String coordinatorURL) throws IOException {
  2. final URL url = new URL("http://" + coordinatorURL + DRUID_ENDPOINT);
  3. final HttpURLConnection con = (HttpURLConnection) url.openConnection();
  4. con.setRequestMethod("POST");
  5. con.setRequestProperty("Content-Type", "application/json");
  6. con.setRequestProperty("Accept", "application/json, text/plain, */*");
  7. con.setDoOutput(true);
  8. return con;
  9. }
  10. public void flush(final boolean endOfInput) throws IOException {
  11. final ParallelIndexIOConfig ioConfig = provideDruidIOConfig(data);
  12. final ParallelIndexSupervisorTask indexTask = provideIndexTask(ioConfig);
  13. final String inputJSON = provideInputJSONString(indexTask);
  14. final byte[] input = inputJSON.getBytes();
  15. try (final OutputStream os = httpURLConnection.getOutputStream()) {
  16. os.write(input, 0, input.length);
  17. }
  18. try (final BufferedReader br =
  19. new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), StandardCharsets.UTF_8))) {
  20. final StringBuilder response = new StringBuilder();
  21. String responseLine;
  22. while ((responseLine = br.readLine()) != null) {
  23. response.append(responseLine.trim());
  24. }
  25. LOG.info("Druid write task has been sent, and the response is {}", response);
  26. }
  27. }

close方法

关闭之前创建的各种目标数据源连接对象。

示例

  1. public void close() throws IOException {
  2. bulkProcessor.close();
  3. restClient.close();
  4. checkErrorAndRethrow();
  5. }

【关于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的更多相关文章

  1. 干货 | BitSail Connector 开发详解系列一:Source

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 BitSail 是字节跳动自研的数据集成产品,支持多种异构数据源间的数据同步,并提供离线.实时.全量.增量场景下全 ...

  2. Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节

    简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...

  3. wpf 客户端【JDAgent桌面助手】开发详解(四) popup控件的win8.0的bug

    目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...

  4. Eureka详解系列(四)--Eureka Client部分的源码和配置

    简介 按照原定的计划,我将分三个部分来分析 Eureka 的源码: Eureka 的配置体系(已经写完,见Eureka详解系列(三)--探索Eureka强大的配置体系): Eureka Client ...

  5. Handler详解系列(四)——利用Handler在主线程与子线程之间互发消息,handler详解

    MainActivity如下: package cc.c; import android.app.Activity; import android.os.Bundle; import android. ...

  6. PayPal 开发详解(四):买家付款

    1.点击[立即付款] 2.使用[个人账户]登录paypal  Personal测试帐号 3.核对商品信息 4.确认信息无误,点击[立即付款],提示付款成功,跳转到商家设置的URL 5.URL中包含pa ...

  7. 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码

    简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...

  8. Java源码详解系列(十二)--Eureka的使用和源码

    eureka 是由 Netflix 团队开发的针对中间层服务的负载均衡器,在微服务项目中被广泛使用.相比 SLB.ALB 等负载均衡器,eureka 的服务注册是无状态的,扩展起来非常方便. 在这个系 ...

  9. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

  10. Java源码详解系列(十)--全面分析mybatis的使用、源码和代码生成器(总计5篇博客)

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

随机推荐

  1. [C++]线段树 区间查询 单点修改

    线段树 区间查询 单点修改 算法思想 这个算法是用于数组的查询和修改 可以高效的进行查询修改 但是会增加内存的使用 本质上是一种 空间换时间 的算法 这个算法把一串数组无限二分 直到分的只剩下一个数据 ...

  2. 记录jdk17相对于jdk8增加的一下主要语法糖和新特性

    jdk17 发布已经好久了,作为java的长期支持版本,引入了许多有趣且实用的新特性.这些特性不仅提高了开发效率,还增强了语言的表现力和安全性.并且是SpringBoot 3.0以后版本的硬性要求,之 ...

  3. KMeans算法全面解析与应用案例

    本文深入探讨了KMeans聚类算法的核心原理.实际应用.优缺点以及在文本聚类中的特殊用途,为您在聚类分析和自然语言处理方面提供有价值的见解和指导. 关注TechLead,分享AI全维度知识.作者拥有1 ...

  4. Avalonia 实现跨平台的IM即时通讯、语音视频通话(源码,支持信创国产OS,统信、银河麒麟)

    在 Avalonia 如火如荼的现在,之前使用CPF实现的简单IM,非常有必要基于 Avalonia 来实现了.Avalonia 在跨平台上的表现非常出色,对信创国产操作系统(像银河麒麟.统信UOS. ...

  5. 【源码系列#02】Vue3响应式原理(Effect)

    专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核推荐 欢迎各位ITer关注点赞收藏 Vue3中响应数据核心是 reactive , reactive 的实 ...

  6. 自研、好用的ORM 读写分离功能使用

    Fast Framework 作者 Mr-zhong 代码改变世界.... 一.前言 Fast Framework 基于NET6.0 封装的轻量级 ORM 框架 支持多种数据库 SqlServer O ...

  7. 【scikit-learn基础】--『数据加载』之外部数据集

    这是scikit-learn数据加载系列的最后一篇,本篇介绍如何加载外部的数据集. 外部数据集不像之前介绍的几种类型的数据集那样,针对每种数据提供对应的接口,每个接口加载的数据都是固定的.而外部数据集 ...

  8. netty整合websocket(完美教程)

    websocket的介绍: WebSocket是一种在网络通信中的协议,它是独立于HTTP协议的.该协议基于TCP/IP协议,可以提供双向通讯并保有状态.这意味着客户端和服务器可以进行实时响应,并且这 ...

  9. [CF1601C] Optimal Insertion

    Optimal Insertion 题面翻译 题目大意 给定两个序列 \(a,b\),长度分别为 \(n,m(1\leq n,m\leq 10^6)\).接下来将 \(b\) 中的所有元素以任意方式插 ...

  10. [USACO2007FEB S] The Cow Lexicon S

    题目描述 Few know that the cows have their own dictionary with W (1 ≤ W ≤ 600) words, each containing no ...