mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
7504 字
21 分钟
Apache Druid 查询与运维调优
2025-07-13

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.json

Druid SQL 走 /druid/v2/sql 端点,请求体是 JSON 格式,包含 query 字段和可选的 contextparameters。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
}
}
Note

Druid SQL 用 useApproximateTopN 控制是否允许把 ORDER BY + LIMIT 的单维度分组查询翻译成 TopN。设为 false 时改用 GroupBy 以保证精确结果。这个参数是 SQL 查询性能和精度之间的重要开关,TopN 一节会细说。

1.2 scatter-gather 查询路径#

查询的 scatter-gather 路径上文已经提过,这里从查询处理的角度再梳理一遍,重点放在 pruning 和下推。

查询进入 Broker 后,处理分为四步:

  1. Broker 根据 __time 条件确定涉及哪些时间区间的 Segment。Druid 按 segmentGranularity 做时间分片,查询的时间范围只匹配对应的 Segment,这是第一步裁剪。除了时间,如果数据做了二级分片(按维度分区),Broker 还能用分片信息进一步裁剪。
  2. Broker 确定这些 Segment 分布在哪些 Historical 和 Middle Manager 上。实时正在摄入的数据由 Middle Manager 上的 Peon 服务,已发布的 Segment 由 Historical 服务。
  3. Broker 把子查询发给所有相关节点。各节点在本地 Segment 上执行聚合,返回局部结果。
  4. 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/countquery/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 可选 maxTimeminTime,不设则两者都返回。除了 TimeBoundary,还有 SegmentMetadata(查 Segment 的列信息)和 DatasourceMetadata(查数据源的 Segment 列表)两种元数据查询,用法类似。

2.6 查询类型选择#

查询类型分组维度聚合精确性内存开销典型场景
Timeseries仅时间精确趋势图、指标曲线
TopN单维度 + 时间近似(基数大于 K 时)排行榜、热门列表
GroupBy多维度 + 时间精确复杂多维分析
Scan不聚合数据导出、明细查看
TimeBoundary无聚合极低数据范围确认

Druid SQL 的翻译逻辑也遵循这个优先级。无 GROUP BY 的查询翻译成 Scan;只按时间分组的翻译成 Timeseries;单维度加 ORDER BY 和 LIMIT 的翻译成 TopN(除非 useApproximateTopNfalse);其余聚合查询翻译成 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 文件,文件数本身也是资源开销(文件句柄、合并时的排序缓冲)。高基数维度加多个聚合器的查询容易触发这个问题。

Important

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 exceededQuery 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.useCachedruid.realtime.cache.populateCache 默认都是 false,因为实时数据在持续写入,缓存容易失效。Historical 的 Segment 是不可变的,缓存的有效性有保证。

Note

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 处理未来数据。

Warning

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 操作。broadcastForeverbroadcastByPeriodbroadcastByInterval 三种类型对应不同的时间匹配方式。官方文档建议在测试环境使用,生产环境慎用。

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。
  • dimensionsSpecmetricsSpec:可选,在 compaction 时调整 schema。
  • tuningConfig:继承原生批摄入的 tuning 参数,包括 maxRowsPerSegmentmaxNumConcurrentSubTasks 等。

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 还支持配置 enginemsq,用 MSQ 任务引擎做压缩。如果不确定用哪种,优先选 compaction supervisor,它的可观测性和管理体验更好。

七、监控与故障排查#

7.1 关键监控指标#

Druid 的监控指标通过 druid.monitoring 配置发射,可以对接 Prometheus 或其他监控系统。所有指标共享一组通用字段:timestampmetric(指标名)、service(服务名)、hostversionvalue,以及各指标特有的维度。

查询相关指标是排查查询性能的基础:

指标含义关注点
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中间持久化次数频繁说明 maxRowsInMemorymaxBytesInMemory 设得小
ingest/handoff/counthandoff 情况卡住说明 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/countauto 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/countsegment/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 查询与运维调优
https://blog.souloss.cn/posts/middleware/apache-druid/apache-druid-query-and-operations/
作者
Souloss
发布于
2025-07-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时