《通过SeaTunnel 把 MySQL 的数据搬进 Doris》小白学习系列
SeaTunnel + Doris + MySQL-CDC 数据同步故障排查个人笔记
前面几篇把"怎么同步、故障排查"讲完了,这篇补的是"同步过来之后"的问题:表会不会爆?Doris 到底比 MySQL 快多少?数据在 Doris 里是怎么切分存放的?
先说结论:MySQL 的瓶颈是"数据量" ------单表行数到了一个量级,性能就滑坡,这是结构决定的;Doris 的瓶颈是"元数据规模"------单表行数本身几乎不是问题,真正卡住它的是表切得太碎、分区太多,把 FE(管理节点)的内存撑爆了。一个被数据量限制,一个被"管理开销"限制。记住这句话,下面的表格就都好懂了。
一、MySQL 的瓶颈在哪
MySQL 单表的"2000 万行"你应该听过。先澄清:这不是 MySQL 代码里写死的限制,是行业经验值。理论上 InnoDB 单表能存到 TB 级,但过了某个量级,体验会断崖式下降。滑坡的原因有四个:
-
B+ 树变深。MySQL 靠 B+ 树索引查数据,2000 万行左右正好是索引从 3 层变 4 层的临界点,每次查询多一次磁盘 IO,点查延迟肉眼可见地变慢。
-
内存装不下热数据。buffer pool(缓冲池)就那么大,表大了命中率下降,查询开始频繁读磁盘。
-
DDL 变成灾难。给一张 5000 万行的表加个字段,锁表几十分钟,这期间业务可能直接不可用。
-
备份、恢复、碎片整理全都变慢,运维窗口越来越难挤。
MySQL 单表容量参考表
| 单表行数 | 状态 | 你会观察到什么 |
|---|---|---|
| 500 万以内 | 舒适区 | 读写都快,索引高效,基本不用操心 |
| 500 万 ~ 1000 万 | 正常 | 还扛得住,开始留意慢查询 |
| 1000 万 ~ 2000 万 | 预警区 | 报表类查询明显变慢,该做容量评审了 |
| 2000 万以上 | 高危区 | 慢查询频发、加字段锁表、备份超时,考虑分表/分区/归档 |
| 5000 万以上(分析型查询) | 基本没法用 | 一条 GROUP BY 跑几分钟很正常 |
再次强调:这个表说的是"分析型查询"的体验 。如果是纯点查(按主键查一行),MySQL 到几千万行依然很快------它就是为这个生的。它扛不住的是"扫一大片、算一堆"。
二、Doris 的瓶颈在哪
先看几个真实生产案例,感受一下它的量级:
| 案例 | 规模 |
|---|---|
| 浩瀚深度 | 单表 13PB、534 万亿行,日均导入 145TB,117 节点集群 |
| 小米 | 单表日增 120 亿行,PB 级存储,单集群日均 2 万+ 分析查询 |
| 美团 | 数十 PB 级统一 OLAP,支持百亿级数据量的单 SQL 查询 |
所以"一张表能存多少行"这个问题,对 Doris 来说基本没有有意义的答案------你大概率先用不到它的行数上限。
那它到底会被什么卡住?
| 瓶颈点 | 说明 | 症状 |
|---|---|---|
| Tablet 数量(真正的上限) | Doris 把表切成 tablet 分布到各节点,单集群 tablet 总数约 100 万个是经验上限,受 FE 内存制约 | FE 内存持续飙高、元数据同步变慢、FE 冷启动很久 |
| 分区切太碎 | 按天分区 + 大量表 + 大量分桶,tablet 数会爆炸式增长(这笔账第三章算给你看) | 同上 |
| FE 单点元数据压力 | 所有表的元数据都装在 FE 的 JVM 内存里,tablet 越多压力越大 | 集群规模大了之后 FE 成为短板 |
| 写入吞吐 | 单节点 Stream Load 约百万行/秒,一般场景够用 | 除非日导入量上百 TB |
| 并发查询 | 适合低中并发大查询;高并发点查不是它的主场 | 把它当业务库高并发查会难受 |
给小白的实操建议 :Doris 建表时把"分桶数"想清楚。分桶太少,数据倾斜、并行不起来;分桶太多,tablet 数暴涨,伤 FE。经验值是单个 tablet 1~10GB 数据量。
三、分区与分桶:数据在表里是怎么"切"的
回答一个建表时躲不开的问题:PARTITION BY 和 DISTRIBUTED BY 到底啥区别?先把一句话放前面:
分桶是强制的,分区才是可选的。 Doris 里每张表都必须分桶;分区爱建不建。
1. 一句话定义
想象一个图书馆:
-
分区 :按"楼层"分------1 楼放 2023 年的书,2 楼放 2024 年的书,3 楼放 2025 年的书。这是按规则把数据分堆。
-
分桶 :每层楼里再按"书架号"分------把这层的书按书名哈希扔到 10 个书架上。这是把每堆数据切片,散到多台机器上并行干活。
先分堆(分区),堆内切片(分桶)。
2. 逐项对比
| 对比项 | 分区(Partition) | 分桶(Bucket) |
|---|---|---|
| 是什么 | 按规则把数据分成大堆 | 把每堆数据切成小片(tablet) |
| 用什么切 | 时间范围、枚举值等(RANGE / LIST) | 对某列取哈希(HASH)或随机(RANDOM) |
| 是否必须 | 可选 | 必须,不写 Doris 也给默认分 |
| 列的限制 | 分区列必须是 Key 列 | 任意列都行(一般选主键) |
| 建好后能改吗 | 能加、能删分区 | 桶数建表定死,不能改 |
| 解决什么问题 | 查询少扫数据、按时间删数据、生命周期管理 | 并行计算、数据均匀散到多台 BE |
| 类比 | 楼层 / 文件夹 | 书架 / 文件分片 |
3. 拿行为日志表画两张图
以 ihm_user_behavior_log(用户行为日志)为例,只分桶不分区是这样的:
TypeScript
ihm_user_behavior_log
└── 默认分区(一整个,所有年份的日志全在这)
├── 桶0 id%10=0 的所有行 ──→ BE 节点 A
├── 桶1 id%10=1 的所有行 ──→ BE 节点 B
├── ...
└── 桶9 id%10=9 的所有行 ──→ BE 节点 C
加了按天分区之后:
ihm_user_behavior_log
├── p20261010(10月10日的日志)
│ ├── 桶0 ──→ BE A 每个分区内部各自分桶
│ ├── 桶1 ──→ BE B
│ └── ...桶9
├── p20261011(10月11日)
│ ├── 桶0 ──→ BE A
│ └── ...
└── ...
注意:分桶在每个分区内部独立进行。分区从 1 个变 365 个,桶从 10 个变 3650 个。这笔账就是第二章说的"tablet 数 = 分区数 × 桶数",tablet 切得太碎会撑爆 FE,所以分区和分桶要联动设计,不能俩都往大了开。
4. 查询的时候,俩各干各的活
sql
SELECT app_name, COUNT(*)
FROM ihm_user_behavior_log
WHERE create_time >= '2026-10-01'
GROUP BY app_name;
分区先干活(做减法):一看条件是 10 月,直接把之前的分区全部扔掉不看。存了两年数据的话,700 多个分区只扫十几个------这叫"分区裁剪",是分区最大的价值。
分桶再干活(做加法) :剩下分区里的 10 个桶同时开算,10 路并行,最后汇总。BE 节点越多,并行越给力。
一句话总结分工:
分区让查询"少看",分桶让查询"快算"。
对写入和清理也一样:删一年前的日志,无分区表要 DELETE 几亿行(慢且产生垃圾),分区表 DROP PARTITION 一秒完事。
5. 建表规则与实战改造
三个硬规则先记住:
-
分区列必须是 Key 列 。比如
UNIQUE KEY(id)的表想按create_time分区,会直接报错------解法是把分区列挪进 Key:UNIQUE KEY(id, create_time)(id唯一,联合键自然唯一,不影响 CDC 同步)。 -
Range 分区列只支持 DATE、DATETIME、整数。拿 varchar 存的时间戳当分区列,没戏。
-
无分区表不能事后变成分区表。分区方式建表定终身,只能重建表搬数据。
行为日志表的分区改造 DDL(动态分区版):
sql
CREATE TABLE `ihm_user_behavior_log` (
`id` bigint NOT NULL COMMENT "主键",
`create_time` datetime NOT NULL COMMENT "创建时间(分区列)",
-- ... 其余字段同 MySQL 侧 ...
) ENGINE=OLAP
UNIQUE KEY(`id`, `create_time`) -- ① 分区列进 Key
PARTITION BY RANGE(`create_time`) () -- ② 按创建时间切
DISTRIBUTED BY HASH(`id`) BUCKETS 5 -- ③ 每个分区内再分 5 桶
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"dynamic_partition.enable" = "true", -- ④ 动态分区管家
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-730", -- 保留最近 730 天,过期自动删
"dynamic_partition.end" = "3", -- 预建未来 3 天
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "5",
"dynamic_partition.replication_num" = "1",
"enable_unique_key_merge_on_write" = "true"
);
动态分区就是"分区管家":自动建明天的分区、自动删过期的分区,你永远不用管"分区够不够用""陈年数据怎么清"。
三个坑提前打招呼:
| 坑 | 症状 | 解法 |
|---|---|---|
| "no partition for this tuple" | 写入的数据落在分区范围外,全量同步最常撞上 | start 一定要覆盖到 MySQL 里最老的那条数据,宁可写 -3650 |
| 分区数上限 | 每张表默认最多 4096 个分区,按天 ≈ 11 年,按小时 ≈ 170 天 | 撞了调 FE 配置 max_dynamic_partition_num |
| 事后想加分区 | 不支持 | 建新表重灌。CDC 同步最省事的路径:drop 旧表 → 新 DDL → 重启作业,全量+增量自动重来 |
6. x张表怎么定分区策略
| 表类型 | 例子 | 策略 |
|---|---|---|
| 日志流水表,只增不删涨得快 | 行为日志、操作日志 | 必须分区,按天 + 动态分区 |
| 业务明细表,千万~亿级 | 就诊记录、费用明细 | 建议分区 ,量不大就按月(time_unit = "MONTH") |
| 维度表、配置表,几万行 | 科室字典、渠道表 | 不分区,桶分 3 个就完 |
| 会被 UPDATE 的状态表 | 订单状态、随访进度 | 量不大不分区;更新多的行跨分区有额外开销 |
判断法:预计单表最终超过 5000 万行,就上分区。
四、Doris 比 MySQL 快多少(实测对比)
先说结论的量级:点查 MySQL 更快,分析查询 Doris 快 5~10 倍,复杂多表 JOIN 能快 8 倍以上,差距随数据量放大。
同机部署的实测数据(Doris 1.2 vs MySQL 8.0,亿级数据场景):
| 查询类型 | MySQL 耗时 | Doris 耗时 | Doris 快多少 | 谁赢 |
|---|---|---|---|---|
| 主键点查一条记录 | 2.1ms | 3.5ms | 慢 ~60% | MySQL |
| 二级索引查一条记录 | 8.7ms | 12.3ms | 慢 ~40% | MySQL |
| 全表 COUNT | 4.2s | 0.8s | 快 ~5 倍 | Doris |
| 日期范围聚合(GROUP BY) | 6.8s | 1.2s | 快 ~5~6 倍 | Doris |
| 5 表 JOIN + 聚合 | 28.5s | 3.4s | 快 ~8 倍 | Doris |
多种 OLAP 场景的对比测试结论一致:
| 场景 | 相对速度 |
|---|---|
| 简单单表聚合 | 差距不大(1~2 倍) |
| 多表 JOIN | Doris 快 3~5 倍 |
| 复杂聚合计算 | Doris 耗时约为 MySQL 的 1/4 |
| 时间序列分析 | 列存优势明显,5 倍以上 |
| 内存占用 | 相同查询下 Doris 比 MySQL 省 ~30% |
还有个更直观的说法:同样的 SQL,从 MySQL 挪到 Doris,耗时大约降到原来的十分之一。
但必须把话说全:数据量越大,差距越大
| 数据量 | MySQL 聚合查询 | Doris 聚合查询 | 差距 |
|---|---|---|---|
| 百万级 | 几百毫秒 | 几十~几百毫秒 | 不明显,别为了这个上 Doris |
| 千万级 | 几秒 ~ 十几秒 | 1 秒内 | 5~10 倍 |
| 亿级 | 十几秒 ~ 几分钟 | 1~3 秒 | 10 倍以上 |
| 十亿级 | 跑不动/锁死业务 | 秒级 ~ 十几秒 | 差距拉到没法比 |
百万级数据量上 Doris 是没必要的,MySQL 完全扛得住。Doris 的价值从千万级开始兑现------医疗业务库里就诊、费用这类表大概率是千万到亿级,正好落在收益区间里。
顺带说清楚"Doris 为什么快"的三个来源,跟第三章正好呼应:
-
列存------只读查询涉及的列,IO 砍掉九成;
-
分区裁剪------按时间查只扫相关分区(所以行为日志表要分区);
-
分桶并行------一条 SQL 拆到多台 BE 同时算(所以分桶不能只分 1 个)。
五、一页总结
| 维度 | MySQL | Doris |
|---|---|---|
| 单表行数上限 | 经验值 ~2000 万,不是硬限制 | 实际无行数上限,生产案例 534 万亿行 |
| 真正的瓶颈 | B+ 树深度、buffer pool、DDL 锁表 | Tablet 数量(~100 万)撑爆 FE 内存 |
| 数据切分 | 分库分表(应用层自己折腾) | 分区(可选,管分堆)+ 分桶(必须,管切片并行) |
| 切分能改吗 | 分完后悔只能再迁 | 分区可增删;桶数定死 |
| 点查一条记录 | 2.1ms,赢 | 3.5ms |
| 单表聚合 | 秒~分钟级 | 快 5~6 倍 |
| 多表 JOIN | 十几秒~分钟级 | 快 5~8 倍 |
| 亿级数据分析 | 基本不可用 | 秒级 |
| 写入方式 | 事务写,高并发点写 | Stream Load 批量灌,单节点百万行/秒 |
| 角色定位 | 业务读写 | 分析查询 |
给 x 张表的行动清单:
-
每张表先估"最终多大",超 5000 万行的上分区;
-
日志流水表按天分区 + 动态分区,
start覆盖全量数据的最早时间; -
分区列选 datetime 类型的列(如
create_time),并把它挪进 Key; -
分桶列用主键,桶数按"单桶 1~10GB"倒推,宁少勿多(tablet 数 = 分区数 × 桶数);
-
维度小表别凑热闹,不分区,少分桶。
两句话带回去够用很久:分区管分堆,分桶管并行;分区列必须进 Key,桶数建表定终身。