目录:
- 什么是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... ,欢迎关注公众号"程序员笨鸥"一起交流学习吧!