Apache Doris 实时 OLAP 建模实践
需要"用户行为明细 + 亚秒级聚合查询"时,MySQL 撑不住、ClickHouse 的运维与关联查询又麻烦,很多团队会选 Apache Doris。它兼容 MySQL 协议、支持标准 JOIN、写入即时可见,确实填补了中间地带。但 Doris 的性能高度依赖建表模型的选择,模型选错就退化成一个更慢的 MySQL。
一、三种数据模型是所有设计的地基
| 模型 | 语义 | 典型场景 | |---|---|---| | Duplicate Key | 原样存明细,不去重不聚合 | 明细查询、事件宽表 | | Aggregate Key | 按 key 预聚合,value 列按聚合函数合并 | 固定维度报表 | | Unique Key | 按 key 覆盖写(Merge-on-Write) | 订单、用户等需纠正的实体 |
选错的代价很直接:明细场景用 Aggregate 会丢数据;实体场景用 Duplicate 会有重复行;报表场景用 Duplicate 则每次查询都在实时聚合全量。
CREATE TABLE dwd_evt (
evt_time DATETIME NOT NULL,
user_id BIGINT,
evt_type VARCHAR(32),
page VARCHAR(128),
props JSON
)
DUPLICATE KEY(evt_time, user_id, evt_type)
PARTITION BY RANGE(evt_time) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("replication_num" = "3");
CREATE TABLE dwd_order (
order_id BIGINT NOT NULL,
user_id BIGINT,
status TINYINT,
amount DECIMAL(18,2),
updated_at DATETIME
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true"
);
merge_on_write 是 Doris 相较早期版本的关键改进:读取时不再合并 delete marker,点查性能大幅提升。老库升级要显式确认这个属性是否开启。
二、分区与桶数要匹配数据量
Doris 的物理布局是"分区 → Tablet(桶)→ 副本",桶数直接决定并行度,也是 Doris 最重要的一个手工决策。
单桶目标大小 ≈ 1~10GB(热数据取下限)
桶数 = 分区预估数据量 ÷ 单桶目标大小
一天 200GB 明细、单桶 5GB 就是 40 个桶。经验上把单个 BE 节点的 tablet 数控制在 200 到 400 之间:太少浪费并行度,太多增加元数据与调度压力。
分区交给动态分区自动滚动:
ALTER TABLE dwd_evt SET ("dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-60",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "32",
"dynamic_partition.replication_num" = "3");
高频错误是分桶键选低基数列(例如 evt_type),造成严重倾斜:一个桶 100GB、其他桶 100MB,查询永远在等一台机器。要选高基数且分布均匀的列,如 user_id、order_id。
三、排序键与物化视图决定查询性能
Doris 的 key 列既是模型键也是排序键,数据在其上物理有序,构成前缀索引。规则严格:
- 前缀索引只能用 key 列从左到右的连续前缀;
- 有效长度约 36 字节,超出部分不建索引;
- 高频过滤列必须放在 key 最左。
-- 查询模式:WHERE evt_time BETWEEN ? AND ? AND evt_type = ? AND user_id = ?
DUPLICATE KEY(evt_time, evt_type, user_id)
需要另一种排序维度时用 rollup:
ALTER TABLE dwd_evt ADD ROLLUP r_user (user_id, evt_time, evt_type, page);
Rollup 是按不同排序键物化的一份副本,存储会翻倍,每个 rollup 都要单独评估收益。是否命中看执行计划里有没有 rollup: r_user。
四、写入方式与版本积压
Doris 是 MPP 列存,写入按版本攒批后可见(默认约 3 秒一个版本)。四类入口各有适用面:
| 方式 | 适用 | 说明 | |---|---|---| | Stream Load | 微批(数百 KB 到数 GB) | HTTP,最常用 | | Routine Load | Kafka 持续消费 | 内置 checkpoint | | Broker Load | 对象存储批量导入 | 全量初始化 | | Insert Into | 小批、单行 | 高频单行写入是反模式 |
CREATE ROUTINE LOAD db.load_kafka_evt ON dwd_evt
PROPERTIES (
"format"="json",
"jsonpaths"="[\"$.t\",\"$.uid\",\"$.type\",\"$.page\",\"$.props\"]"
)
FROM KAFKA (
"kafka_broker_list" = "kafka:9092",
"kafka_topic" = "events",
"property.group.id" = "doris-load",
"kafka_partitions" = "0,1,2,3",
"kafka_offsets" = "OFFSET_BEGINNING"
)
PROPERTIES ("desired_concurrent_number" = "4", "max_batch_interval" = "10");
两个高频事故:小批量高频 Stream Load 会造成版本积压并报 -235 too many versions,解法是攒更大批次(10 秒以上或约 100MB)而非调大 max_tablet_version_num;Routine Load 的并发数不应超过 Kafka 分区数,配多了无意义还会拖慢 checkpoint。
版本数是必须监控的量:SHOW TABLET FROM dwd_evt; 里的 VersionCount 只增不减就要立刻查写入端。
五、Colocate Join 要提前规划
两表按相同分桶键与桶数建表时,join 不发生数据 shuffle,直接本地关联:
CREATE TABLE dwd_order (...) DISTRIBUTED BY HASH(user_id) BUCKETS 16;
CREATE TABLE dwd_user_profile (...) DISTRIBUTED BY HASH(user_id) BUCKETS 16;
ALTER TABLE dwd_order SET ("colocate_with" = "user_key_group");
ALTER TABLE dwd_user_profile SET ("colocate_with" = "user_key_group");
约束很硬:分桶列类型、桶数、副本数必须完全一致。不满足时优化器退化为 Broadcast Join(小表)或 Shuffle Join(大表)。这决定了"订单表和用户表在建表时就要按同一个键分桶"——事后重做分桶的代价非常高。
六、观测与四条告警
SHOW PROC '/cluster_health';
SHOW BACKENDS; -- CapacityUsedPct、TabletNum、AvgIOPercent
SHOW ROUTINE LOAD; -- 消费积压
SHOW TABLET FROM dwd_evt; -- VersionCount
FE 与 BE 都暴露 /metrics,接入 Prometheus 后配四条告警:
- BE 磁盘使用率超过 75%。Doris 有空闲比例阈值,超过会拒绝调度新 tablet,表现为写入莫名卡住;
- 单表 VersionCount 呈持续增长趋势;
- Routine Load 消费积压超阈值;
- 查询耗时 P99 超过 5 秒。
第四条要特别小心:它通常说明排序键或分桶设计与查询模式不匹配,正确动作是回去改建表模型,而不是加 BE 节点。
小结
Doris 的性能来自四件事,按影响大小排序:选对数据模型(明细 Duplicate、实体 Unique 加 MoW、报表 Aggregate)、分桶键选高基数均匀列且桶数与分区数据量匹配、把高频过滤列放进排序键最左、写入攒批并监控版本积压。Colocate Join 需要在建表阶段就规划分桶,事后代价大。遇到查询慢,第一反应应该是 EXPLAIN 看是否命中 rollup、是否发生 shuffle,而不是加机器。Doris 属于"设计对了很快、设计错了永远不够快"的那类系统。
评论(0)
登录 后参与评论。