Doris批量导入慢?Spring Boot整合Doris Routine Load是如何提升数据导入性能

目录:

  • 什么是Doris Routine Load?
  • 准备工作:创建Doris表
  • SpringBoot集成Doris Routine Load任务
  • SpringBoot发送数据到kafka
  • Doris Routine Load并发度调优
  • Doris表自动分区性能调优
  • 总结

在项目实践中,通过JDBC的insert into语法的方式插入数据,比如利用Mybatis-plus框架saveBatch方法写入数据的速度非常缓慢,一分钟也只能插入几百条数据。Doris作为优秀的OLAP数据库,官方也提供了实时数据高效写入的方案,即Routine Load方式写入数据;

什么是Doris Routine Load?

Routine Load是Doris提供的一种从Kafka中持续导入数据的功能。它通过FE节点提交一个长驻的导入任务,不断从Kafka中消费消息并写入Doris表中。

如上图所说,服务端应用只需要往kafka中写入数据,让Doris Routine Load任务自己去Kafka拉数据,负责后续的数据导入工作。这样做的优势有以下几个方面:

  • 解放应用端:业务只管往Kafka发数据,不用关心入库情况
  • 批量高效:Doris内部自动攒批写入,性能大幅提升
  • 支持Exactly-Once语义:保证数据不重不丢

准备工作:创建Doris表

我们首先创建一张用户操作日志记录表,表类型声明为DUPLICATE KEY明细模型表,使用动态分区,可以自动按天自动删除和创建分区。

sql 复制代码
CREATE TABLE IF NOT EXISTS user_log
(
user_id  BIGINT,
log_time DATETIME NOT NULL,
log_type VARCHAR(50) NOT NULL,
content JSON
) ENGINE=OLAP
DUPLICATE KEY(user_id, log_type, log_time)
PARTITION BY RANGE(log_time)()
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
"replication_num" = "1",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-60",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p"
);

SpringBoot集成Doris Routine Load

利用Spring Boot实现对Doris Routine Load的创建和任务状态的监控。我们可以利用JDBC或Spring Boot的DataSource数据源来操作Doris Routine Load任务。

  • DataSource获取数据库连接配置
java 复制代码
// 获取当前链接的Doris数据源
@Resource(name = "dorisDataSource")
private DataSource dorisDataSource;

/**
* 获取 Doris 数据库连接
*
* @return Connection 对象
  */
  private Connection getDorisConnection() throws SQLException {
  if (dorisDataSource == null) {        
  throw new SQLException("Doris DataSource 未配置");    
  }    
  return dorisDataSource.getConnection();
  }

/**
* 执行 SQL 语句
*
* @param sql 待执行的 SQL
* @return 是否执行成功
  */
  private boolean executeSql(String sql) throws SQLException {
  try (Connection conn = getDorisConnection();
  Statement stmt = conn.createStatement()) {
  stmt.execute(sql);
  return true;    
  }
  }
  • 创建Doris Routine Load任务

创建Doris Routine Load任务用于消费Kafka中的数据导入到Doris中。

java 复制代码
/**
* 创建 Routine Load 任务
* <p>
* 用于从 Kafka 持续加载数据到主键表中
*
* @param tableName 创建Doris Routine Load任务的目标数据库表名
* @param jobName   创建Doris Routine Load任务的任务名称,唯一
* @return 是否创建成功
* @throws SQLException
  */
  public boolean createRoutineLoad(String tableName, String jobName) throws SQLException {
  String sql = "CREATE ROUTINE LOAD IF NOT EXISTS " + jobName +
  "ON " + tableName +
  "COLUMNS(user_id, log_time, log_type, content) " +
  "PROPERTIES " +
  "(" +
  "    "desired_concurrent_number" = "3"," +
  "    "max_batch_interval" = "10"," +
  "    "max_batch_rows" = "200000"," +
  "    "format" = "json"" +
  ") " +
  "FROM KAFKA " +
  "(" +
  "    "kafka_broker_list" = "${kafka.brokers}"," +
  "    "kafka_topic" = "${kafka.topic}"," +
  "    "kafka_partitions" = "0,1,2"," +
  "    "property.group.id" = "user_behavior_group"" +
  ")";
  log.info("create routine load 执行 Routine Load 创建语句: {}", sql);
  return executeSql(sql);
  }
  • 暂停Doris Routine Load任务
java 复制代码
/**
* 暂停 Routine Load 任务
*
* @param jobName 任务名称
* @return 是否暂停成功
  */
  public boolean pauseRoutineLoad(String jobName) {
  if (!routineLoadEnabled) {
  log.warn("pause routine load 功能未启用");
  return false;
  }
  try {
  String sql = "PAUSE ROUTINE LOAD FOR " + jobName;
  log.info("pause routine load 暂停任务 {}", jobName);
  return executeSql(sql);
  } catch (Exception e) {
  log.error("pause routine load 暂停 Routine Load 任务异常", e);
  return false;
  }
  }
  • 恢复Doris Routine Load任务

暂停的任务可以通过resume routine load for jobName来恢复任务的执行。

java 复制代码
/**
* 恢复 Routine Load 任务
*
* @param jobName 任务名称
* @return 是否恢复成功
  */
  public boolean resumeRoutineLoad(String jobName) {
  if (!routineLoadEnabled) {
  log.warn("resume routine load 功能未启用");
  return false;
  }
  try {
  String sql = "RESUME ROUTINE LOAD FOR " + jobName;
  log.info("resume routine load 恢复任务 {}", jobName);
  return executeSql(sql);
  } catch (Exception e) {
  log.error("resume routine load 恢复 Routine Load 任务异常", e);
  return false;
  }
  }
  • 停止Doris Routine Load任务

