4. Segment详解
Segment 是 Druid 存储和查询的核心单元。说白了,Druid 能跑多快、能存多省,全靠 Segment 设计得好不好。我刚开始接触 Druid 时,觉得 Segment 不就是个文件嘛,后来踩了不少坑才明白——它的结构、生命周期、不可变特性,每一个细节都藏着性能优化的门道。
一句话总结:Segment 是 Druid 中不可变的、按时间组织的、列式存储的数据文件。它是查询加速和存储压缩的基础。
4.1 Segment 的内部结构
一个 Segment 对应一个时间区间(比如一小时、一天)和一个数据源。它内部长什么样?我拆开给你看。
每个 Segment 内部,数据是按列存储的。每一列又分成三个部分:
- 编码区:字典编码、浮点编码等,把原始值变成整数 ID
- 索引区:Bitmap 索引、倒排索引、min/max 索引,用于快速过滤
- 数据区:压缩后的实际数据,LZ4 或 ZSTD 压缩
我的经验:时间列和维度列一定要建 Bitmap 索引。我在一个日志分析项目中,没给维度列建索引,查询慢得离谱。加上 Bitmap 索引后,过滤速度提升了 10 倍以上。
4.2 Segment 的生命周期
一个 Segment 从生到死,经历四个阶段。我习惯用「生、老、病、死」来记它。
| 阶段 | 状态 | 说明 | 存储位置 |
|---|---|---|---|
| 1. 生成 | APPENDING | 实时任务正在写入,数据还在内存中 | 实时节点内存 |
| 2. 发布 | PUBLISHING | Segment 落盘,正在上传到 Deep Storage | 本地磁盘 + 上传中 |
| 3. 可用 | USABLE | 已上传完成,可以被查询 | Deep Storage + 历史节点 |
| 4. 淘汰 | UNUSABLE | 被新版本覆盖,不再用于查询 | Deep Storage(可清理) |
为什么会这样设计?你想想看,实时数据不断进来,如果 Segment 一直在变,查询就没法做缓存和优化。所以 Druid 的做法是:实时数据先攒着,攒够了就生成一个不可变的 Segment,然后扔给历史节点去服务查询。
注意:Segment 在 APPENDING 状态时,查询性能很差。我曾经在生产环境遇到过实时查询超时,就是因为 Segment 还没落盘,查询走了实时节点的内存数据。后来我把实时窗口调小了,让 Segment 尽快落盘,问题就解决了。
4.3 Segment 的不可变性
Segment 一旦生成,就永远不能修改。这是 Druid 最核心的设计原则之一。
不可变性带来了三个好处:
- 查询稳定:数据不变,缓存可以放心用,查询计划可以复用
- 存储高效:列式压缩、编码、索引,都是基于不变数据做的优化
- 容错简单:Segment 是只读的,不需要考虑并发写入的问题
但不可变性也有代价。比如你想修改一条历史数据,怎么办?Druid 的做法是:生成一个新的 Segment 版本,把旧版本标记为不可用。查询时,Druid 会自动选择最新版本。
核心机制:Segment 的版本号是一个时间戳。新版本覆盖旧版本,查询永远读最新版本。旧版本不会立即删除,而是保留一段时间,用于回滚或恢复。
我记得有一次,业务方要求修改三天前的一条数据。如果是传统数据库,一条 UPDATE 就搞定了。但在 Druid 里,我得重新生成那段时间的 Segment,然后发布新版本。虽然麻烦,但保证了查询的稳定性和一致性。
4.4 Segment 的合并与优化
Segment 太小太多,查询性能会下降。为什么?因为查询时要扫描的 Segment 数量太多,每个 Segment 都有元数据开销。我见过一个极端案例,一个数据源有 10 万个 Segment,查询光打开文件就花了 5 秒。
解决方案就是 Segment 合并(Compaction)。
4.4.1 什么时候需要合并?
- 实时任务产生的 Segment 太小(比如几分钟一个)
- Segment 数量太多,超过几千个
- 数据有更新,产生了大量小 Segment 版本
4.4.2 合并怎么做?
Druid 提供了两种合并方式:
| 方式 | 触发方式 | 适用场景 | 注意事项 |
|---|---|---|---|
| 自动合并 | Druid 自动调度 | 实时数据持续产生小 Segment | 需要配置合并策略 |
| 手动合并 | 提交合并任务 | 批量数据导入后、数据更新后 | 需要指定时间区间和目标大小 |
合并的核心逻辑很简单:把多个小 Segment 读出来,重新排序、压缩、建索引,生成一个大的 Segment。合并后的 Segment 查询更快,存储也更省。
我建议:Segment 的大小控制在 300MB 到 700MB 之间。太小了查询慢,太大了加载慢。我在一个日活千万的系统中,把 Segment 大小设为 500MB,查询性能提升了 30%。
4.4.3 合并的避坑指南
我曾经踩过一个坑:合并任务跑得太频繁,把历史节点的 CPU 打满了,导致查询超时。后来我调整了合并策略,只在凌晨低峰期执行合并,问题就解决了。
这里有几个要点:
- 合并不要并发太多:建议同时只跑 1-2 个合并任务
- 合并时间窗口要合理:一次合并的时间区间不要太大,建议 1-7 天
- 合并后要验证:检查 Segment 大小和数量是否符合预期
4.4.4 合并的配置示例
在 Druid 的 Coordinator 配置中,可以设置自动合并策略:
{
"type": "compaction",
"dataSource": "my_datasource",
"interval": "2024-01-01/2024-12-31",
"tuningConfig": {
"maxRowsPerSegment": 5000000,
"maxTotalRows": 20000000
},
"taskConfig": {
"maxNumConcurrentSubTasks": 2,
"maxRetry": 3
}
}
这个配置的意思是:对 my_datasource 在 2024 年的数据做合并,每个 Segment 最多 500 万行,总共不超过 2000 万行,同时最多跑 2 个合并子任务。
注意:合并任务会消耗大量内存和 CPU。如果你的集群资源紧张,建议先做一次小规模测试,看看合并对查询的影响。
4.5 小结
Segment 是 Druid 的基石。理解它的结构、生命周期、不可变性和合并优化,你就能更好地调优 Druid 的性能。
我个人觉得,Segment 的不可变性是 Druid 最聪明的设计。虽然它带来了一些限制,但换来了查询的稳定和存储的高效。至于合并,记住一句话:小 Segment 是万恶之源,定期合并是解药。