java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot接入Apache Doris

SpringBoot接入Apache Doris实时数仓的实战教学

作者:(farerboy)

Doris 高度兼容 MySQL 协议,所以在 Java 里直接用 MySQL 的 JDBC 驱动就能连,本文主要分了Spring Boot 接入 Doris 搞实时数仓的完整流程,希望对大家有所帮助

以前搞离线数仓,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");

避坑指南:

  1. 两张表的分桶数(Buckets)必须一模一样!
  2. 当集群发生扩缩容,数据在后台重分布的时候,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的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文