停止任务可以用stop routine load for jobName语句实现,任务停止即意味着任务被删除了,如果需要恢复任务则重新执行create routine load创建任务即可。

java 复制代码
/**
* 停止 Routine Load 任务
*
* @param jobName 任务名称
* @return 是否停止成功
  */
  public boolean stopRoutineLoad(String jobName) {
  if (!routineLoadEnabled) {
  log.warn("stop routine load 功能未启用");
  return false;
  }
  try {
  String sql = "STOP ROUTINE LOAD FOR " + jobName;
  log.info("stop routine load 停止任务 {}", jobName);
  return executeSql(sql);
  } catch (Exception e) {
  log.error("stop routine load 停止 Routine Load 任务异常", e);
  return false;
  }
  }
  • 查询Doris Routine Load任务

我们可以通过查询任务状态State为RUNNING,且Progress在变化,说明任务正常执行中。

java 复制代码
/**
* 执行 SQL 并获取结果
*
* @param sql 待执行的 SQL
* @return 执行结果
  */
  private ResultSet executeSqlAndGetResult(String sql) throws SQLException {
  try (Connection conn = getDorisConnection();
  Statement stmt = conn.createStatement()) {
  return stmt.executeQuery(sql);
  }
  }

SpringBoot发送数据到kafka

java 复制代码
/**
* 推送Kafka消息
*
* @param userLog 推送的消息
  */
  public void produce(UserLog userLog) {
  String message = JSON.toJSONString(userLog);
  Properties properties = new Properties();
  //设置Kafka服务器地址
  properties.put("bootstrap.servers", "localhost:9092");
  //设置数据key的序列化处理类
  properties.put("key.serializer", StringSerializer.class.getName());
  //设置数据value的序列化处理类
  properties.put("value.serializer", StringSerializer.class.getName());
  KafkaProducer<String, Object> kafkaProducer = new KafkaProducer<>(properties);
  // 推送kafka消息
  // userId作为消息key,用来保证同一个用户的行为数据进入同一个Kafka分区保证消息消费的顺序性
  kafkaProducer.send(new ProducerRecord<>("user_log_topic", userLog.getUserId(), message));
  }

Doris Routine Load并发度调优

Routine Load的并发度受多个参数影响:

  • 任务级别参数(创建任务时设置)
    • desired_concurrent_number:期望并发任务数,建议设置3~5个
    • max_batch_rows:批量导入参数
    • exec_mem_limit:内存限制,可根据Doris BE节点内存合理设置,通常为2~4GB
    • max_error_number:错误容限,可根据实际情况调整,建议不超过 1000
    • max_batch_interval:设置批量任务间隔时间,可以设置合理的任务间隔时间(如20s~30s)避免任务过于频繁
  • FE 配置(fe.conf)
    • max_routine_load_task_concurrent_num:单个作业最大并发数,默认 5
    • max_routine_load_task_num_per_be:每个 BE 最大并发任务数,默认 5
  • BE配置(be.conf)
    • routine_load_thread_pool_size:任务线程池大小,默认 10

并发度计算公式:

scss 复制代码
单作业实际并发 = min(
Kafka分区数,
desired_concurrent_number,
max_routine_load_task_concurrent_num
)

如果Kafka有3分区,desired_concurrent_number=5,max_routine_load_task_concurrent_num=5,那么实际的并发是3。因此可以通过增加kafka分区和调整desired_concurrent_number等参数能有效提高并发能力。

Doris表自动分区性能调优

使用自动分区表相比于非自动分区表,数据导入速度会比较慢,是因为自动分区在导入时需要进行比如创建分区等额外操作,从而影响发送的间隔。可以通过参数的调整来解决:

sql 复制代码
-- 关闭sink node前移路径,让自动分区的参数生效
SET enable_memtable_on_sink_node = false;
-- 创建任务时调整发送间隔
PROPERTIES
(
"olap_table_sink_send_interval_microseconds" = "10000",
"olap_table_sink_send_interval_auto_partition_factor" = "0.001"
)

通过olap_table_sink_send_interval_auto_partition_factor=0.001参数缩短自动分区的等待时间,来提升导入速度。

总结

Routine Load 的核心优势:

  • 与Kafka无缝集成,天然支持流式数据
  • Doris自动管理导入任务,应用层零负担
  • 支持 Exactly-Once 语义,保证数据一致性
  • 通过并发调优,轻松达到万级TPS

适用场景:

  • 实时日志/用户行为采集
  • 物联网时序数据
  • 业务数据库Binlog同步

写在最后

文章来自mp.weixin.qq.com/s/4LRu5XDbY... ,欢迎关注公众号"程序员笨鸥"一起交流学习吧!

相关推荐
用户2181697049301 小时前
golang 并发 goroutine sync.Lock atomic WaitGroup 协程通信(共享数据,channqel消息)channel
后端
Reart1 小时前
从0解构tinyweb项目(十三)--剩余Handler自读验证(未完成版)
后端
Gopher_HBo1 小时前
接入层Nginx
后端
IT_陈寒2 小时前
Vite热更新把我整不会了,原来还要这样配!
前端·人工智能·后端
码界筑梦坊2 小时前
153-基于FLask的英国希思罗机场天气数据可视化分析系统
python·信息可视化·数据分析·flask
Gauss松鼠会2 小时前
GaussDB(DWS) SQL性能问题案例集
java·数据库·经验分享·spring boot·后端·sql·gaussdb
霸道流氓气质2 小时前
Spring Boot 分页查询接口设计与实现 —— 技术总结与完整示例
java·spring boot·后端
赴前尘3 小时前
Go 语言实现 TOTP 双因素认证完整指南
开发语言·后端·golang