SpringBoot接入Apache Doris实时数仓的实战教学
作者:(farerboy)
以前搞离线数仓,T+1 的报表大家也忍了。但现在业务方动不动就要“秒级响应”、“实时大屏”,传统的 Hive + Spark 那套离线批处理架构根本扛不住。最近团队在重构实时分析平台,选型兜兜转转最后定了 Apache Doris。
这篇笔记主要记录一下我用 Spring Boot 接入 Doris 搞实时数仓的实战经验。不扯虚的,直接上干货,里面有不少是生产环境里踩出来的血泪坑。
1. 为什么选 Doris?
市面上 OLAP 引擎不少,ClickHouse、Hologres、Elasticsearch 我们都用过,但综合下来 Doris 算是把“好用”和“好维护”平衡得最好的。
说白了,它最大的优势就是架构极简。只有 FE(管元数据和解析)和 BE(管存储和计算)两个进程,不用像以前搞 Hadoop 那样配一堆 Zookeeper 和 HDFS,运维起来省心不少。而且它全面拥抱了向量化执行和 CBO 优化器,单表聚合和多表 Join 的性能都很能打。
跟 ClickHouse 比,Doris 的标准 SQL 支持得更好,Join 不会动不动就 OOM;跟 ES 比,它的存储成本低得多,聚合查询也快。对于既要搞实时报表,又要搞高并发点查的场景,Doris 确实是个香饽饽。
2. Spring Boot 连接池配置与避坑
Doris 高度兼容 MySQL 协议,所以在 Java 里直接用 MySQL 的 JDBC 驱动就能连。但 OLAP 场景和 OLTP 不一样,连接池配置如果直接照搬以前连 MySQL 的那套,绝对会出问题。
2.1 依赖引入
直接用 MySQL 驱动,配合 MyBatis-Plus 或者原生 JDBC 都行。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>2.2 HikariCP 调优(重点)
OLAP 查询通常比较慢,动辄几秒甚至几十秒,而且并发没 OLTP 那么高,但单次查询吃资源。连接池配置的核心是超时控制和连接保活。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://doris-fe-host:9030/your_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
hikari:
# 坑点:OLAP 查询慢,连接不能频繁重建。但 max-lifetime 必须小于 Doris 服务端的 wait_timeout(默认8小时)
# 建议设置为 3-4 小时,避免服务端把连接掐了,客户端还在用导致 Communications link failure
max-lifetime: 10800000
idle-timeout: 1800000
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
data-source-properties:
# 开启服务端预处理语句缓存
useServerPrepStmts: true
cachePrepStmts: true
prepStmtCacheSize: 250
# 注意:这个参数对 JDBC 的 executeBatch 有效,如果是走 Stream Load HTTP 接口则无效
rewriteBatchedStatements: true 全栈避坑:用 MyBatis-Plus 的时候,它的分页插件在 Doris 上偶尔会水土不服。如果是简单的分页还好,如果是复杂的分析场景,建议直接放弃 ORM 的分页插件,手写 Doris 原生的 LIMIT 和 OFFSET,或者在业务层做游标分页,能省去很多莫名其妙的报错。
3. 数据模型选型:别瞎建表
Doris 有三种数据模型,选错了后面查询慢得像蜗牛,想改都改不了。
3.1 Duplicate(明细模型)
适合日志、流水这种只追加、不更新的场景。数据原封不动存下来,导入性能最高。
3.2 Aggregate(聚合模型)
适合固定维度的报表,比如每天的 PV/UV。导入的时候自动把相同 Key 的数据聚合掉(Sum、Max 等)。好处是省存储空间,查询直接读结果,不用实时算。
3.3 Unique(主键模型)
适合需要更新的场景,比如 CDC 同步订单状态。
这里有个大坑:早期 Doris 的 Unique 模型用的是 MoR(Merge-on-Read),导入时直接追加,查询时再合并。数据一更新多了,查询性能就断崖式下跌。
现在 1.2 以后的版本,必须开 MoW(Merge-on-Write)。建表时千万记得加上 "enable_unique_key_merge_on_write" = "true",让它在导入时就把数据合并好,查询性能直接起飞。
CREATE TABLE dwd_order_info (
order_id BIGINT,
user_id BIGINT,
order_status TINYINT,
create_time DATETIME
)
UNIQUE KEY(order_id, user_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
"enable_unique_key_merge_on_write" = "true", -- 必须开 MoW
"replication_num" = "3"
);4. 实时数据怎么灌进去?
实时数仓,数据流入是核心。Doris 提供了几种方式,看场景选。
4.1 Stream Load:微批推送
通过 HTTP 接口把数据推给 Doris。如果是 Spring Boot 应用内部产生的微批数据,用这个很方便。
public void streamLoad(String tableName, List<Map<String, Object>> dataList) {
// 注意:FE 默认端口是 8030,BE 是 8040。推荐直连 BE 节点负载均衡
String loadUrl = String.format("http://%s:%s/api/%s/%s/_stream_load",
dorisBeHost, "8040", dbName, tableName);
String jsonData = JSON.toJSONString(dataList);
HttpPut put = new HttpPut(loadUrl);
put.setHeader("Expect", "100-continue");
put.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("root:password".getBytes()));
put.setHeader("Content-Type", "application/json");
put.setHeader("format", "json");
put.setHeader("strip_outer_array", "true");
put.setEntity(new StringEntity(jsonData, StandardCharsets.UTF_8));
// 执行请求并解析响应,检查 Status 是否为 Success
// 提示:如果数据量特别大,别在内存里拼 JSON,直接传文件流,不然容易 OOM
}
4.2 Routine Load:Kafka 持续订阅
如果数据已经写到 Kafka 了,直接用 Routine Load。Doris 自己起线程去消费 Kafka,零代码接入,最省事。
4.3 Flink-Doris-Connector:企业级标配
如果你们重度使用 Flink 做实时计算,直接上 Flink-Doris-Connector。它底层依赖 Doris 的两阶段提交(2PC),能完美支持 Exactly-Once 语义。
配置时注意 sink.batch.size 别设太小,建议 10000 左右,平衡一下延迟和吞吐。
5. 物化视图
物化视图就是把复杂的查询结果提前算好存起来。Doris 的物化视图分同步和异步两种。
同步物化视图只能搞单表,太局限。现在我们都玩异步物化视图,支持多表 Join,而且可以定时刷新,不耽误数据导入。
CREATE MATERIALIZED VIEW mv_dws_sales_daily
BUILD IMMEDIATE
REFRESH ASYNC -- 异步刷新,也可以配合外部调度系统
PARTITION BY RANGE(date_trunc('day', order_time)) ()
DISTRIBUTED BY HASH(store_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"auto_refresh_partitions_limit" = "3" -- 每次只刷新最近3个分区,省资源
)
AS
SELECT
date_trunc('day', o.order_time) as dt,
s.store_id,
SUM(o.pay_amount) as total_pay,
COUNT(DISTINCT o.user_id) as uv
FROM dwd_order_info o
JOIN dim_store s ON o.store_id = s.store_id
GROUP BY dt, s.store_id;最爽的一点是“透明改写”。业务代码里还是查 dwd_order_info 和 dim_store,Doris 的 CBO 会自动识别出这个查询能被物化视图满足,直接路由到物化视图上。业务代码一行不用改,查询速度从几秒变成几十毫秒。
6. 多表 Join 优化:Colocate Join 是神器
OLAP 场景下,多表 Join 最容易出性能问题。Doris 有 Broadcast、Shuffle 等策略,但最高效的是 Colocate Join。
原理很简单:如果两张表的分桶键(Distribute Key)一样,分桶数一样,且数据分布在相同的节点上,Join 的时候就在本地节点完成了,完全不需要网络 Shuffle。
-- 表 A
CREATE TABLE table_a (
id INT, user_id INT, val INT
) DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES ("colocate_with" = "group_user");
-- 表 B
CREATE TABLE table_b (
id INT, user_id INT, info STRING
) DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES ("colocate_with" = "group_user");避坑指南:
- 两张表的分桶数(Buckets)必须一模一样!
- 当集群发生扩缩容,数据在后台重分布的时候,Colocate 组会短暂失效。这时候查询可能会退化回 Shuffle Join,变慢一点。所以尽量在业务低峰期扩缩容。
另外,Doris 默认开启了 Runtime Filter,在 Hash Join 时会自动把过滤条件下推,不用我们手动去写那些优化 SQL,非常省心。
7. 资源隔离:防着业务方跑“大查询”
集群是公共资源,最怕某个业务方写了个没加分区条件的“大查询”,把集群 CPU 和内存吃光,导致核心报表出不来,这锅最后肯定甩给开发。
7.1 Resource Group(资源组)
Doris 1.2 引入了资源组,可以在 CPU 和内存层面做硬隔离。
CREATE RESOURCE GROUP rg_report FOR USER 'report_user' WITH CPU_SHARE = '20', MEM_LIMIT = '30%';
7.2 大查询拦截
对于高并发接口,一定要在 Spring Boot 层或者 Doris 层设置超时。
-- 限制单个用户的最大并发 SET GLOBAL max_user_connections = 50; -- 开启查询队列,超出的排队 SET GLOBAL query_queue_max_queued_queries = 100;
在代码里执行 SQL 时,也建议通过 SET query_timeout = 30 这种 Session 变量,把可能引发 OOM 的慢查询直接掐死,保护集群。
8. 数据更新与 Compaction 调优
做 CDC 实时同步,数据更新是常态。前面说了 Unique 模型要开 MoW。但在 MoW 模式下,后台的 Compaction(数据合并) 任务就成了关键。
如果更新极其频繁,后台合并的速度跟不上导入的速度,就会导致 Version 堆积。这时候查询不仅会变慢,甚至可能报错。
运维建议:写个监控脚本,盯着 BE 节点的 tablet compaction score。如果发现分数居高不下,说明 Compaction 滞后了。去 BE 的配置文件里,把 compaction_task_num_per_disk 和 base_compaction_num_threads_per_disk 调大一点,给合并任务多分配点线程。
9. BI 工具对接的最后一步
平台搭好了,得让业务方用起来。对接 Superset 和 DataGrip 时,有几个细节要注意。
9.1 Apache Superset
Superset 连 Doris 用的是 MySQL 协议。在 Superset 的 SQL Lab 里,务必把 Query Timeout 调大(建议 300s 以上),不然复杂的报表跑一半被超时掐了,业务方会以为系统有 Bug。另外,对于变化不频繁的宽表,开启 Superset 的数据缓存,能大幅降低 Doris 的压力。
9.2 DataGrip / DBeaver 卡死问题
开发同学用 DataGrip 连 Doris,经常会遇到“连上了,但是左侧树加载元数据卡死”的问题。
原因:客户端默认会扫描所有库、表、列的元数据,表一多,Doris 的元数据接口响应就慢。
解决:在 DataGrip 的数据源设置里,关掉 Introspect using JDBC metadata,或者在 Options -> Driver 中取消勾选 Introspect all schemas,只加载当前使用的 Schema,瞬间丝滑。
10. 集群运维:缩容千万别用 DROP
Doris 号称极简运维,但生产环境操作还是得谨慎。
在线扩容很简单,ALTER SYSTEM ADD BACKEND 加进去,数据会自动均衡。
但缩容节点时,千万、千万要用 DECOMMISSION,别用 DROP!
-- 安全下线,会先把数据迁移到其他节点,再移除 ALTER SYSTEM DECOMMISSION BACKEND "old_be_host:9050"; -- 直接删除,数据直接丢了,神仙也救不回来! -- ALTER SYSTEM DROP BACKEND "old_be_host:9050";
这是血的教训,大家操作前一定要看清楚命令。
写在最后
Doris 确实是个好引擎,MPP 架构和极简运维能帮团队省下不少头发。但它也不是银弹,如果数据模型选错了、Colocate Join 没配好、或者 Compaction 没调优,照样跑得慢。
搞实时数仓,三分靠工具,七分靠设计和调优。建议大家上线前多压测,上线后多盯监控。
以上就是SpringBoot接入Apache Doris实时数仓的实战教学的详细内容,更多关于SpringBoot接入Apache Doris的资料请关注脚本之家其它相关文章!
