Druid 的数据按时间分片存成不可变的列式 Segment,查询几乎都带时间范围条件,Broker 拿到时间范围后只定位匹配的 Segment,把子查询 scatter-gather 到持有这些 Segment 的 Historical 和正在摄入的 Middle Manager,各节点在本地做聚合,Broker 合并局部结果返回。数据进来了,接下来要回答的问题就是:查询具体怎么发生?生产环境跑起来又要盯什么?
这篇讲 Druid 的查询机制与运维调优。先讲查询的两种入口和 scatter-gather 的处理路径,再逐个拆 Timeseries、TopN、GroupBy、Scan、TimeBoundary 五种查询类型的适用场景与代价差异,接着讲查询下推、内存限制、Segment pruning 与缓存分层,最后覆盖 Coordinator 的数据生命周期管理、auto-compaction 运维和监控指标。
一、查询入口与处理路径
1.1 两种查询接口
Druid 支持两种查询语言:原生查询和 Druid SQL。两者不是互斥关系,Druid SQL 在 Broker 上被翻译成原生查询后执行,翻译的额外开销很小。
原生查询是 JSON over HTTP。查询发到 Broker 或 Router 的 /druid/v2/ 端点,Content-Type 为 application/json。Druid 也支持 application/x-jackson-smile 作为请求和响应格式,Smile 是二进制 JSON,序列化和传输比纯文本快,适合高频查询场景。
curl -X POST 'http://broker:8082/druid/v2/?pretty' \ -H 'Content-Type:application/json' \ -H 'Accept:application/json' \ -d @query.jsonDruid SQL 走 /druid/v2/sql 端点,请求体是 JSON 格式,包含 query 字段和可选的 context、parameters。SQL 查询的规划在 Broker 上完成,Broker 把 SQL 翻译成原生查询后按原生查询的路径执行。
{ "query": "SELECT country, COUNT(*) AS cnt FROM wikipedia WHERE __time >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR GROUP BY country ORDER BY cnt DESC LIMIT 10", "context": { "useApproximateTopN": true }}Druid SQL 用 useApproximateTopN 控制是否允许把 ORDER BY + LIMIT 的单维度分组查询翻译成 TopN。设为 false 时改用 GroupBy 以保证精确结果。这个参数是 SQL 查询性能和精度之间的重要开关,TopN 一节会细说。
1.2 scatter-gather 查询路径
查询的 scatter-gather 路径上文已经提过,这里从查询处理的角度再梳理一遍,重点放在 pruning 和下推。
查询进入 Broker 后,处理分为四步:
- Broker 根据
__time条件确定涉及哪些时间区间的 Segment。Druid 按 segmentGranularity 做时间分片,查询的时间范围只匹配对应的 Segment,这是第一步裁剪。除了时间,如果数据做了二级分片(按维度分区),Broker 还能用分片信息进一步裁剪。 - Broker 确定这些 Segment 分布在哪些 Historical 和 Middle Manager 上。实时正在摄入的数据由 Middle Manager 上的 Peon 服务,已发布的 Segment 由 Historical 服务。
- Broker 把子查询发给所有相关节点。各节点在本地 Segment 上执行聚合,返回局部结果。
- Broker 收集所有局部结果做最终合并,返回给客户端。
聚合下推到数据节点是关键。各 Historical 在本地完成大部分聚合计算,只把部分聚合后的结果传给 Broker。Broker 只做 N-way merge(N 路合并),网络传输量比把原始数据拉到 Broker 再聚合小得多。这也是 Druid 能做高并发查询的基础:每条查询的大部分计算分布在多个节点上并行执行,Broker 不成为计算瓶颈。
1.3 查询取消与超时
Druid 支持显式取消查询。每条查询可以设置唯一标识 queryId,通过 Broker 或 Router 上的 DELETE 端点取消:
curl -X DELETE 'http://broker:8082/druid/v2/abc123'查询超时由 timeout 参数控制(毫秒)。超时后查询返回 HTTP 504 和 Query timeout 错误码。查询被取消则返回 HTTP 500 和 Query cancelled 错误码。这两种情况在监控里分别对应 query/timeout/count 和 query/interrupted/count 指标。
二、五种查询类型
Druid 的原生查询分为三类。聚合查询包括 Timeseries、TopN、GroupBy,是最常用的三种。元数据查询包括 TimeBoundary、SegmentMetadata、DatasourceMetadata,用于查询数据源的元信息。其他查询包括 Scan 和 Search。本节按使用频率和重要程度依次讲前五种。
2.1 Timeseries:最轻量的时间聚合
Timeseries 查询按时间粒度返回聚合结果,适合趋势图。它假设查询的分组维度只有时间,不做其他维度分组。由于 Segment 按 __time 排序,Timeseries 能直接利用这个排序做高效聚合,是所有聚合查询里最轻量的。
{ "queryType": "timeseries", "dataSource": "wikipedia", "granularity": "hour", "aggregations": [ { "type": "longSum", "name": "total_edits", "fieldName": "count" } ], "intervals": ["2025-01-01T00:00:00.000/2025-01-02T00:00:00.000"]}granularity 控制结果的时间分桶粒度,设为 hour 意味着每小时返回一个数据点。Timeseries 还支持两个 context 参数(放在查询的 context 字段里,不是顶层字段):grandTotal 设为 true 时在结果末尾加一行总计(总计行没有 timestamp),skipEmptyBuckets 控制是否填充空时间桶。如果查询的时间范围内某个小时没有数据,默认会用聚合器的默认值(如 SUM 返回 0)填充该桶,设为 skipEmptyBuckets: true 则跳过空桶。写法是在查询体里加 "context": { "grandTotal": true, "skipEmptyBuckets": true },直接把这两个字段放到查询顶层不会生效。
官方文档的建议是:能用 Timeseries 就别用 GroupBy。如果查询只是按时间做聚合,Timeseries 比 GroupBy 快得多,资源开销也更小。
2.2 TopN:近似分组取前 N
TopN 查询按某个维度分组,根据指定指标排序后取前 N 个结果。它本质上是对单维度做近似 GroupBy 再排序。适用场景是维度基数大、只需要排名前 N 的结果,比如”过去一小时访问量 Top 10 的页面”。
{ "queryType": "topN", "dataSource": "wikipedia", "dimension": "page", "threshold": 10, "metric": "count", "granularity": "all", "aggregations": [ { "type": "longSum", "name": "count", "fieldName": "count" } ], "intervals": ["2025-01-01T00:00:00.000/2025-01-02T00:00:00.000"]}threshold 是要取的前 N 个数,metric 是排序依据。TopN 比 GroupBy 快的关键在于它的近似机制:每个数据进程(Historical 或 Peon)在本地计算自己的 Top K 结果,只把 K 条返回给 Broker 做全局合并。K 的默认值是 max(1000, threshold),可以通过查询上下文的 minTopNThreshold 调整。
近似性体现在:如果维度不同值的数量超过 K,每个进程只保留排名前 K 的局部结果,排在 K 之后的维度值不会上报给 Broker。这意味着最终结果在排名和聚合值上都可能有偏差。如果维度不同值少于 1000 个,TopN 结果在排名和聚合值上都是精确的。
什么时候 TopN 会出问题?官方文档给了一个典型场景:某个维度值在每个小时间窗口里都排在 Top 1000 的边缘,但跨多天聚合后它实际上排进了 Top 500。由于每个小时只保留前 K 个局部结果,这个维度值可能在某些小时被丢弃,最终聚合结果不包含它或排名不准。
如果需要精确结果,有两个选择。一是用 GroupBy 查询后自行排序,代价是内存开销大。二是分两步:先发一个 TopN 查询拿到近似的维度值列表,再用带维度过滤条件的 TopN 查询精确计算这些值对应的聚合指标。
2.3 GroupBy:精确分组聚合
GroupBy 是最灵活也最重的聚合查询。它支持多维度分组、having 过滤、limitSpec 排序和限制、多值维度分组。需要精确结果或 Timeseries 和 TopN 无法满足的查询,都用 GroupBy。
{ "queryType": "groupBy", "dataSource": "wikipedia", "granularity": "day", "dimensions": ["country", "device"], "limitSpec": { "type": "default", "limit": 5000, "columns": ["country", "data_transfer"] }, "aggregations": [ { "type": "longSum", "name": "total_usage", "fieldName": "user_count" }, { "type": "doubleSum", "name": "data_transfer", "fieldName": "data_transfer" } ], "having": { "type": "greaterThan", "aggregation": "total_usage", "value": 100 }, "intervals": ["2025-01-01T00:00:00.000/2025-01-03T00:00:00.000"]}GroupBy 的重在于内存。Timeseries 和 TopN 的中间结果有固定大小,GroupBy 的中间结果随分组键的数量线性增长。GroupBy 用一个堆外哈希表做聚合,哈希表的大小由分组键的基数(不同维度组合的数量)决定。维度组合越多,哈希表越大,内存压力越高。
Druid 的官方文档把 GroupBy 描述为”传统聚合引擎”:它给出精确的排名和聚合值,支持丰富的功能,但内存占用也最大。Timeseries 和 TopN 的结果始终在内存中计算,GroupBy 在内存不够时可以溢出到磁盘。
2.4 Scan:流式返回原始行
Scan 查询不做聚合,流式返回原始行数据。适合数据导出、明细查看、或者把 Druid 当数据源做下游处理。
{ "queryType": "scan", "dataSource": "wikipedia", "resultFormat": "list", "columns": ["__time", "page", "added", "user"], "intervals": ["2016-01-01/2017-01-02"], "batchSize": 20480, "limit": 100}resultFormat 支持三种:list(JSON 对象数组)、compactedList(紧凑数组,体积更小)、valueVector(向量格式,文档标注当前仅支持前两者)。batchSize 控制每次返回的行数,limit 限制总行数。Scan 查询可以直接发给 Historical 或正在跑流式摄入的 Peon,绕过 Broker 做并行数据拉取,适合大批量导出。
Scan 查询默认不参与缓存。配置里 unCacheable 的默认值就包含 scan,因为原始行数据量大且重复查询价值低。
2.5 TimeBoundary:元数据查询
TimeBoundary 查询返回一个数据源最早和最晚的数据时间点。它不做计算,只查 Segment 的元信息,常用于确认数据是否到位、时间范围是否正确。
{ "queryType": "timeBoundary", "dataSource": "wikipedia"}bound 可选 maxTime 或 minTime,不设则两者都返回。除了 TimeBoundary,还有 SegmentMetadata(查 Segment 的列信息)和 DatasourceMetadata(查数据源的 Segment 列表)两种元数据查询,用法类似。
2.6 查询类型选择
| 查询类型 | 分组维度 | 聚合精确性 | 内存开销 | 典型场景 |
|---|---|---|---|---|
| Timeseries | 仅时间 | 精确 | 低 | 趋势图、指标曲线 |
| TopN | 单维度 + 时间 | 近似(基数大于 K 时) | 中 | 排行榜、热门列表 |
| GroupBy | 多维度 + 时间 | 精确 | 高 | 复杂多维分析 |
| Scan | 无 | 不聚合 | 低 | 数据导出、明细查看 |
| TimeBoundary | 无 | 无聚合 | 极低 | 数据范围确认 |
Druid SQL 的翻译逻辑也遵循这个优先级。无 GROUP BY 的查询翻译成 Scan;只按时间分组的翻译成 Timeseries;单维度加 ORDER BY 和 LIMIT 的翻译成 TopN(除非 useApproximateTopN 为 false);其余聚合查询翻译成 GroupBy。理解这个翻译逻辑,写 SQL 时就能预判底层会跑哪种原生查询。
三、查询下推与内存限制
3.1 聚合下推到数据节点
scatter-gather 模式下,聚合计算下推到 Historical 和 Peon。每个节点在本地 Segment 上执行过滤、聚合,返回部分聚合结果。Broker 做最终合并。
这种设计的好处是数据局部性:计算发生在数据所在节点,不需要跨网络搬运原始数据。GroupBy 的 N-way merge 在 Broker 上完成,但 Broker 合并的是已经部分聚合的结果,不是原始行。
GroupBy 还有一层 limit pushdown 优化。Druid 会把 limitSpec 下推到 Historical 的 segment 级别,尽早裁剪掉不需要的中间结果,减少传给 Broker 的数据量。默认情况下,只有当 ORDER BY 的字段是分组键的子集时才做下推,因为下推不保证跨 segment 的精确排序。如果能接受近似结果,可以开启 forceLimitPushDown 强制下推。
3.2 GroupBy 的内存与磁盘溢出
GroupBy 的内存使用受几个配置参数控制。理解这些参数才能调优和排查内存问题。
GroupBy 聚合用堆外哈希表(off-heap hash table),哈希表的大小由 druid.processing.buffer.sizeBytes 控制。这个 buffer 是每个查询的中间计算缓冲区,Historical 和 Realtime 进程都用它做堆外聚合。druid.processing.numMergeBuffers 控制 merge buffer 的数量,它也是 GroupBy 查询的并发上限。
GroupBy 有两层堆内字典。druid.query.groupBy.maxSelectorDictionarySize 控制 segment 级别的字典大小,druid.query.groupBy.maxMergingDictionarySize 控制查询级别的合并字典大小。当字典超过限制时,GroupBy 开始用磁盘做聚合。
druid.query.groupBy.maxOnDiskStorage 控制每个查询能用的磁盘空间,默认是 0,意味着不做磁盘溢出。设为大于 0 后,内存不够时 GroupBy 会把中间结果排序后写到磁盘的 spill 文件,然后继续在内存里聚合。如果磁盘空间也用完了,查询失败,返回 Resource limit exceeded 错误。
druid.query.groupBy.maxSpillFileCount 限制 spill 文件数量。这个参数补充了 maxOnDiskStorage 的不足:后者限制总字节数,但一个大查询可能产生大量小 spill 文件,文件数本身也是资源开销(文件句柄、合并时的排序缓冲)。高基数维度加多个聚合器的查询容易触发这个问题。
GroupBy 的资源限制是排查查询失败的关键。查询返回 HTTP 400 和 Resource limit exceeded 错误码时,根据错误消息判断是哪个限制被打满:字典超限、堆外 buffer 超限、还是磁盘溢出超限。调优方向通常是增大 druid.processing.buffer.sizeBytes、调高 maxOnDiskStorage 允许溢出、或者改用 TopN/Timeseries 减少内存压力。
3.3 merge buffer 与查询并发
druid.processing.numMergeBuffers 是 GroupBy 查询的并发上限。这个参数对所有进程都重要:Broker 上的基本 GroupBy 不需要 merge buffer,但带子查询的 GroupBy 需要 1 到 2 个 merge buffer(单层子查询 1 个,多层嵌套 2 个),带 subtotalsSpec 的也需要 1 个。Historical 和摄入任务每个 GroupBy 查询需要 1 个 merge buffer,开启并行合并时需要 2 个。
这意味着如果 numMergeBuffers 设为 2,一个带嵌套子查询且用了 subtotalsSpec 的 GroupBy 就需要 3 个 buffer,可能直接打满并发限制。监控 query/failed 指标里的 Resource limit exceeded 和 Query capacity exceeded 错误,能发现这类问题。
四、Segment pruning 与缓存
4.1 时间裁剪与位图过滤
查询的裁剪分两层。第一层在 Broker 上做:根据 __time 条件确定涉及哪些时间区间的 Segment,跳过所有时间不匹配的 Segment。这层裁剪依赖 segmentGranularity 时间分片。
第二层在 Historical 的 Segment 内部做。Segment 的维度列有位图索引,结构是字典加位图的三段式(字典把维度值映射到整数 ID,列值列表存 ID,每个不同值对应一个位图标识哪些行包含该值)。查询的过滤条件会被翻译成位图运算:AND 对应位图交集,OR 对应并集。位图运算后得到匹配的行集合,Historical 只读取这些行的指标列做聚合。如果查询不需要某列,该列的数据完全不加载。
这两层裁剪是 Druid 对时序 OLAP 查询加速的基础。时间裁剪把扫描范围从全量数据缩减到匹配的时间区间,位图过滤把 segment 内的扫描范围从全行缩减到匹配行,列式存储保证不涉及的列零开销。
4.2 缓存分层
Druid 的查询缓存有两层:Broker 缓存和 Historical(或 Realtime)缓存。两层的缓存粒度不同。
Broker 缓存的是查询的最终结果。相同的查询(相同的数据源、过滤条件、聚合、时间范围)第二次发起时,Broker 直接从缓存返回结果,不转发到数据节点。Broker 缓存对重复查询多的场景收益大,比如仪表盘每隔几秒刷新一次。
Historical 缓存的是 per-segment 的局部聚合结果。同一个 Segment 被多个查询涉及时,如果局部结果还在缓存里,Historical 不需要重新计算。这层缓存对查询条件部分重叠的场景有用,比如多个查询都涉及同一个 Segment 但过滤条件不同,Segment 级别的计算结果可以复用。
缓存配置通过几个参数控制。useCache 控制是否读取缓存,populateCache 控制是否写入缓存。unCacheable 列出不参与缓存的查询类型,默认包含 Scan。maxEntrySize 限制单个缓存条目的大小,避免大结果撑爆缓存。
Realtime 进程的缓存默认关闭。druid.realtime.cache.useCache 和 druid.realtime.cache.populateCache 默认都是 false,因为实时数据在持续写入,缓存容易失效。Historical 的 Segment 是不可变的,缓存的有效性有保证。
Druid 整体支持本地缓存(local、caffeine)和远程缓存(memcached),Broker 和 Historical 都能用 memcached。但 Realtime(task executor / Peon)只支持本地缓存,如果给它配 memcached 这样的远程缓存会被忽略。这是设计取舍:实时数据的缓存一致性难以保证,远程缓存的延迟也抵消了部分收益。
4.3 Segment 本地缓存
除了查询结果缓存,Historical 还有 Segment 文件的本地磁盘缓存。Historical 从 Deep Storage 下载 Segment 到本地 druid.segmentCache.locations 指定的目录,通过 mmap 映射到内存服务查询。操作系统 page cache 决定哪些 Segment 数据常驻内存。
频繁访问的 Segment 会留在 page cache 里,冷数据会被换出。这也是 Historical 需要足够内存给 page cache 的原因。Segment 缓存和查询结果缓存是两个层面:前者缓存原始数据,后者缓存计算结果。两者叠加,Druid 对重复查询的响应可以做到亚秒级甚至从缓存直接返回。
五、数据生命周期管理
5.1 load rules:Segment 分配与副本
Coordinator 用 load rules 管理 Segment 在 Historical 上的分配。load rules 定义哪些 Segment 加载到哪个 tier(层级),每个 tier 放几份副本。
tieredReplicants 是 load rules 的核心字段,它是一个 tier 名到副本数的映射。默认配置是 {"_default_tier": 2},即所有 Segment 在默认 tier 放两份副本。如果定义了额外的 tier,比如 hot,可以配置不同时间段的 Segment 加载到不同 tier:
{ "type": "loadByPeriod", "period": "P1M", "includeFuture": true, "tieredReplicants": { "hot": 2, "_default_tier": 1 }}这个规则把最近一个月的 Segment 在 hot tier 放 2 份副本,在默认 tier 放 1 份。冷热分层的逻辑是:热数据访问频繁,放在配置更高的 Historical 上(SSD、大内存),副本数也多以分摊查询压力;冷数据访问少,放在普通 Historical 上,副本数可以少。
load rules 有三种类型。loadForever 对所有 Segment 生效,是默认规则。loadByPeriod 按相对当前时间的时间段匹配。loadByInterval 按固定时间区间匹配。规则按数组顺序执行,每个 Segment 只匹配第一条适用的规则。
5.2 retention rules:数据保留与删除
drop rules 控制 Druid 什么时候把 Segment 从集群卸载。Segment 被 drop 后不再加载到任何 Historical,但 Deep Storage 里的文件还在。要彻底删除 Deep Storage 里的数据,需要提交 kill task。
常见的保留策略是”保留最近 N 天数据”:用 dropBeforeByPeriod 删除 N 天前的数据,再配一条 loadForever 加载剩余数据。比如保留最近 30 天:
[ { "type": "dropBeforeByPeriod", "period": "P30D" }, { "type": "loadForever", "tieredReplicants": { "_default_tier": 2 } }]dropBeforeByPeriod 匹配时间段在指定 period 之前的 Segment。dropByPeriod 匹配包含当前时间段的 Segment(总是删最近数据)。dropByInterval 按固定区间删。dropForever 删所有未匹配前面规则的 Segment,通常放最后做兜底。
规则的顺序很重要。Coordinator 按数组顺序逐条匹配,每个 Segment 只匹配第一条适用的规则。如果你想让最近一个月的数据加载到 hot tier,30 天前的数据删掉,规则顺序应该是:先 loadByPeriod(P1M, hot) 加载最近一个月,再 dropBeforeByPeriod(P1M) 删更早的,最后 loadForever 处理未来数据。
drop rules 把 Segment 标记为 unused(不在集群加载),但 Deep Storage 里的文件不会自动删除。要彻底清理需要 kill task。如果不配 drop rules 也不配 kill task,Deep Storage 的数据会无限增长。
5.3 broadcast rules
broadcast rules 把 Segment 加载到所有 Broker 上,让 Broker 本地就能服务这些 Segment 的查询,不需要 scatter-gather 到 Historical。适用场景是把小维度的 lookup 表广播到 Broker,加速 JOIN 操作。broadcastForever、broadcastByPeriod、broadcastByInterval 三种类型对应不同的时间匹配方式。官方文档建议在测试环境使用,生产环境慎用。
5.4 冷热分层实践
冷热分层是 load rules 和 tieredReplicants 的组合应用。典型做法是定义两个 tier:hot tier 用大内存、SSD 的 Historical,cold tier 用普通磁盘的 Historical。load rules 配置最近数据在 hot tier 多放副本,老数据在 cold tier 少放副本。
冷热分层还能配合 Deep Storage 查询做更激进的省存储。把某些 Segment 的 tieredReplicants 设为空数组、useDefaultTierForNull 设为 false,这些 Segment 不加载到任何 Historical,查询时直接走 Deep Storage。牺牲延迟换存储成本,适合不常查的历史归档数据。
六、auto-compaction 运维
6.1 为什么需要 auto-compaction
这一节从运维角度讲 auto-compaction 的调优要点,重点是配置参数、与摄入任务的冲突处理,以及两种运行方式的取舍。
auto-compaction 解决的核心问题是小 Segment 膨胀。流式摄入按 taskDuration 周期产出 Segment,如果 taskDuration 短、分区数多,每天产出几十上百个小 Segment。每个 Segment 有固定的调度开销(Metadata Storage 记录、Coordinator 分配、查询时 per-segment 处理),小 Segment 太多会拖慢查询、增加 Coordinator 负载。
auto-compaction 读取一个时间区间的现有 Segment,合并成更大的新 Segment。合并后的 Segment 数量更少、大小更接近官方建议的 300 到 700 MB 区间,查询时 per-segment 处理开销降低。
6.2 配置要点
auto-compaction 的配置是动态的,不需要重启 Druid。核心字段包括:
dataSource:要压缩的数据源。skipOffsetFromLatest:避开最新时间段,减少与实时摄入的冲突。比如设为PT1H,compaction 只处理 1 小时以前的数据。taskPriority:compaction task 的优先级。默认摄入 task 优先,提高这个值可以让 compaction 抢占。granularitySpec:可选,调整压缩后的 segmentGranularity 和 queryGranularity。dimensionsSpec和metricsSpec:可选,在 compaction 时调整 schema。tuningConfig:继承原生批摄入的 tuning 参数,包括maxRowsPerSegment、maxNumConcurrentSubTasks等。
auto-compaction 跳过 segmentGranularity 为 ALL 的数据源。ALL 意味着整个数据源是一个时间块,没有细分,compaction 对它无意义。
6.3 与摄入的冲突处理
compaction task 和摄入 task 可能操作同一个时间区间。默认行为是摄入 task 优先:如果摄入 task 要往正在被 compaction 的时间区间写数据,compaction task 会失败退出。这是因为 compaction 会锁定时间区间,但摄入 task 默认优先级更高,会抢占锁。
处理冲突有三种方式。一是 skipOffsetFromLatest 让 compaction 避开最新数据,等实时 task 把数据 handoff 给 Historical 后再压缩。二是调高 taskPriority 让 compaction 优先。三是在低峰期跑 compaction,减少和实时摄入的碰面机会。
compaction task 失败后会重试。如果反复失败,检查是不是摄入 task 一直在占用该时间区间。监控 compact/task/count 指标能看到每次 auto compaction run 发出的 task 数,配合 task/failed/count 能看出 compaction task 是否正常执行。
6.4 compaction supervisor 与 Coordinator duty
auto-compaction 有两种运行方式。compaction supervisor(推荐)在 Overlord 上用 Supervisor 框架管理,支持 MSQ 任务引擎,响应更快,能通过 supervisor API 查看状态。传统方式作为 Coordinator 的 duty 运行。
两种方式效果类似,管理方式不同。compaction supervisor 还支持配置 engine 为 msq,用 MSQ 任务引擎做压缩。如果不确定用哪种,优先选 compaction supervisor,它的可观测性和管理体验更好。
七、监控与故障排查
7.1 关键监控指标
Druid 的监控指标通过 druid.monitoring 配置发射,可以对接 Prometheus 或其他监控系统。所有指标共享一组通用字段:timestamp、metric(指标名)、service(服务名)、host、version、value,以及各指标特有的维度。
查询相关指标是排查查询性能的基础:
| 指标 | 含义 | 关注点 |
|---|---|---|
query/time | 查询完成耗时(毫秒) | 正常值小于 1 秒,持续偏高要查原因 |
query/bytes | 查询返回的字节数 | 异常大可能说明查询没加时间范围或 limit |
query/segments/count | 查询涉及的 Segment 数 | 数量大说明时间范围宽或 Segment 粒度细 |
query/failed/count | 查询失败数 | 区分 timeout、Resource limit exceeded、cancelled |
query/timeout/count | 查询超时数 | 超时多说明查询太重或集群资源不够 |
query/cache/total/hitRate | 查询缓存命中率 | 命中率低说明查询模式分散或缓存配置有问题 |
摄入相关指标反映数据进入集群的健康度:
| 指标 | 含义 | 关注点 |
|---|---|---|
ingest/events/processed | 摄入的事件数 | 突降说明上游数据量减少或摄入 task 出问题 |
ingest/persists/count | 中间持久化次数 | 频繁说明 maxRowsInMemory 或 maxBytesInMemory 设得小 |
ingest/handoff/count | handoff 情况 | 卡住说明 Historical 没加载 Segment 或磁盘满 |
Segment 和 Coordinator 指标反映集群的数据拓扑健康度:
| 指标 | 含义 | 关注点 |
|---|---|---|
segment/count | 集群 Segment 总数 | 持续增长说明 compaction 没跟上 |
segment/assigned/count | 已分配到 Historical 的 Segment 数 | 和 available 对比看是否有 segment 没被加载 |
segment/loadQueue/count | 等待加载的 Segment 队列长度 | 队列长说明 Historical 加载慢或磁盘 IO 瓶颈 |
segment/dropped/count | 被 drop 的 Segment 数 | 异常增长检查 retention rules |
compact/task/count | auto compaction run 发出的 task 数 | 结合 task/failed/count 看 compaction 是否正常 |
7.2 常见故障
Segment 数量爆炸。 这是 Druid 运维最常见的问题。症状是查询变慢、Coordinator 负载升高、Metadata Storage 膨胀。根因通常是 taskDuration 设得太短、segmentGranularity 太细、或 auto-compaction 没开。排查方向:看 segment/count 趋势,确认 compaction 配置是否生效,必要时手动触发 compaction task 合并历史小 Segment。
Historical drop segment。 Historical 不断加载和卸载 Segment,集群抖动。根因可能是 Historical 磁盘空间不足、druid.segmentCache.size 设得太小、或 Coordinator 的负载均衡策略在频繁迁移 Segment。排查方向:看 Historical 磁盘使用率,看 segment/loadQueue/count 和 segment/dropped/count 指标,确认是不是 tieredReplicants 配置导致副本数超出磁盘容量。
摄入 task 失败。 Peon 或 Indexer 跑的摄入 task 频繁失败。常见原因包括 maxRowsInMemory 设得太大导致 OOM、completionTimeout 太短导致 Segment 还没发完就被杀、或 Kafka 分区数据倾斜导致某个 task 处理量过大。排查方向:看 task report 里的错误信息,看 ingest/persists/count 指标判断是不是 persist 太频繁。
Broker 内存压力。 GroupBy 查询多、merge buffer 不够、或查询返回结果太大。症状是 Broker OOM 或查询返回 Query capacity exceeded 错误。排查方向:看 query/failed/count 里的错误类型,确认是不是 numMergeBuffers 不够、或某个 GroupBy 查询的分组键基数太高。
Deep Storage 不写。 Segment 发布失败,Deep Storage 里没有新文件。根因通常是 Deep Storage 连接问题(S3 权限、网络不通)、或 Deep Storage 容量满。排查方向:看摄入 task report 里的发布错误,确认 Deep Storage 的连通性。
Metadata Storage 不一致。 Coordinator 看到的 Segment 状态和实际不一致,导致 Segment 重复加载或丢失。根因可能是 Metadata Storage 的事务出问题、或 Coordinator 的轮询延迟(druid.manager.segments.pollDuration 默认 1 分钟)。排查方向:用 Coordinator 的 API 查 Segment 状态,对比 Metadata Storage 里的记录,必要时手动标记 Segment 状态。
7.3 监控面板建议
Druid 的 Web Console(Router 提供)自带数据源管理、任务管理、Segment 查看功能,日常运维够用。对于生产监控,建议用 Prometheus 或 Datadog 对接 Druid 的 metrics,关注三条曲线:
- 查询延迟 P99(
query/time按 dataSource 和 queryType 维度聚合) - Segment 总数趋势(
segment/count) - 摄入吞吐(
ingest/events/processed按 dataSource 维度)
这三条曲线覆盖了 Druid 运维最核心的问题域:查询性能、数据拓扑健康度、摄入链路健康度。出问题时,先从这三条曲线判断故障域,再用更细的指标定位。
参考资料
- Apache Druid - Native queries - 原生查询总览,查询类型分类与错误码定义
- Apache Druid - Timeseries queries - Timeseries 查询语法、grandTotal 与 skipEmptyBuckets
- Apache Druid - TopN queries - TopN 近似算法机制、K 值与 minTopNThreshold
- Apache Druid - GroupBy queries - GroupBy 查询语法、内存调优参数与磁盘溢出
- Apache Druid - Scan queries - Scan 查询语法、resultFormat 与时间排序限制
- Apache Druid - TimeBoundary queries - TimeBoundary 查询语法
- Apache Druid - Query processing - 查询处理路径、Segment pruning 与 scatter-gather
- Apache Druid - SQL query translation - SQL 到原生查询的翻译逻辑与查询类型选择
- Apache Druid - Druid SQL API - SQL API 端点与请求格式
- Apache Druid - Using rules to drop and retain data - load rules、drop rules、broadcast rules 与 tieredReplicants
- Apache Druid - Compaction - compaction 机制与摄入冲突处理
- Apache Druid - Automatic compaction - auto-compaction 配置、compaction supervisor 与 skipOffsetFromLatest
- Apache Druid - Metrics - 全部监控指标定义与维度
- Apache Druid - Configuration - 缓存配置、processing buffer 与 merge buffer 参数
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






