公司在 Apache Doris 的生产实践中,发现在做数据需求时候,遇到这样一个"致命"问题:
"一张 7000 万行的主键模型账单表,执行全表 INSERT 时,不仅写入慢得像蜗牛,还把整个 BE 节点拖到 CPU 100%、IO 打满,集群几乎瘫痪。"
今天这篇文章用来记录下这个问题,且做详细剖析,并给出一套可落地、高性价比的优化方案。
首先,我从Doris官网了解到,这并非Doris的缺陷,而是主键模型(Unique Key / Primary Key)的写入机制与全量导入场景天然不匹配。以下是进行问题分析和问题解决。
一、为什么主键模型 INSERT 这么慢?------ 根本原因
Doris 的主键模型(特别是 Merge-on-Write 实现)在写入时必须保证数据唯一性,因此采用 "读-改-写"(Read-Modify-Write) 机制:
python
INSERT INTO t_bill_center VALUES (id=1001, ewb_no ='010010101099')
↓
1. 查找 id=1001 是否已存在(需扫描主键索引)
2. 若存在 → 在 delete_bitmap 中标记旧记录为"删除"
3. 再将新记录写入 MemTable
关键性能瓶颈:
| 瓶颈点 | 技术细节 | 影响 |
|---|---|---|
| delete_bitmap 维护 | 每次更新都要修改位图(tablet_meta.cpp:763) | 内存压力大,频繁 GC |
| MemTable 冲突 | 多线程写同一 tablet 时锁竞争(tablet.cpp:363) | 并发写入吞吐骤降 |
| 无批量合并 | 默认每条 INSERT 独立提交 | 网络 + 日志开销爆炸 |
| Compaction 压力 | 高频写入触发大量 Delta Compaction | IO 打满,查询变慢 |
一句话总结:
主键模型是为"实时更新"设计的,不是为"全量覆盖"准备的。
二、核心优化方案:启用 Group Commit(攒批提交)
Doris 从 1.2 版本 开始引入 Group Commit 功能,专为解决高频小写入场景的性能问题。它能将多个 INSERT 请求自动攒批、合并提交,大幅降低事务开销。
启用方式(两种)
方式 1:建表时指定(推荐)
python
CREATE TABLE t_bill_center (
user_id BIGINT,
ewb_no VARCHAR(100),
email VARCHAR(100)
) ENGINE=OLAP
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 64
PROPERTIES (
"group_commit_interval_ms" = "10000", -- 每 10 秒提交一批
"group_commit_data_bytes" = "134217728" -- 或累计 128MB 提交
);
方式 2:全局 FE 配置(fe.conf)
python
# 默认值(Config.java:646-652)
group_commit_interval_ms = 10000
group_commit_data_bytes = 134217728 # 128MB
原理:
Group Commit 在 BE 端维护一个内存队列(group_commit_queue_mem_limit),当达到时间或大小阈值时,一次性将多条 INSERT 合并为一个事务提交,减少 WAL 写入和锁竞争。
三、BE 层关键参数调优(be.conf)
主键模型对内存和 IO 极其敏感,以下配置可显著提升稳定性:
python
# 1. 增大写缓冲(默认 200MB,5000 万数据建议 ≥ 500MB)
write_buffer_size = 536870912 # 512MB
# 2. 启用自适应缓冲(根据负载动态调整)
enable_adaptive_write_buffer_size = true
# 3. 控制并行 flush 任务数(避免 IO 打满)
memtable_flush_running_count_limit = 2
# 4. Group Commit 队列内存(默认 64MB)
group_commit_queue_mem_limit = 268435456 # 256MB
# 5. Group Commit 处理线程(根据 CPU 核数调整)
group_commit_insert_threads = 8
# 6. WAL 磁盘限制(防止日志占满磁盘)
group_commit_wal_max_disk_limit = 10%
注意:
write_buffer_size 过大会导致 OOM,建议不超过 BE 节点内存的 20%。
四、终极建议:换用更高效的导入方式
对于 7000 万级全量数据,绝对不要用 INSERT INTO ... VALUES (...)!这是性能灾难的根源。
推荐方案:Stream Load + 分批导入
步骤 1:将数据拆分为多个文件(如每份 200 万行)
python
split -l 2000000 full_data.csv chunk_
# 生成 chunk_aa, chunk_ab, ...
步骤 2:使用 Stream Load 并行导入
python
# 示例:导入一个分片
curl --location-trusted -u user:passwd \
-H "label:load_20240601_aa" \
-H "column_separator:, " \
-T chunk_aa \
http://fe_host:8030/api/lbdb/t_bill_center/_stream_load
优势:
性能提升 5~10 倍(Stream Load 专为批量设计)
自动重试 + 断点续传
不占用 MySQL 协议连接池
对比:
INSERT:单连接,逐条解析,事务开销大
Stream Load:HTTP 并行,批量解析,零事务开销
五、如果必须用 INSERT?------ 安全分批策略
若因架构限制只能用 INSERT,请务必分批 + 限流:
python
-- 错误示范(直接全量插入)
INSERT INTO t_bill_center SELECT * FROM temp_table;
-- 正确做法:循环分页插入
INSERT INTO t_bill_center
SELECT * FROM temp_table
ORDER BY user_id
LIMIT 100000 OFFSET 0; -- 第1批
INSERT INTO t_bill_center
SELECT * FROM temp_table
ORDER BY user_id
LIMIT 100000 OFFSET 100000; -- 第2批
-- ... 循环直到完成
同时配合:
应用层加 sleep(1s) 避免压垮 BE
监控 show proc '/backends' 内存使用
六、监控与诊断:如何判断优化是否生效?
关键监控命令:
python
-- 1. 查看 BE 节点状态(重点关注 Mem、IO)
SHOW PROC '/backends';
-- 2. 查看 Compaction 任务是否堆积
SHOW PROC '/compaction';
-- 3. 查看 Group Commit 队列(Doris 2.0+)
SHOW PROC '/group_commit';
七、架构升级建议:Doris 2.0+ 用户必看
如果你使用的是 Doris 2.0 或更高版本,强烈建议:
迁移到 Primary Key 模型(非 Unique Key)
新主键模型使用 Memory + RocksDB 存储索引
写入性能比旧版提升 3~5 倍
支持 Partial Update(只更新部分列)
开启 Light Schema Change
避免 ALTER TABLE 锁表
八、总结:优化 checklist
| 优化项 | 操作 | 预期效果 |
|---|---|---|
| 启用 Group Commit | 建表加 PROPERTIES | 写入吞吐 +200% |
| 调大 write_buffer_size | be.conf 配置 | 减少 flush 频率 |
| 换用 Stream Load | 分批 HTTP 导入 | 避免集群卡死 |
| 分批 INSERT | LIMIT + OFFSET | 降低瞬时压力 |
| 升级 Doris 2.0+ | 使用 Primary Key | 根本性性能提升 |
最后说明:
"主键模型 ≠ 全量导入工具"。
用对场景(实时更新),才能发挥 Doris 的真正威力。