数据分析场景里有一类查询很常见:给定一个时间范围,按若干维度做分组聚合,要的是亚秒级响应,而且数据还在实时写入。用 Elasticsearch 做全文检索可以,但它的强项是文本召回,不是高并发聚合。用 ClickHouse 做列式 OLAP 也可以,但它的实时摄入体验不如专门的流式管道顺滑。Apache Druid 就是冲着这个夹缝定位来的:面向事件数据、按时间分片、列式存储加预聚合,写入后立即可查。
这篇讲 Druid 的架构与数据模型。要搞清楚的是 DataSource 与 Segment 的关系、列式存储里维度列和指标列各自怎么编码、六种节点类型各管什么,以及写入和查询在节点间怎么流转。这些是理解 Druid 实时性与容错性的基础:数据按时间分片存成不可变 Segment,摄入和查询由不同节点分工,写入后立即可查的能力就建在这套结构上。
一、Druid 是什么
1.1 实时分析数据库的定位
Druid 是一个开源的实时分析数据库,设计目标是针对大规模数据集做快速的切片切块分析(OLAP 查询)。它的核心架构融合了数据仓库、时序数据库和日志搜索系统三类思路:列式存储降低扫描量、时间分片加速按时间过滤的查询、流式摄入让写入数据立刻可查。
Druid 最擅长的数据是事件导向的。典型应用场景包括点击流分析、网络遥测、服务器指标存储、应用性能监控、数字广告分析、供应链分析等。这些场景的共同点是:数据按时间源源不断产生、查询几乎都带时间范围条件、需要按维度做聚合统计而不是逐行取明细。如果你有一张按分钟写入的访问日志表,要查”过去一小时各省份的 PV 和 UV”,Druid 就是冲着这类查询设计的。
理解 Druid 的关键特性,可以对照官方文档列出的几条:
- 列式存储格式,查询只加载所需列。
- 分布式可扩展,典型部署从几台到几百台服务器,摄入速率可达每秒百万条记录,保留万亿条记录时查询延迟仍在亚秒到几秒。
- 大规模并行处理,每条查询在集群内并行执行。
- 实时或批量摄入均可,摄入后立即可查。
- 自愈自平衡,加减节点时集群后台自动重平衡,不停机。
- 云原生容错架构,摄入后数据在 Deep Storage 存一份副本,所有 Druid 服务器都挂了也能从 Deep Storage 恢复。
- 索引加速过滤,用 Roaring 或 Concise 压缩位图索引。
- 按时间分片,时间相关查询只访问匹配时间范围的分片。
- 近似算法,内置近似去重、近似排序、近似直方图和分位数计算,内存占用可控且常比精确计算快得多。
1.2 与 Elasticsearch、ClickHouse 的定位差异
这三个系统经常被放在一起比较,但定位有明确区别。
ES 倒排索引与写入原理 讲过 ES 的核心数据结构是倒排索引,强项是全文检索和文本召回。Druid 不做全文检索,它的索引是维度列的位图索引,为加速过滤和分组聚合服务。ES 也能做聚合,但它的设计起点是”给定一个词,找到包含它的文档”,聚合是附加能力。Druid 的设计起点是”给定一个时间范围和一组维度,算出指标聚合值”,两者优化方向不同。
ClickHouse 和 Druid 都是列式 OLAP,但 ClickHouse 是通用分析数据库,Druid 是面向事件数据的实时分析数据库。ClickHouse 的列式存储和向量化查询引擎更通用,Druid 的强制时间分片和流式摄入对时序场景更友好。一个偏”什么分析都能做”,一个偏”事件数据的高并发实时聚合”。
| 维度 | Druid | Elasticsearch | ClickHouse |
|---|---|---|---|
| 设计起点 | 事件数据实时聚合 | 全文检索 | 通用列式 OLAP |
| 索引结构 | 列式 + 位图索引 | 倒排索引 | 列式 + 稀疏主索引 |
| 时间分片 | 强制,按时间分 Segment | 无原生时间分片 | 可按时间分区但不强制 |
| 实时摄入 | 原生流式(Kafka/Kinesis) | 近实时(refresh 1s) | 支持但不主打 |
| 典型查询 | 时间范围 + 维度聚合 | 关键词召回 + 聚合 | 复杂分析查询 |
| 适合场景 | 时序 OLAP、指标监控 | 全文搜索、日志分析 | 通用分析、宽表查询 |
二、数据模型:DataSource 与 Segment
2.1 DataSource 是逻辑表
Druid 的数据存在 DataSource(数据源)里。DataSource 类似关系数据库里的表,是逻辑概念。Druid 的数据模型同时兼具关系模型和时序模型的特点:有列、有行,但所有数据都按时间组织,查询几乎都带时间范围条件。
Druid 的列分三类:
- 主时间戳(Primary Timestamp):每个 DataSource 必须有一个主时间戳列,Druid 用它做数据分片和排序。无论源数据里时间戳字段叫什么,Druid 总是把它存到
__time列。时间范围查询靠它做分区裁剪,数据生命周期管理(按时间删除或覆盖数据块)也靠它。 - 维度(Dimensions):原样存储的列,用于分组、过滤。维度列在查询时可以做 groupBy、过滤、甚至做聚合,但摄入时不做预聚合。
- 指标(Metrics):以聚合形式存储的列。指标列在摄入时可以指定聚合函数,配合 rollup 机制把多行相同维度和时间的数据合并成一行,保留聚合后的指标值。
2.2 Segment 是物理数据单元
DataSource 是逻辑表,Segment 是 Druid 存储数据的物理单元。一个 Segment 就是一个按时间分片的、不可变的、列式存储的数据文件。
Druid 按 segmentGranularity 把数据按时间切成多个时间块(time chunk),每个时间块对应一个或多个 Segment。如果某个时间区间没有数据,那个区间就不存在 Segment。segmentGranularity 是摄入配置里 granularitySpec 的参数,常用值是小时和天。流式摄入用小时较常见,因为较小的分片能更快触发 compaction 合并小 Segment。
{ "type": "granularitySpec", "segmentGranularity": "HOUR", "queryGranularity": "MINUTE"}queryGranularity 控制 rollup 时时间戳截断的粒度。比如设为 MINUTE,同一分钟内维度值相同的行会被合并。segmentGranularity 控制时间分片大小,queryGranularity 控制行级聚合粒度,两者作用层次不同。
Druid 官方建议单个 Segment 文件大小在 300 到 700 MB 之间。如果 Segment 太大,查询时扫描开销大;如果太小,文件数量膨胀增加调度开销。Segment 太大时可以调小 segmentGranularity 或做二级分片(secondary partitioning),每个时间块拆成多个 Segment。
2.3 Segment 不可变
Segment 一旦生成并发布,内容不可修改。这个设计带来几个好处:
- 无锁读取,多个查询线程并发读同一个 Segment 不需要加锁。
- 缓存友好,不可变数据可以被操作系统 page cache 完整缓存。
- 压缩率高,不可变数据可以用更激进的压缩算法。
不可变带来的限制是更新和删除。Druid 不支持原地更新。要更新数据,通常是重新摄入(reindex)生成新版本的 Segment,用版本号管理切换。删除可以按时间块整体删除(通过数据保留规则或覆盖),但不是行级删除。Druid 用版本号实现一种多版本并发控制(MVCC),跨多个 Segment 时间区间的更新在每个区间内是原子的,跨区间不保证原子。
Segment 的标识符包含 DataSource 名、时间区间起止、版本号,如果还做了二级分片则包含分区号,格式为 datasource_intervalStart_intervalEnd_version_partitionNum。
例如 wikipedia_2015-01-01T00:00:00.000Z_2015-01-02T00:00:00.000Z_v1_0。如果同一时间区间有多个 Segment(做了二级分片),分区号递增:_0、_1、_2。重新摄入换 schema 后版本号变,比如 v1 变成 v2。
三、Segment 内部的列式存储
3.1 列式存储的基本结构
Segment 文件是列式的:每列的数据放在独立的数据结构里。查询时只读取涉及到的列,不涉及到的列直接跳过。这对分析查询收益巨大,因为一次聚合查询通常只选几列,而表可能有几十上百列。
三类列的存储方式不同。时间戳列和指标列是整数或浮点数数组,用 LZ4 压缩。查询定位到需要的行后解压、取出对应值、做聚合。如果查询不需要某列,Druid 跳过该列的全部数据。维度列因为要支持过滤和 groupBy 操作,结构更复杂。
3.2 维度列的三段结构
维度列在 Segment 内部由三个数据结构组成:
- 字典(Dictionary):把维度值(Druid 把维度值统一当字符串处理)映射到整数 ID。这样列数据和位图里不用存原始字符串,只存整数 ID,节省空间。
- 列值列表(List of column data):用字典编码后的整数 ID 表示这一列各行的值。groupBY 和 TopN 查询需要访问它。如果查询只是基于过滤做指标聚合,不需要访问这个列表。
- 位图(Bitmap):为列里每个不同的值建一个位图,标识哪些行包含该值。位图让过滤操作很快,因为 AND 和 OR 操作天然适合位图运算。这种结构也叫倒排索引。
举个例子,某行的 Page 列有四行数据:Justin Bieber、Justin Bieber、Ke$ha、Ke$ha。三段结构如下:
{ "Dictionary": { "Justin Bieber": 0, "Ke$ha": 1 }, "List": [0, 0, 1, 1], "Bitmaps": { "Justin Bieber": [1, 1, 0, 0], "Ke$ha": [0, 0, 1, 1] }}字典和列表随数据量线性增长。位图的大小是数据量乘以列基数(不同值的个数)的乘积,即每个不同值一个位图。高基数列(不同值多)的位图会非常稀疏,因而压缩率极高。Druid 用 Roaring bitmap 压缩算法处理高基数维度列的位图,在密集和稀疏场景下都能保持高效。
Druid 的位图索引和 ES 的倒排索引不是一回事,虽然 Druid 文档里也称位图为 inverted index。ES 的倒排索引是”词到文档列表”的映射,用 FST 词典加 posting list,核心目的是文本召回。Druid 的位图索引是”维度值到位图”的映射,核心目的是加速过滤和 groupBy,处理的都是结构化维度值,不做分词。
3.3 指标列的压缩
指标列按数据类型存储为整数或浮点数数组,用 LZ4 压缩。Druid 支持的指标类型在聚合器层面定义,常见的有:
- longSum:64 位整数求和。
- doubleSum:64 位浮点求和。
- floatSum:32 位浮点求和。
- count:行数计数。
- doubleMin/doubleMax、longMin/longMax、floatMin/floatMax:极值。
- doubleFirst/doubleLast、longFirst/longLast、floatFirst/floatLast:按时间列取首尾值。
除了精确聚合,Druid 还内置近似算法。近似去重方面,Druid 原生有 hyperUnique 和 cardinality 聚合器,两者都基于 HyperLogLog 算法。官方文档明确建议新场景评估 Apache DataSketches 扩展提供的 Theta Sketch 和 HLL Sketch,它们比经典的 hyperUnique 和 cardinality 精度更高、性能更好。hyperUnique 和 DataSketches 的 sketch 不互相兼容,迁移时要注意。此外还有近似直方图和分位数(DataSketches Quantiles Sketch)等近似聚合能力。
3.4 多值列与空值处理
Druid 的维度列支持多值,一行数据的某个维度可以有多个值。多值列在三段结构里表现为:列表里该行的值是一个数组而非单个整数,位图里该行在多个值的位图中都有非零位。多值列在 groupBy 时会产生笛卡尔积,使用时要注意。
空值处理方面,字符串列总是把 null 存为字典里的 id 0(第一个位置),并建一个对应的位图用于过滤。数值列也存一个 null 值位图索引,标识哪些行是 null,用于聚合时的 null 检查和过滤匹配。
3.5 Segment 文件的物理组成
一个 Segment 在磁盘上是多个文件,核心的有这几个:
version.bin:4 字节,表示 Segment 格式版本号。当前版本是 v9。meta.smoosh:元数据文件,记录其他 smoosh 文件的文件名和偏移。XXXXX.smoosh:Smoosh 文件,存放实际的列数据。Smoosh 是”smush”(压扁拼接)的意思,把多个列的数据拼接成大文件,减少文件描述符占用。单个 smoosh 文件不超过 2 GB,为了适配 Java 的内存映射 ByteBuffer 限制。smoosh 文件里包含每列一个独立的数据块,以及一个__time列(时间戳列)的数据块,还有一个index.drd文件存 Segment 额外元数据。
每列在 smoosh 文件里分两部分存储:一个 Jackson 序列化的 ColumnDescriptor(列元数据,如类型、是否多值),加上该列的二进制数据。ColumnDescriptor 用 Jackson 的多态反序列化机制,方便后续加新的序列化方式而不大改代码。
四、六种节点类型
Druid 的架构是分布式多进程的,不同进程类型各司其职。官方文档列出的服务类型有以下几种。为了便于理解,先按官方推荐的 Master、Query、Data 三类服务器归类。
4.1 Coordinator:管理数据可用性
Coordinator 监视 Data 服务器上的 Historical 服务,负责把 Segment 分配到具体的 Historical 节点,并确保 Segment 在各 Historical 间均衡分布。它的职责是数据可用性:哪些 Segment 应该加载、加载到哪个节点、什么时候卸载。
Coordinator 周期性轮询 Metadata Storage 里的 Segment 表,确定当前集群应该有哪些”used”状态的 Segment。然后根据负载规则(load rules)决定把这些 Segment 分配给哪些 Historical,或者从哪些 Historical 卸载。数据保留规则(retention rules)也由 Coordinator 执行,按时间区间把过期 Segment 标记为 unused。
4.2 Overlord:管理摄入任务
Overlord 监视 Data 服务器上的 Middle Manager 服务,是数据摄入的控制器。它负责把摄入任务分配给 Middle Manager,并协调 Segment 的发布。
Overlord 接收摄入任务(batch 或 streaming),把任务分发给 Middle Manager 的 Peon 执行。任务完成后,产出的 Segment 发布到 Deep Storage,元数据写入 Metadata Storage。Overlord 管理任务的整个生命周期:提交、运行、成功、失败。
在 Segment 数量非常大的集群里,Coordinator 和 Overlord 的工作负载都会随 Segment 数量增长,这时可以把两者分开部署。小集群可以用 druid.coordinator.asOverlord.enabled 把 Coordinator 和 Overlord 合并成一个服务运行。
4.3 Middle Manager 与 Peon:运行摄入任务
Middle Manager 负责把新数据摄入集群。它从外部数据源读数据,发布新的 Druid Segment。
Middle Manager 本身是任务调度器,实际执行任务的是它派生出的 Peon 进程。每个 Peon 跑在单独的 JVM 里,负责执行单个任务。Peon 总是和派生它的 Middle Manager 跑在同一台主机上。这种设计类似进程隔离,单个任务出问题不影响其他任务。
除了 Middle Manager + Peon 的模式,Druid 还提供 Indexer 作为可选替代。Indexer 不为每个任务 fork 独立 JVM,而是在单个 JVM 进程里以线程方式跑多个任务。Indexer 配置和部署更简单,资源在任务间共享更好,对流式摄入更友好。官方文档标注 Indexer 目前是实验性功能。生产环境通常三选一:MiddleManager、Kubernetes 上的 MiddleManager-less 摄入、或 Indexer,不会同时部署多种。
4.4 Historical:服务历史数据
Historical 负责历史数据的存储和查询,包括流式摄入后已经提交(committed)的稳定数据。Historical 从 Deep Storage 下载 Segment 到本地磁盘,用内存映射(mmap)方式服务查询。它不接收写入。
Historical 的设计是只读的。它启动时从 Coordinator 获取应该加载哪些 Segment,从 Deep Storage 下载到本地,然后服务查询。Segment 在本地磁盘和 Deep Storage 各存一份,Historical 挂了不影响数据安全,Coordinator 会把这些 Segment 调度到其他 Historical。
Historical 的查询性能依赖本地缓存。Segment 文件通过 mmap 映射到内存,操作系统的 page cache 决定哪些数据常驻内存。频繁访问的 Segment 数据会留在内存里,冷数据会被换出。所以 Historical 节点需要足够的内存给 page cache 用,这也是大集群里把 Historical 和 Middle Manager 分开部署的原因之一。
4.5 Broker:合并查询结果
Broker 接收来自外部客户端的查询,把查询转发到 Data 服务器,收集子查询结果后合并返回。客户端通常查 Broker,不直接查 Historical 或 Middle Manager。
Broker 的核心工作是结果合并。一条查询如果涉及多个 Segment(分布在不同 Historical 和 Middle Manager 上),Broker 把查询拆给所有相关节点,各节点返回局部聚合结果,Broker 做最终合并。这种 scatter-gather 模式是分布式 OLAP 数据库的常见查询处理方式。
4.6 Router:统一入口
Router 在 Broker、Overlord、Coordinator 前面做统一 API 网关。它把请求路由到对应的后端服务:查询请求到 Broker,管理请求到 Coordinator 或 Overlord。Router 还跑 Web 控制台,提供加载数据、管理 DataSource 和任务、查看服务器状态和 Segment 信息的 UI。
小集群可以不部署 Router,直接连 Broker。大集群或多团队共用时,Router 做统一入口和鉴权边界。
4.7 服务部署形态
Druid 把这些服务按 Master、Query、Data 三类服务器组织。Master 服务器跑 Coordinator 和 Overlord,管理摄入和可用性。Query 服务器跑 Broker 和 Router,服务查询请求。Data 服务器跑 Historical 和 Middle Manager,存数据并执行摄入。
这种分组不是强制的。小集群可以把多种服务混部在同一台机器上。大集群为了性能隔离,会把 Historical 和 Middle Manager 分开,因为摄入任务的 CPU 和内存开销可能影响 Historical 的查询性能。Druid 的设计原则是各服务可独立配置和扩缩容,一个组件挂了不立即影响其他组件。
五、Deep Storage 与 Metadata Storage
Druid 的存储分两层,各自职责不同。理解这两层的分离,是理解 Druid 容错和弹性架构的关键。
5.1 Deep Storage:Segment 的持久层
Deep Storage 是 Druid 存放 Segment 文件的共享存储。Druid 本身不提供存储基础设施,Deep Storage 是外部依赖。它的作用是:
- 存所有摄入进来的数据。Historical 节点本地缓存的 Segment 在 Deep Storage 也有一份,作为备份。
- 作为 Druid 服务间后台数据传输的中转。Segment 先写入 Deep Storage,再由 Historical 从 Deep Storage 下载。
- 支持”从 Deep Storage 直接查询”,不必把所有 Segment 都加载到 Historical,用性能换存储成本。
Deep Storage 的选项包括:
| 类型 | 说明 |
|---|---|
| Amazon S3(或 S3 兼容如 Minio) | 云上最常用 |
| Google Cloud Storage | GCP 环境 |
| Azure Blob Storage | Azure 环境 |
| HDFS | Hadoop 生态 |
| 本地/NFS | 单机或共享文件系统,多机生产环境不推荐 |
Deep Storage 的持久性决定了数据的安全性。只要 Deep Storage 里的 Segment 还在,即使所有 Data 服务器都挂了并重新部署,Druid 都能从 Deep Storage 恢复。如果 Segment 从 Deep Storage 消失,对应数据就丢了。
Deep Storage 的容量规划要注意:Deep Storage 要能装下所有摄入的数据,Historical 本地磁盘只需要装下需要低延迟查询的那部分数据。冷数据可以只留在 Deep Storage,不加载到 Historical,需要时走 Deep Storage 查询。
5.2 Metadata Storage:集群的元数据中枢
Metadata Storage 存的是 Druid 集群运行的元数据,不存实际数据。它也是外部依赖,Druid 用它记录:
- Segment 记录:哪些 Segment 应该在集群里可用。Coordinator 轮询这张表决定加载哪些 Segment。
- 规则记录:数据保留和负载规则,Coordinator 用它做 Segment 分配决策。
- 配置记录:运行时配置对象。
- 任务相关表:Overlord 和 Middle Manager 管理任务时用。
- 审计记录:配置变更的审计历史。
Metadata Storage 必须是 ACID 合规的。官方支持的选项:
| 类型 | 适用场景 |
|---|---|
| Apache Derby | 默认选项,单机用,不适用于生产 |
| MySQL | 生产环境 |
| PostgreSQL | 生产环境 |
Derby 是 Druid 默认的 Metadata Storage,但官方文档明确说它不适合生产。生产集群用 MySQL 或 PostgreSQL。访问 Metadata Storage 的进程只有 Coordinator、Overlord 相关进程和 Realtime 进程,Historical 和 Broker 不直接访问。
Metadata Storage 的高可用很重要。官方文档建议做高可用部署,因为元数据丢了无法恢复。Segment 数据在 Deep Storage 有备份,但 Segment 的元数据(哪个 Segment 属于哪个 DataSource、时间区间、版本、加载位置)在 Metadata Storage 里,丢了就意味着 Druid 不知道该加载哪些 Segment。
Deep Storage 和 Metadata Storage 的分工:Deep Storage 存 Segment 的二进制数据文件,Metadata Storage 存 Segment 的元信息(标识、时间区间、版本、加载位置)。Historical 启动时,Coordinator 从 Metadata Storage 拿到应该加载哪些 Segment 及其在 Deep Storage 的位置,再从 Deep Storage 下载到本地。
六、集群协同:写入与查询路径
6.1 写入路径
以流式摄入为例,数据从 Kafka 进入 Druid 的完整路径如下。
Overlord 把摄入任务分给 Middle Manager,Middle Manager 启动 Peon 执行。Peon 从 Kafka 读数据,在内存里增量构建 Segment(列式编码、建字典和位图索引)。Segment 达到一定行数或时间窗口后,Peon 把它发布:Segment 文件写入 Deep Storage,元数据写入 Metadata Storage。
Peon 在构建 Segment 期间就能服务实时查询(通过 Broker 查 Middle Manager 拿到正在摄入的数据)。Segment 发布后,Coordinator 轮询 Metadata Storage 发现新 Segment,指示 Historical 从 Deep Storage 下载并加载。加载完成后,该 Segment 由 Historical 服务查询,Peon 可以继续摄入新数据。这就是 Druid “写入后立即可查”的机制:实时数据由 Peon 服务,数据成熟后由 Historical 接管。
6.2 查询路径
查询从客户端到结果的完整路径如下。
客户端把查询发到 Router(或直接发到 Broker),Broker 根据 __time 条件确定涉及哪些时间区间的 Segment。Druid 按时间分片,查询的时间范围只匹配对应的 Segment,不需要扫描全量数据。Broker 把子查询发给拥有这些 Segment 的 Historical 和 Middle Manager(实时正在摄入的数据)。
各节点在本地 Segment 上执行聚合,返回局部结果。Broker 收集所有局部结果后做最终合并,返回给客户端。这种 scatter-gather 模式下,聚合操作下推到数据所在节点,Broker 只合并已经部分聚合过的结果,网络传输量比把原始数据拉到 Broker 再聚合小得多。
6.3 时间分片对查询的加速
Druid 的查询几乎都带时间范围条件。因为数据按 segmentGranularity 切成时间块,每个块对应一组 Segment,Broker 拿到查询的时间范围后,能直接定位到相关的 Segment,跳过所有时间不匹配的 Segment。这个时间分区裁剪是 Druid 对时序查询加速的基础。
如果查询的时间范围是”过去一小时”,而 Segment 按小时切分,只有最近一两个 Segment 需要扫描,哪怕集群里有几年的数据。这是 Druid 亚秒级响应的关键之一:不是查询跑得快,而是需要扫描的数据量被时间分片大幅缩减。
七、与 ES、ClickHouse 对比
7.1 存储结构对比
三个系统的底层存储结构有本质区别。
| 维度 | Druid | Elasticsearch | ClickHouse |
|---|---|---|---|
| 基本单元 | Segment(按时间分片,不可变) | shard 内的 segment(不可变) | data part(不可变,后台合并) |
| 时间分片 | 强制,segmentGranularity 控制 | 无原生时间分片 | 可按时间分区但不强制 |
| 列存储 | 全列式,维度列用字典 + 位图 | 部分列式(doc_values),核心是倒排索引 | 全列式 |
| 索引类型 | 维度列位图索引 | 倒排索引(FST + posting list) | 稀疏主索引 + 跳数索引 |
| 压缩 | LZ4 压缩列数据,Roaring 压缩位图 | FOR 压缩 posting list,Roaring 压缩位图 | 列式压缩 + 专用编解码器 |
| 预聚合 | 摄入时 rollup | 无摄入时预聚合 | 物化视图(AggregateFunction) |
Druid 和 ClickHouse 都是全列式存储,但 Druid 的维度列用字典编码加位图索引做过滤加速,ClickHouse 的索引体系更偏向排序键和稀疏主索引。ES 的核心是倒排索引,列式存储(doc_values)是补充,为了聚合和排序。
7.2 预聚合与实时性对比
Druid 的 rollup 是摄入时预聚合,把相同时间戳(按 queryGranularity 截断)和维度值的行合并,指标列做聚合。这能大幅减少存储的数据量,代价是丢失明细行。官方文档说 rollup 能把行数减少一到两个数量级。rollup 默认开启,不需要预聚合时设为 false。
ES 没有摄入时预聚合的机制。写入什么文档就存什么文档,聚合在查询时现算。ClickHouse 有物化视图和 AggregateFunction 类型,可以预聚合,但不是摄入时的内建机制,需要自己建视图。
实时性方面,Druid 的流式摄入(Kafka/Kinesis)数据写入后立即可查,由 Peon 直接服务实时查询。ES 的近实时靠 refresh(默认 1 秒)让新写入数据可搜。ClickHouse 的写入后可查延迟取决于引擎和插入批次,通常也是近实时。
7.3 选型建议
| 场景 | 推荐 | 理由 |
|---|---|---|
| 事件数据实时聚合监控 | Druid | 时间分片 + 流式摄入 + 亚秒级聚合 |
| 全文检索和日志搜索 | Elasticsearch | 倒排索引 + 分词,文本召回强项 |
| 通用分析、宽表查询 | ClickHouse | 列式向量化引擎,复杂分析强 |
| 时序指标存储和看板 | Druid 或 ClickHouse | 两者都适合,Druid 实时性更好 |
| 高并发低延迟聚合 API | Druid | 为高并发聚合查询优化 |
三个系统不是互斥关系,实际架构里经常共存。Druid 做实时指标看板,ES 做日志搜索,ClickHouse 做离线宽表分析,各取所长。
参考资料
- Apache Druid - Architecture - Druid 官方架构文档,涵盖服务类型与 Master/Query/Data 服务器分组
- Apache Druid - Segments - Segment 文件结构、列式存储、字典编码与位图索引、Segment 标识符
- Apache Druid - Deep storage - Deep Storage 的作用、选项与容量规划
- Apache Druid - Metadata storage - Metadata Storage 的表结构、Derby/MySQL/PostgreSQL 选项
- Apache Druid - Schema model - DataSource、主时间戳、维度与指标的数据模型定义
- Apache Druid - Data rollup - Rollup 预聚合机制与 perfect/best-effort 模式
- Apache Druid - Partitioning - segmentGranularity 时间分片与二级分片
- Apache Druid - Aggregations - 指标聚合器类型与近似去重(hyperUnique/DataSketches)
- Apache Druid - Introduction - Druid 的核心特性与典型应用场景概述
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






