为什么“零散、高频度的并发 INSERT”在 Doris 中是不被推荐的?
在 Apache Doris(以及 ClickHouse 等大多数现代 OLAP 数据库)中,“零散、高频度的并发 INSERT”(通常指每次只插入几条甚至一条数据,且并发量很高)是不被推荐的。
这主要是由 Doris 的存储引擎架构(LSM-Tree 类似结构)、事务机制以及 OLAP 系统的设计初衷决定的。如果强行采用这种写入方式,会导致系统迅速崩溃或性能严重下降。
以下是具体原因的深度解析:
1. LSM-Tree 存储机制与“小文件问题”
Doris 的底层存储结构类似于 LSM-Tree(Log-Structured Merge-Tree)。
- Rowset 与版本号: 每次执行一次导入(包括一个
INSERT事务),Doris 都会在后台生成一个数据分片文件,称为一个 Rowset(对应一个新版本号)。 - Compaction(文件合并)压力: 为了保证查询性能,Doris 后台会有线程不断地将这些小 Rowset 合并成大 Rowset(这个过程叫 Compaction)。
- 后果(Too many versions): 如果高频写入,生成小文件的速度将远快于后台 Compaction 的速度。很快就会触发 Doris 的自我保护机制,报
Too many versions(错误码 -235) 错误,进而拒绝写入。同时,海量的小文件会导致查询时需要扫描极多的文件,使查询性能急剧恶化。
2. 极高的元数据与事务开销
在 Doris 中,每一次 INSERT INTO ... VALUES 都是一个完整的数据库事务:
- FE(Frontend)的负担: FE 需要解析 SQL、生成物理执行计划、向 BE 分发任务、协调分布式事务、记录元数据日志(Edit Log)。
- 元数据锁竞争: 高并发的
INSERT会导致 FE 的元数据锁竞争激烈,CPU 飙升,甚至导致 FE 节点无响应(OOM 或假死)。 - 网络与 RPC 开销: 每次写入都需要 FE 与多个 BE(Backend)之间进行多次 RPC 通信来确认事务状态,高频写入会导致网络带宽和 CPU 被这些“握手”信号榨干,实际有效数据传输率极低。
3. I/O 效率低下(写入放大)
OLAP 数据库是为大吞吐量设计的,采用的是列式存储。
- 列存的劣势: 在列式存储中,数据是按列连续存放的。写入一行数据,实际上要修改底层的几十个列文件。
- 写入放大: 每次只写一两行,会导致频繁的磁盘随机 I/O,无法发挥顺序写片的优势,磁盘 I/O 很快就会达到瓶颈,并造成严重的“写入放大”效应,极大缩短 SSD 寿命。
4. 线程池与连接耗尽
每次并发 INSERT 都会占用 BE 的执行线程和全局连接数。高并发的零散写入会瞬间占满 BE 的写入线程池,导致后续的导入任务排队、超时,甚至影响到正常的查询请求(查询线程被抢占)。
官方的破局方案与最佳实践
针对这种“实时、高频”的数据写入需求,Doris 并没有坐视不管,而是提供了以下标准解决方案:
方案一:微批(Micro-Batch)攒批写入(最推荐)
不要在客户端收到一条数据就写一条。应当在应用层(如使用 Java 内存队列、Flink 等)进行攒批:
- 攒批策略: 达到 1 万条,或者时间过去 5 秒,再发起一次写入。
- 工具选择: 推荐使用 Stream Load 或 Flink-Doris-Connector。它们在底层都实现了高效率的流式攒批写入。
方案二:开启 Group Commit(组提交)—— Doris 2.0+ 新特性
为了解决用户无法在客户端攒批的问题,Doris 在 2.0 及以上版本引入了 Group Commit 功能。
- 原理: 当用户发送很多个单条
INSERT时,Doris 的 FE/BE 不会立刻写入磁盘,而是在内存中将这些小INSERT攒成一个大批次,然后一次性提交。 - 效果: 极大地降低了版本冲突和小文件问题,使得单条高频
INSERT成为可能,但依然会消耗较多的 SQL 解析 CPU,性能仍略低于客户端攒批。
方案三:使用 Routine Load(对接 Kafka)
如果是来自消息队列(如 Kafka)的数据,推荐使用 Routine Load。Doris 会在后台自动控制消费速率和攒批大小,保证写入的高效和稳定。
总结
OLAP 系统的核心奥义是“以空间换时间,化零为整”。Doris 擅长的是“单次写入 100 万行”,而不是“100 万次每次写入 1 行”。理解并顺应这一架构特性,才能发挥出 Doris 强大的实时分析性能。