架构总览:实时摄入链路与查询链路深度解析

说实话,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 是摄入链路的最后一环。它负责:

  1. 从 Kafka(或其他数据源)拉取数据
  2. 将数据按时间窗口和维度列进行聚合、排序
  3. 生成不可变的 Segment 文件
  4. 将 Segment 推送到 Deep Storage(如 HDFS、S3)
  5. 通知 Coordinator 更新元数据

Peon 的生命周期和任务绑定。任务完成,Peon 就销毁。所以 Peon 是「用完即走」的临时进程。

你想想看,这种设计有什么好处?隔离性极强。一个 Peon 挂了不会影响其他 Peon,Overlord 只需要重新调度就行。

查询链路:Broker → Router → Historical

说完写入,再看查询。查询链路也有三个核心角色:

  • Broker —— 查询入口,负责解析 SQL、路由请求、合并结果。
  • Router —— 可选组件,负责负载均衡和请求分发。
  • Historical —— 数据存储节点,负责加载 Segment 并响应查询。

Broker:查询的「大脑」

Broker 是用户接触的第一个节点。你发的 SQL 先到 Broker,Broker 做三件事:

  1. 解析 SQL:把 SQL 转成 Druid 内部的查询对象(Native Query)
  2. 路由:根据查询的时间范围和数据分布,决定去哪些 Historical 节点拉数据
  3. 合并:把多个 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 通知它。这个延迟通常在秒级到分钟级。

整体架构图

下面这张图把两条链路的关系画清楚了。我建议你多看几遍,理解每个组件的位置和作用。

Druid 实时摄入与查询架构总览 数据源 (Kafka) Overlord MiddleManager Peon Deep Storage 客户端 Router Broker Historical Coordinator Metadata Storage 摄入链路 查询链路 存储层 元数据层 说明: • 实线箭头表示数据流,虚线箭头表示元数据流 • Peon 将 Segment 推送到 Deep Storage 后,Coordinator 更新元数据,Broker 通过元数据感知 Segment 分布

小结

两条链路,六个核心角色。记住几个关键点:

  • Overlord 只调度,不处理数据
  • Peon 是临时进程,任务完成就销毁
  • Broker 是查询入口,负责解析和合并
  • Historical 存的是已发布的 Segment,不碰实时数据
  • Router 不是必须的,但多 Broker 场景下强烈推荐

理解了这个架构,后面调优才有方向。比如查询慢,你得先判断是 Broker 合并慢还是 Historical 扫描慢,对症下药才行。