如果你的业务库已经从几十 GB 涨到了几百 GB,报表平台拉一次数据要等半分钟,BI 页面偶尔还会因为一个慢查询把线上 MySQL 拖到 CPU 打满——那你大概率已经在网上反复比较过 Doris、ClickHouse、StarRocks 到底应该选哪一个。我这套选型落地过程打算分两篇来写,本文是第一篇,只讲前半截:为什么最终选了 Apache Doris + Apache Superset 这套开源组合,以及如何用最低的硬件成本,把实时数仓和可视化层完整串联起来。
这篇内容偏实战,适合正在做数据平台选型、又要自己动手搭建报表系统的同学参考。第二篇会专门讲 Doris 查询优化、物化视图、权限隔离,以及 Superset 企业级图表和大屏的深度配置。
1. 这套组合解决的是“BI 慢”还是“数仓重”的问题
1.1 传统方案的三类典型痛点
先说我遇到的实际场景。业务底层是 MySQL,订单表、支付流水表单表过亿,多表关联时 MySQL 的索引优势基本失效。当时报表组最常干的事情是从 MySQL 拉全量数据到 Excel,或者在 BI 工具里直连 MySQL 跑大查询。数据库 DBA 几乎每周都要处理一次慢查询告警。
传统方案通常有三类解法,但各有各的难受:
- 直连 MySQL:简单,但不敢上复杂查询,报表并发稍高就把业务库拖垮;
- 离线数仓:用 Hive + Spark 做 T+1,开发链路长、组件多,一个人运维不过来;
- 商业 BI + 独立数仓一体机:好用,但 License 费用对小团队来说非常劝退。
这个阶段的核心矛盾不是“不会写 SQL”,而是缺少一个“能扛住查询压力的中间层”。数据量到了百 GB 级,MySQL 的查询计算和存储引擎已经不太适合做分析负载了。
1.2 为什么不是 ClickHouse,而是 Doris
很多人第一反应是 ClickHouse,我也在选型时认真测过。ClickHouse 的列式存储和向量化执行确实快,但它在“实时更新”这个场景上有天然短板:虽然 24.x 版本开始有 ReplacingMergeTree 轻量删除和更新,但合并行为是异步的,你很难保证刚写入主键覆盖后立刻查到的是新值,而且高频更新会造成后台 merge 压力,需要投入额外精力做分区裁剪和合并调优。
Apache Doris 在我这个场景里更顺手,核心原因有三点:
第一,Doris 原生支持 Unique Key 模型,写入时按主键自动去重,读时返回最新版本,实时覆盖更新是开箱即用的能力;第二,Routine Load 能直接消费 Kafka,不需要额外部署一套 Stream 处理框架,链路短、组件少;第三,Doris 兼容 MySQL 协议,团队迁移成本低,什么 join、group by、窗口函数都能直接跑。
| 对比项 | Apache Doris | ClickHouse | 直连 MySQL |
|---|---|---|---|
| 实时 Upsert | 原生 Unique Key | 需依赖 ReplacingMergeTree 异步合并 | 天然支持但扛不住分析查询 |
| 实时链路 | 内建 Routine Load 直接消费 Kafka | 需自建 Kafka Engine 或用第三方同步组件 | 无 |
| MySQL 协议兼容 | 兼容,应用迁移成本低 | 不兼容,需要专用客户端 | 本身就是 MySQL |
| 运维复杂度 | FE + BE 两个角色 | ClickHouse 集群 + ZK(或 ClickHouse Keeper) | 最低 |
| 扩展成本 | 加 BE 节点即可水平扩展 | 需要手工处理分片权重 | 分库分表困难 |
1.3 什么场景不适合这套组合
Doris + Superset 不是银弹。如果你要做的是全文检索,Doris 的倒排索引虽然新版在增强,但和 Elasticsearch 比仍有差距;如果你需要事务型 OLTP,Doris 完全没有事务和行级锁能力;如果只是给老板看周报月报,T+1 就够,那直接做成定时快照更省心。
这套组合真正擅长的是:大量明细数据、持续写入、需要秒级可见、要求报表查询并发稳定的在线分析场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 容量规划与数据链路:先想清楚再动手
2.1 整体数据流设计
部署之前先把技术选型确认掉,第一条链路就是:
业务库 MySQL → Canal 监听 Binlog 解析变更 → 写入 Kafka Topic → Doris 通过 Routine Load 消费 Kafka → Superset 连接 Doris 做图表展示
为什么不用 Flink CDC?因为这套平台的第一诉求是“低开销”。Canal 本质就是一个 Java 进程,1GB 内存足够跑;Kafka 可以复用公司已有的集群;Routine Load 是 Doris 内置功能,不需要额外部署服务。而 Flink CDC 到 Doris 需要单独维护一个 Flink 集群,任务本身是 JM + TM 常驻进程,小规模团队遇到状态后端改配置、Checkpoint 超时这些问题,排查成本相当高。
2.2 容量估算参考
先把规模算清楚再买机器。这里我给一个自己常用的参考表,按“数据总量”和“并发查询数”两个维度估算:
| 数据规模 | FE 建议配置 | BE 节点建议 | 总内存建议 | 适用场景 |
|---|---|---|---|---|
| 100GB 以下 | 4C8G x 1 | 1 台 8C32G | 40G | 单体报表、团队内部看板 |
| 100GB ~ 1TB | 4C16G x 1 | 2~3 台 8C32G | 80G~128G | 部门级实时分析 |
| 1TB ~ 5TB | 8C32G x 3 | 4~6 台 16C64G | 256G+ | 全公司实时数据平台 |
拿订单场景举例,如果一天新增 500 万条订单明细,保留 90 天,数据量约 4.5 亿行,单行按 300 字节算,原始数据约 13GB,加上副本和中间合并空间,按 2 倍冗余估算,30GB 存储足够,一台 BE 就能扛住,但为了查询并发稳定,我一般建议至少两台。
2.3 版本选择建议
Doris 选 2.1 系列 LTS 版本,不要追最新的大版本,生产环境稳定优先。我实测用的是 2.1.7,官方推荐生产都用 LTS 系列。
Superset 版本差异比较大,连接 Doris 有两个方案:一是旧版本用 MySQL 兼容协议连接,二是 Superset 4.0 以上原生支持 Apache Doris 方言。建议装 Superset 4.x 以上,直接用 Doris 原生的 SQLAlchemy 方言,SQL 下推更彻底。Python 环境建议 3.10 或 3.11,太新的 3.13 容易踩第三方依赖兼容坑。
3. Doris 集群初始化:FE 与 BE 的第一次握手
3.1 下载与目录规划
从 Apache 官网下载 Doris 2.1.x 的二进制包,解压到 /data/doris。如果你拿到的机器只有一块数据盘,也需要把 FE 元数据目录、BE 数据目录规划成独立文件夹,避免日志把磁盘写满。
bash复制tar -xzvf apache-doris-2.1.7-bin-x64.tar.gz -C /data/doris
cd /data/doris
mkdir -p fe/meta be/storage
这一步别偷懒。Doris 的 FE 负责元数据管理,元数据损坏整个集群就废了;BE 的数据目录设计不合理,后期数据均衡和扩容都要吃苦头。
3.2 FE 启动与核心配置项
FE 的配置在 fe/conf/fe.conf。小集群最重要的几个参数:
properties复制# 监听端口,默认 9030,应用访问用这个
query_port=9030
# Web 管理页面端口
http_port=8030
# FE 元数据目录
meta_dir=/data/doris/fe/meta
# JVM 内存,建议 4G~8G,不要超过物理内存的三分之一
JAVA_OPTS="-Xmx8192m -Xms8192m"
启动 FE:
bash复制cd /data/doris/fe
./bin/start_fe.sh --daemon
启动后用 mysql -h fe_host -P 9030 -uroot 登录,能进说明 FE 正常。
3.3 BE 节点注册与状态检查
BE 的配置在 be/conf/be.conf:
properties复制# BE 数据目录,可写多个,逗号分隔
storage_root_path=/data/doris/be/storage
# BE 可用内存上限比例,默认 80%
mem_limit=80%
# 心跳端口,默认 9050
heartbeat_service_port=9050
启动 BE 后用 MySQL 客户端把 BE 节点加进集群:
sql复制ALTER SYSTEM ADD BACKEND "be_host:9050";
-- 用下面语句确认状态:
SHOW BACKENDS;
看到 Alive 字段为 true,说明 BE 已经和 FE 完成心跳握手。
3.4 初始账号安全设置
Doris 初始化时的 root 账号默认无密码,且只有 root 能创建表和用户。这里要养成习惯,第一时间创建业务账号:
sql复制CREATE USER 'doris_admin' IDENTIFIED BY '这里写强密码';
GRANT ALL ON *.* TO 'doris_admin';
权限体系等第二篇再展开,第一篇先保证能安全访问。
4. 实时管道:Canal 到 Kafka 再到 Routine Load 的轻量实现
4.1 为什么选 Canal 而不是直接写业务代码双写
双写在应用里改代码,侵入性强,而且容易漏写、顺序错乱。Canal 伪装成 MySQL 从库拉 Binlog,业务完全无感知,解析后的 JSON 通过 MQ 解耦,这套模式是实时数仓里最成熟的组合之一。
Canal 部署方式选 single 模式即可,解压 canal.deployer 包后改两个配置文件。
conf/canal.properties 里主要改端口和 MQ 相关配置:
properties复制canal.serverMode = kafka
canal.mq.servers = kafka_host1:9092,kafka_host2:9092
kafka.bootstrap.servers = kafka_host1:9092,kafka_host2:9092
conf/example/instance.properties 里配置监听源:
properties复制canal.instance.master.address = mysql_host:3306
canal.instance.dbUsername = canal_user
canal.instance.dbPassword = 这里写密码
canal.instance.filter.regex = shop_db\\..*
MySQL 端要先创建同步账号,并开启 Binlog,格式必须为 ROW,否则拿不到变更前后完整镜像。
4.2 Kafka Topic 准备与字段口径
Topic 的命名我用和库表一致的规则,比如 shop_db_orders。分区数建议与 Doris 表的 Bucket 数保持倍数关系,我用 3 分区对应 6 个 Bucket,后续消费并行度更容易对齐。
消息体是 Canal 输出的 JSON 结构,大概长这样:
json复制{"data":[{"order_id":"1001","amount":"99.00","status":"1"}],"type":"UPDATE","table":"orders","ts":1710000000}
实际导入 Doris 时,只需要关心 data 数组里字段,无需关注 type。如果业务上的删除操作也要同步,可以在 Canal 侧做过滤转换,或者直接让 Doris 建表的 Unique Key 模型配合导入时的 DELETE ON 标记处理,这个我在第二篇再展开说明。
4.3 Doris 建表:Unique Key 模型的关键作用
实时同步订单场景,最怕主键重复。Doris 的 Unique Key 模型读时合并,主键相同的数据行只在查询时返回最新一条,这样 Canal 发来的 UPDATE 消息天然具备了覆盖更新的语义。
建表 SQL 示例:
sql复制CREATE TABLE shop_db.ods_orders (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(12,2),
status TINYINT,
pay_time DATETIME,
create_time DATETIME
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 6
PROPERTIES (
"replication_num" = "2"
);
注意单机部署时 replication_num 必须设为 1,因为我们只有一个 BE;双机以上可以设为 2。这一点不提前注意,建表就会报错。
4.4 Routine Load 创建:一条 SQL 完成 Kafka 消费
建好表后,创建 Routine Load 任务:
sql复制CREATE ROUTINE LOAD shop_db.ods_orders_load ON ods_orders
PROPERTIES (
"desired_concurrent_number" = "3",
"max_batch_interval" = "20",
"format" = "json",
"jsonpaths" = "[\"$.order_id\",\"$.user_id\",\"$.amount\",\"$.status\",\"$.pay_time\",\"$.create_time\"]"
)
FROM KAFKA (
"kafka_broker_list" = "kafka_host1:9092,kafka_host2:9092",
"kafka_topic" = "shop_db_orders",
"kafka_partitions" = "0,1,2",
"kafka_offsets" = "OFFSET_BEGINNING"
);
几个参数说明一下:
desired_concurrent_number:消费并行度,等于最终分配给任务的最大子任务个数,分区数不够时设太大没用;max_batch_interval:积攒多久攒一批导入,20 秒差不多,既要实时性又不要小批量太频繁;jsonpaths:如果不写,Doris 要求 JSON 字段名和表列名完全一致,所以我建议显式声明,字段变化时排查也方便。
创建任务后用下面的 SQL 检查任务状态:
sql复制SHOW ROUTINE LOAD FROM shop_db \G
重点看 State 字段,是 RUNNING 且 ErrorRows 数量不增长,就说明链路已经通了。
4.5 验证实时性:从业务库写入到 Doris 可见
链路全部搭好后做一次端到端验证。先在业务库插一条测试记录,然后登录 Doris 查询:
sql复制SELECT * FROM shop_db.ods_orders WHERE order_id = 99999;
实测结果:Canal 解析 Binlog 到 Kafka 的延迟基本在 200ms 以内,Routine Load 攒批 20 秒一次,所以从业务写入到 Doris 查询可见,一般不超过 30 秒,高峰期 batch 满 10 万条也会立即触发导入,延迟更低。
5. Superset 接通 Doris:连接串、缓存与第一个实时图表
5.1 Superset 安装:推荐虚拟环境方式
Superset 官方提供 Docker 镜像,但连接 Doris 的原生方言需要额外装驱动,国内拉镜像也不方便,我更推荐用 Python 虚拟环境安装:
bash复制python3.11 -m venv superset-env
source superset-env/bin/activate
pip install "apache-superset[mysql]"
superset db upgrade
superset fab create-admin
superset init
superset run -h 0.0.0.0 -p 8088 --with-threads --reload
安装时遇到 sqlparse 版本冲突是老问题,建议直接固定为 0.4.4。admin 账号创建时可以顺便把密码设置成公司密码规范要求的长度。
5.2 连接 Doris 的两种方式
在 Superset 的 Data -> Databases 页面,选择 Apache Doris 数据库类型,填写连接串:
- Superset 4.0 以上,推荐原生驱动形式:
text复制doris://doris_admin:密码@fe_host:9030/shop_db
- 旧版本用 MySQL 兼容协议:
text复制mysql+pymysql://doris_admin:密码@fe_host:9030/shop_db?charset=utf8mb4
两种方式都能连通。原生驱动对 Doris 的函数、窗口语法翻译更准确,建议能用原生就不用 MySQL 兼容模式。
5.3 时间字段时区偏移这个坑
配置完数据源,创建图表时最容易踩的一个坑是时间偏移 8 小时。原因是 Superset 默认以 UTC 为时区,Doris 返回的时间戳被识别后自动转换,导致图表横轴比实际晚 8 个小时。
解决办法:在连接串中显式声明时区(如果能加参数的情况下),或者在 Doris 侧把会话时区固定为东八区:
sql复制SET GLOBAL time_zone = 'Asia/Shanghai';
然后把 Superset 里的 PyJWT 和使用方的浏览器时区统一设为 Asia/Shanghai。
5.4 创建第一个实时图表
数据源连接成功后,在 Superset 里选择表 ods_orders,新建 Chart,选用 Big Number 组件,把 order_id 作为 COUNT,时间列选 create_time。
这里要重点提一下:Superset 默认的查询缓存是 500 秒。也就是说,即使 Doris 里已经有最新数据,Superset 图表看到的还是五分钟前的旧结果。
需要进入 Database 的高级设置,把缓存过期时间改成 0 或 60 秒:
在 Database 编辑页面 -> Advanced Features -> 过期时间设置,全局写 0 表示关闭缓存,实时图表建议设置 60 秒即可。
5.5 验证图表刷新能力
保存 Chart 后回到 Dashboard,拖入刚才的图表,业务库再插一条新记录,同时打开浏览器控制台观察网络请求,正常情况 30 秒内图表数字会刷新。如果你的业务对“秒级看到数据”有硬要求,60 秒缓存改成 0 即可,代价是所有查询都直击 Doris,并发高时需评估 FE 压力。
6. 联调阶段的失败记录:六个坑和对应解法
6.1 踩坑清单
这个环节我整理了一张问题对照表,全是联调时真实遇到过的:
| 问题现象 | 根本原因 | 解决动作 |
|---|---|---|
| Routine Load 任务变 PAUSED | JSON 字段解析失败,比如 BigDecimal 类型字段出现非数字字符 | SHOW ROUTINE LOAD 查看失败信息,修正 jsonpaths 后 RESUME 任务 |
| Superset 图表比实际慢 8 小时 | Superset 默认 UTC,Doris 会话时区未设置 | Doris 执行 SET GLOBAL time_zone,并同步确认 Superset 时区 |
| BE 一直显示 Unknown | FE 到 BE 的心跳端口 9050 不通 | 检查云安全组和本机防火墙,放行 9050/9060/8060/8030/9030 端口 |
| 导入大批量数据时 BE 卡死 | BE 内存被 import 占满 | be.conf 里调整 mem_limit 从 80% 降到 60%,并限制 max_download_rate_limit |
| Superset 刷新图表没变化 | Database 缓存 500 秒没改 | 在 Database 高级属性里将缓存过期时间改为 60 秒 |
| 登录 Doris 后无法创建表 | 用的是普通账号而非 root/管理员 | 检查用户权限,首次练习请直接用 root 或先执行 GRANT 授权 |
6.2 一条亲测稳定的配置文件参考
最后给出一个配置组合,供直接参考使用。这套组合我在 2 台 8C32G 的机器上稳定运行了两个多月,实时报表和临时查询都没有出现明显瓶颈:
- FE 1 台:
query_port=9030,JVM-Xmx8g,meta_dir=/data/doris/fe/meta - BE 1 台:
storage_root_path=/data/doris/be/storage,mem_limit=70%,heartbeat_service_port=9050 - Routine Load:
desired_concurrent_number=3,max_batch_interval=20 - Superset:Python 3.11 虚拟环境,连接使用
doris://原生驱动,数据库缓存过期时间 60 秒
在这套配置下,亿级订单明细表的聚合查询响应时间基本维持在 1 秒以内,周报月报类大范围查询也可以压缩到 5 到 10 秒。
6.3 一点个人体会
很多人搭建实时平台时总想把 Flink、Doris、Superset、Kafka、Canal 全部环节都用上,但组件越多,故障面越大。如果你所在的团队规模不大、数据量在 TB 级以下,完全可以先用“Canal + Kafka + Routine Load”这条轻链路跑起来,等遇到性能瓶颈再逐步引入复杂组件。
这套平台后续能扩展的方向不少:Doris 的物化视图可以加速预聚合查询、分区时间策略决定冷热数据怎么迁移、Superset 的角色权限可以对接企业 LDAP。下一篇我再把这些内容详细展开。至少到现在,这套低开销组合已经帮我省掉了大半的报表取数工作,算是投入产出比相当高的一次技术选型了。
