以前搞离线数仓,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的资料请关注代码网其它相关文章!
发表评论