架构总览:实时摄入链路与查询链路深度解析
说实话,Druid 的架构第一次看会觉得有点乱。Overlord、MiddleManager、Peon、Broker、Router、Historical……一堆角色,各司其职。我当年刚接触时也绕了好一阵子。
但说白了,Druid 就干两件事:把数据写进去,把数据查出来。写和查是两条完全独立的链路,各自有各自的组件。咱们一条一条拆开看。
核心认知:Druid 的实时摄入和查询是解耦的。写入链路负责把流式数据转成不可变的 Segment,查询链路负责在这些 Segment 上跑 SQL。两条链路只在 Deep Storage 和 Metadata Storage 处交汇。
实时摄入链路:Overlord → MiddleManager → Peon
先看写入端。实时数据怎么进来的?
我习惯把这条链路叫作「三段式流水线」。三个角色各司其职:
- Overlord —— 调度大脑,负责分配任务、管理集群资源。
- MiddleManager —— 执行容器,负责拉起 Peon 进程。
- Peon —— 真正的干活单元,每个 Peon 负责一个数据分片(Segment)的写入。
Overlord:调度大脑
Overlord 不直接处理数据。它的职责是:
- 接收外部提交的摄入任务(比如 Kafka 索引任务)
- 查看当前集群里哪些 MiddleManager 有空闲槽位
- 把任务分配给合适的 MiddleManager
- 监控任务状态,失败时重新调度
我在项目中遇到过一个问题:Overlord 的队列积压导致任务提交超时。后来发现是 MiddleManager 的 druid.worker.capacity 设得太小,任务全堵在 Overlord 里了。嗯,这里要注意——Overlord 本身不干活,它只调度,瓶颈往往在下游。
调优小技巧:Overlord 的 druid.indexer.runner.javaOpts 里堆内存别给太大,它不存数据。真正吃内存的是 Peon。我一般给 Overlord 4-8GB 就够用了。
MiddleManager:任务容器
MiddleManager 是 Overlord 的「打工人」。它收到任务后,会 fork 出一个独立的 JVM 进程——也就是 Peon。
每个 MiddleManager 可以同时运行多个 Peon,数量由 druid.worker.capacity 控制。举个例子:
# 每个 MiddleManager 最多同时运行 3 个 Peon
druid.worker.capacity=3
# 每个 Peon 的 JVM 参数
druid.indexer.runner.javaOpts=-Xmx4g -Xms4g
这里有个坑:Peon 是独立进程,不是线程。这意味着每个 Peon 都有自己的堆内存。如果你把 capacity 设成 10,每个 Peon 给 4GB,那一个 MiddleManager 就要吃掉 40GB 内存。我曾经见过有人把 capacity 设得很大,结果 MiddleManager 直接 OOM 了。
避坑指南:MiddleManager 的总内存 = capacity × 每个 Peon 的堆内存 + 系统开销。建议预留 20% 的内存给操作系统和 MiddleManager 自身。比如机器有 64GB,capacity 设 8,每个 Peon 给 6GB,这样 8×6=48GB,加上 20% 开销 ≈ 57.6GB,刚好在安全范围内。
Peon:真正的数据工人
Peon 是摄入链路的最后一环。它负责:
- 从 Kafka(或其他数据源)拉取数据
- 将数据按时间窗口和维度列进行聚合、排序
- 生成不可变的 Segment 文件
- 将 Segment 推送到 Deep Storage(如 HDFS、S3)
- 通知 Coordinator 更新元数据
Peon 的生命周期和任务绑定。任务完成,Peon 就销毁。所以 Peon 是「用完即走」的临时进程。
你想想看,这种设计有什么好处?隔离性极强。一个 Peon 挂了不会影响其他 Peon,Overlord 只需要重新调度就行。
查询链路:Broker → Router → Historical
说完写入,再看查询。查询链路也有三个核心角色:
- Broker —— 查询入口,负责解析 SQL、路由请求、合并结果。
- Router —— 可选组件,负责负载均衡和请求分发。
- Historical —— 数据存储节点,负责加载 Segment 并响应查询。
Broker:查询的「大脑」
Broker 是用户接触的第一个节点。你发的 SQL 先到 Broker,Broker 做三件事:
- 解析 SQL:把 SQL 转成 Druid 内部的查询对象(Native Query)
- 路由:根据查询的时间范围和数据分布,决定去哪些 Historical 节点拉数据
- 合并:把多个 Historical 返回的部分结果合并成最终结果
Broker 不存数据,它只做计算。所以 Broker 的瓶颈通常在 CPU 和网络 I/O,而不是内存。
我记得有一次线上查询特别慢,查了半天发现是 Broker 的 druid.broker.http.maxConnections 设得太小,导致查询请求在 Broker 层排队。调大这个参数后,延迟直接降了一半。
经验之谈:Broker 的堆内存主要用来缓存查询中间结果。如果查询涉及大量 GroupBy,建议给 Broker 多分配一些内存。我一般给 16-32GB,具体看查询复杂度。
Router:可选但推荐
Router 是一个轻量级的代理层。它不是必须的,但如果你有多个 Broker,Router 就很有用了。
Router 的作用很简单:
- 接收客户端请求
- 按某种策略(轮询、最少连接数等)把请求分发给后端的 Broker
- 返回 Broker 的处理结果
说白了,Router 就是一个负载均衡器。你可以用 Nginx 替代它,但 Druid 自带的 Router 支持一些高级功能,比如按查询类型路由(把聚合查询发给专门的 Broker 池)。
Historical:数据的「仓库」
Historical 是查询链路的终点。它负责:
- 从 Deep Storage 下载 Segment 到本地磁盘
- 将 Segment 加载到内存或内存映射文件中
- 响应 Broker 发来的查询请求
Historical 是真正的数据存储节点。它的性能直接决定了查询速度。
这里有个关键点:Historical 加载的 Segment 都是「已发布的」——也就是已经完成摄入、不可变的数据。实时数据还在 Peon 里,Historical 不碰实时数据。
注意:如果查询涉及实时数据(比如最近几分钟的数据),Broker 会同时向 Peon 和 Historical 发请求,然后把两边的结果合并。这就是所谓的「实时+历史」混合查询。我遇到过一个问题:Peon 上的实时 Segment 没有做预聚合,导致混合查询时 Broker 合并压力巨大。后来在摄入时加了 rollup 才解决。
两条链路的交汇点
实时摄入链路和查询链路不是完全独立的。它们在两个地方交汇:
| 交汇点 | 说明 |
|---|---|
| Deep Storage | Peon 把 Segment 推到这里,Historical 从这里拉取 |
| Metadata Storage | Peon 通知 Coordinator 更新元数据,Broker 通过元数据知道 Segment 分布 |
这两个交汇点保证了数据的「最终一致性」。Peon 写完数据后,Historical 不会立即加载,而是等 Coordinator 通知它。这个延迟通常在秒级到分钟级。
整体架构图
下面这张图把两条链路的关系画清楚了。我建议你多看几遍,理解每个组件的位置和作用。
小结
两条链路,六个核心角色。记住几个关键点:
- Overlord 只调度,不处理数据
- Peon 是临时进程,任务完成就销毁
- Broker 是查询入口,负责解析和合并
- Historical 存的是已发布的 Segment,不碰实时数据
- Router 不是必须的,但多 Broker 场景下强烈推荐
理解了这个架构,后面调优才有方向。比如查询慢,你得先判断是 Broker 合并慢还是 Historical 扫描慢,对症下药才行。