news 2026/9/23 14:05:10

Apache Druid 设计导览:实时分析数据库的架构、存储模型与适用场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Apache Druid 设计导览:实时分析数据库的架构、存储模型与适用场景
  • 数据库
  • OLAP
  • 大数据
  • 后端

【免费下载链接】druid

Apache Druid: a high performance real-time analytics database.

项目地址:https://gitcode.com/gh_mirrors/druid6/druid
点击查看免费下载

Apache Druid 是一个面向海量数据集的实时分析数据库,专为快速"切片与切块"式 OLAP 查询而设计,在实时摄入(real-time ingestion)、毫秒级到秒级查询延迟与高可用(high uptime)之间取得了良好平衡。本文以官方设计文档为骨架,结合仓库源码与配套文档,系统讲解 Druid 的定位、十大关键特性、分布式服务架构、以 Segment 为核心的存储模型,以及"何时该用、何时不该用 Druid"的决策指南,帮助读者建立从选型到原理的完整认知。

Druid 是什么

Apache Druid 是一个实时分析数据库(real-time analytics database),核心目标是在大型数据集上提供快速的切片与切块式分析,即 OLAP 类查询。它最常见的应用形态是:作为分析类应用 GUI 的数据库后端,或作为需要快速聚合能力的高并发 API 的数据层。Druid 对**事件型数据(event-oriented data)**最为契合——典型场景包括点击流、监控指标、日志、IoT 事件等。

一个典型的 Druid 数据流可以概括为:外部数据源(Kafka、HDFS、对象存储等)→ 摄入任务生成 Segment → 写入 Deep Storage → 由 Historical 服务加载对外提供查询,全程由元数据存储与 ZooKeeper 协同调度。

典型应用场景

官方文档给出了 Druid 最常见的应用领域,覆盖从互联网到传统行业的广泛分析需求:

应用场景说明
点击流分析(Clickstream analytics)分析网站与移动应用上的用户行为,理解导航路径、热门内容与用户参与度
网络遥测分析(Network telemetry analytics)监控与分析网络流量及性能指标,优化网络效率、定位瓶颈、保障服务质量
服务器指标存储(Server metrics storage)采集 CPU、内存、磁盘 I/O、网络活动等性能指标,监控服务器健康并优化资源分配
供应链分析(Supply chain analytics)利用供应链各环节数据优化库存管理、精简物流、预测需求、提升运营效率
应用性能指标(Application performance metrics)监控分析软件应用性能,定位改进空间、排查问题、保障用户体验
数字营销/广告分析跨社交、搜索、展示广告等渠道追踪分析数字营销与广告投放效果
BI/OLAP 分析从大数据集中挖掘洞察、生成报表、支撑数据驱动的业务决策
客户分析(Customer analytics)分析客户偏好、行为与购买模式,支撑个性化营销、改善服务与留存
IoT 分析处理分析 IoT 设备产生的数据,支撑自动化、优化与预测性维护
金融分析评估财务数据、管理风险、检测欺诈、辅助投资决策
医疗分析改善患者预后、优化医疗服务、降低成本、识别疾病趋势与模式
社交媒体分析监控分析点赞、分享、评论等社交行为,理解受众情绪、追踪品牌认知

这些场景的共同特征是:数据量大、持续追加写入、查询以聚合和报表为主、对查询延迟敏感——这正是 Druid 擅长的领域。

Druid 的十大关键特性

Druid 的核心架构融合了数据仓库(data warehouse)、时序数据库(timeseries database)与日志搜索系统(log search system)的设计思想。官方文档将其关键特性归纳为以下十点,下文逐条展开并补充实现层面的细节。

1. 列式存储格式(Columnar storage format)

Druid 采用列式存储:查询时只加载实际需要的列,因此"只取少数几列"的查询性能大幅提升。同时,Druid 会针对每列的数据类型做存储优化,以支撑快速扫描与聚合。关于列式布局的细节,可进一步阅读 Segment 文件结构 与 存储概览。

2. 可扩展的分布式系统(Scalable distributed system)

典型 Druid 部署横跨数十到数百台服务器,可以每秒摄入数百万条记录,同时保有万亿级记录,并将查询延迟维持在亚秒到数秒之间。分布式能力的基础是下文详述的多类服务角色(Coordinator、Overlord、Broker、Historical 等),可参见 架构文档。

3. 大规模并行处理(Massively parallel processing)

Druid 能够在整个集群范围内并行处理每一条查询。Broker 服务负责将查询路由到持有相关 Segment 的多个服务上并行执行,再合并结果返回,参见 Broker 服务。

4. 实时或批量摄入(Realtime or batch ingestion)

Druid 支持实时流式摄入(如 Kafka、Kinesis)与批量摄入(Hadoop 批量摄入、Native 批量摄入)。已摄入的数据立即可查——实时任务构建中的 Segment 在发布前即可被查询。

5. 自愈、自均衡、易运维(Self-healing, self-balancing, easy to operate)

运维人员通过增减服务器即可横向扩缩容,集群会在后台自动再均衡,且无需停机。若某台 Druid 服务器故障,系统会自动绕开故障路由数据,直到服务器被替换。Druid 被设计为可无计划停机地持续运行,配置变更与软件升级同样如此。

6. 云原生、容错、不丢数据的架构

摄入完成后,Druid 会将数据副本安全地存入 Deep Storage——通常是云对象存储、HDFS 或共享文件系统。即使所有 Druid 服务器全部宕机,也可以从 Deep Storage 恢复数据;对于只影响少数服务器的局部故障,副本机制(replication)保证恢复期间查询依然可用。

7. 支持快速过滤的索引(Indexes for quick filtering)

Druid 使用 Roaring 或 CONCISE 压缩位图索引实现跨多列的快速过滤与检索。位图索引在源码中以独立的位图抽象呈现,例如 BitmapFactory、RoaringBitmapFactory、WrappedRoaringBitmap 与 WrappedImmutableRoaringBitmap 等,位图的序列化逻辑集中在 BitmapSerde。默认使用 Roaring 压缩,也可在 IndexSpec 中配置为 Concise。

8. 基于时间的分区(Time-based partitioning)

Druid 首先按时间对数据分区,可选地再按其他字段做二次分区。时间范围查询只会访问与查询时间区间匹配的分区,从而带来显著的性能提升。时间分区的粒度由摄入时的segmentGranularity参数(位于granularitySpec)决定。

9. 近似算法(Approximate algorithms)

Druid 内置了近似 count-distinct、近似排名、近似直方图与分位数等算法,内存占用有界、且通常远快于精确计算;当精度比速度更重要时,Druid 也提供精确的 count-distinct 与精确排名。

10. 摄入时自动汇总(Automatic summarization at ingest time)

Druid 可选地在摄入阶段对数据做部分预聚合(即 Rollup),可以显著节省存储成本并提升查询性能。参见 Rollup 文档。

何时该使用 Druid(以及何时不该)

适合使用 Druid 的场景

官方文档列出的"匹配清单"包括:

  • 写入速率非常高,但更新较少。
  • 大部分查询是聚合与报表类查询(如 "group by"),也可能包含搜索与扫描查询。
  • 目标查询延迟在 100ms 到数秒之间。
  • 数据带有时间维度——Druid 针对时间做了专门的优化与设计取舍。
  • 可能有多张表,但每条查询只命中一张大的分布式事实表,可额外命中若干较小的 "lookup" 维表。
  • 存在高基数(high cardinality)数据列(如 URL、用户 ID),需要对其做快速计数与排名。
  • 希望从 Kafka、HDFS、扁平文件或 Amazon S3 等对象存储加载数据。

不适合使用 Druid 的场景

以下情况通常不建议选择 Druid:

  • 需要按主键对"既有记录"做低延迟更新。Druid 支持流式插入(streaming inserts),但不支持流式更新;更新只能通过后台批量作业完成。
  • 离线报表系统,对查询延迟不敏感。
  • "大 join":即一张大事实表 join 另一张大事实表,且能接受这类查询耗时较长。

分布式架构:六类服务与三种服务器形态

Druid 采用云友好的分布式架构,各服务可独立配置与伸缩,以获得最大的集群运维灵活性。其容错设计保证:某一组件故障不会立即影响其他组件。

服务角色总览

Druid 包含以下服务类型(详见 架构文档):

  • Coordinator:管理集群中数据的可用性(Segment 的加载、均衡与淘汰)。
  • Overlord:控制数据摄入工作负载的任务分配。
  • Broker:处理来自外部客户端的查询。
  • Router:将请求路由到 Broker、Coordinator 与 Overlord。
  • Historical:存储可查询的数据。
  • MiddleManager 与 Peon:执行数据摄入。
  • Indexer:作为 MiddleManager + Peon 任务执行系统的替代方案。

所有服务都可以在 Web Console 的Services页签中查看:

三种服务器类型与推荐部署

为便于部署,官方建议将服务组织为三种服务器类型:MasterQueryData

服务器类型承载服务职责
Master serverCoordinator + Overlord管理数据摄入与数据可用性:启动新的摄入作业、协调 Data server 上数据的可用性
Query serverBroker + Router提供用户与客户端应用交互的端点,将查询路由到 Data server(或可选地代理 Master server 请求)
Data serverHistorical + MiddleManager(可选 Indexer)执行摄入作业并存储可查询数据
Master server
  • Coordinator:监视 Data server 上的 Historical 服务,负责把 Segment 分配给具体服务器,并确保 Segment 在 Historical 之间均衡分布。
  • Overlord:监视 Data server 上的 MiddleManager 服务,是数据摄入的控制器,负责将摄入任务分配给 MiddleManager 并协调 Segment 发布。
Query server
  • Broker:接收外部客户端的查询并转发给 Data server;收到各子查询结果后合并返回给调用方。通常应查询 Broker,而不是直接查询 Historical 或 MiddleManager。
  • Router:提供位于 Broker、Overlord 与 Coordinator 之前的统一 API 网关。Router 服务还运行 Web Console——一个用于加载数据、管理 datasource 与任务、查看服务器状态和 Segment 信息的 UI。
Data server
  • Historical:负责历史数据的存储与查询(包括已在系统中提交的流式数据)。Historical 从 Deep Storage 下载 Segment 并响应针对这些 Segment 的查询,但不接受写入
  • MiddleManager:负责将新数据摄入集群,从外部数据源读取数据并发布新的 Druid Segment。
    • Peon:由 MiddleManager 派生的任务执行引擎。每个 Peon 运行在独立的 JVM 中,负责执行单个任务,且始终与派生出它的 MiddleManager 位于同一台主机上。Druid 使用独立 JVM 来隔离任务资源与日志;每个 Peon 同时只能运行一个任务,而一个 MiddleManager 可以管理多个 Peon(参见 MiddleManager 服务)。
  • Indexer(可选):MiddleManager + Peon 的替代方案。与"每个任务 fork 独立 JVM 进程"不同,Indexer 在单个 JVM 进程内以线程方式运行任务。它更易配置部署,并支持任务间资源共享;但作为较新的功能,目前仍标记为 实验性。通常二选一部署:要么 MiddleManager,要么 Indexer,而不是两者同时。

服务合设(Colocation)建议

按服务器类型合设服务通常能更好地利用硬件资源;超大规模集群则建议拆分到独立服务器以避免资源竞争:

  • Coordinator 与 Overlord:两者的负载都随集群 Segment 数量增长,其中 Coordinator 增长更明显。Segment 数量极大的集群可考虑拆分两者,为 Coordinator 的均衡负载留出资源。也可以通过设置druid.coordinator.asOverlord.enabled属性将两者合并为单一服务运行,参见 Coordinator 运维配置。
  • Historical 与 MiddleManager:在摄入或查询负载较高时,建议将两者部署在不同主机上以避免 CPU 与内存竞争。Historical 需要空闲内存用于内存映射 Segment,这也是分开部署的另一个理由。

三大外部依赖:Deep Storage、元数据存储与 ZooKeeper

除了内置服务,Druid 还依赖三个外部组件,它们被设计为尽量复用已有的基础设施。

Deep Storage

Deep Storage 是 Segment 的存放地,是一种不由 Druid 提供的存储机制,它直接决定数据持久性(durability):只要 Druid 进程能访问到该存储上的 Segment,无论丢失多少个 Druid 节点都不会丢数据;若 Segment 从该存储层消失,则其代表的数据即告丢失。配置的 load rules 决定 Segment 主要存在于 Deep Storage,还是 Deep Storage 与 Historical 进程的组合。

Druid 支持多种 Deep Storage 选项:

选项说明
本地存储(Local)适用于单机,或多台服务器共享文件系统(如 NFS)的场景。生产多服务器集群建议改用云对象存储或 HDFS
Amazon S3 或 S3 兼容存储druid-s3-extensions扩展支持
Google Cloud Storagedruid-google-extensions扩展支持
Azure Blob Storagedruid-azure-extensions扩展支持
HDFSdruid-hdfs-storage扩展支持

本地存储的配置项如下(写入common.runtime.properties):

属性可选值说明默认值
druid.storage.typelocal存储类型必须设置
druid.storage.storageDirectory任意本地目录存放 Segment 的目录,必须与druid.segmentCache.locationsdruid.segmentCache.infoDir不同/tmp/druid/localStorage
druid.storage.ziptruefalseSegment 以目录(false)还是 zip 文件(true)写入false

配置示例:

druid.storage.type=local druid.storage.storageDirectory=/tmp/druid/localStorage

Deep Storage 的用途包括:存放全部已摄入数据(加载到 Historical 的低延迟查询 Segment 也会保留在 Deep Storage 作为备份,仅存于 Deep Storage 的 Segment 可用于 从 Deep Storage 查询);以及在服务之间后台传递数据(即 Segment 文件)。Historical 在本地磁盘缓存 Segment 并提供查询,磁盘上的 Segment 是 Druid 低延迟查询性能的来源。你也可以直接查询仅存在于 Deep Storage 的 Segment,用一部分性能换取"无需扩容 Historical 即可查询更多数据"的能力。容量规划时注意两点:Deep Storage 需能容纳全部已摄入数据;Historical 磁盘需能容纳你希望加载到其上、需要低延迟查询的热数据。

元数据存储(Metadata storage)

元数据存储保存各类共享系统元数据,如 Segment 用量信息与任务信息,但不保存实际数据(参见 元数据存储文档)。集群部署通常使用 PostgreSQL 或 MySQL 这类传统 RDBMS;单机部署通常使用本地 Apache Derby 数据库。Derby 是默认元数据存储,但不适合生产环境,生产建议使用 MySQL 或 PostgreSQL(注意元数据存储必须 ACID 兼容,否则可能引发任务偶发失败等问题)。

元数据存储包含:Segment 记录规则记录(Segment 应落在何处)、配置记录任务相关表(由 Overlord 与 MiddleManager 管理任务时使用)与审计记录(规则等配置变更的审计历史)。

其中 Segment 表由druid.metadata.storage.tables.segments属性指定,由 Coordinator 轮询以确定集群中应可查询的 Segment 集合(即"used segments")。used列值为 1 表示应被集群加载使用,为 0 表示不应加载(保留元数据以支持回滚);payload列存储 Segment 元数据的 JSON blob,例如:

{ "dataSource":"wikipedia", "interval":"2012-05-23T00:00:00.000Z/2012-05-24T00:00:00.000Z", "version":"2012-05-24T00:10:00.046Z", "loadSpec":{ "type":"s3_zip", "bucket":"bucket_for_segment", "key":"path/to/segment/on/s3" }, "dimensions":"comma-delimited-list-of-dimension-names", "metrics":"comma-delimited-list-of-metric-names", "shardSpec":{"type":"none"}, "binaryVersion":9, "size":size_of_segment, "identifier":"wikipedia_2012-05-23T00:00:00.000Z_2012-05-24T00:00:00.000Z_2012-05-23T00:10:00.046Z" }

Derby 的配置方式(写入 Druid 配置文件):

druid.metadata.storage.type=derby druid.metadata.storage.connector.connectURI=jdbc:derby://localhost:1527//opt/var/druid_state/derby;create=true

自定义数据库连接池(DBCP)属性时,使用druid.metadata.storage.connector.dbcp.前缀,例如:

druid.metadata.storage.connector.dbcp.maxConnLifetimeMillis=1200000 druid.metadata.storage.connector.dbcp.defaultQueryTimeout=30000

注意usernamepasswordconnectURIvalidationQuerytestOnBorrow这几个属性必须使用druid.metadata.storage.connector.前缀设置。

只有以下进程会访问元数据存储:Indexing service 进程(如有)、Realtime 进程(如有)、Coordinator 进程。因此只需为这些机器授予访问权限(例如在 AWS 安全组中)。由于丢失的元数据无法恢复,官方还建议为元数据存储搭建高可用环境;相关清理操作可参考 元数据记录自动清理。

ZooKeeper

用于内部服务发现、协调与领导者选举(leader election),详见 ZooKeeper 文档。Broker 依赖 ZooKeeper 中的 Segment 分布元数据来路由查询,Historical 通过 ZooKeeper 的 load queue 路径获知加载指令、通过 served segments 路径对外宣告可用 Segment,Coordinator 也通过 ZooKeeper 向 Historical 下发加载/卸载指令。

存储模型:Datasource、时间块与 Segment

Druid 将数据存储在datasource中(类似传统 RDBMS 的表)。每个 datasource 按时间分区,并可再按其他属性分区。每个时间范围称为一个chunk(时间块,例如按天分区时的"一天")。chunk 内的数据再划分为一个或多个 Segment,每个 Segment 是单个文件,通常包含数百万行数据。由于 Segment 组织在时间块中,可以将其理解为排在一条时间线上:

一个 datasource 可能只有几个 Segment,也可能有几十万甚至上百万个 Segment。每个 Segment 由 MiddleManager 创建,创建时是可变的(mutable)且未提交(uncommitted)——数据一旦写入未提交的 Segment 即可查询。Segment 构建过程通过以下方式为后续查询加速:

  • 转换为列式格式
  • 建立位图索引
  • 压缩:String 列做字典编码并最小化 ID 存储;位图索引做位图压缩;所有列做类型感知压缩

Segment 定期被提交并发布到 Deep Storage,变为不可变,并从 MiddleManager 移交到 Historical 服务;同时,关于该 Segment 的元数据记录(自描述的元数据,包含 schema、大小、在 Deep Storage 中的位置等)被写入元数据存储,Coordinator 据此了解集群中有哪些数据可用。

索引与移交(Indexing and handoff)

索引(indexing)是创建新 Segment 的机制,移交(handoff)是 Segment 被发布并由 Historical 服务的机制。

摄入侧流程:

  1. 摄入任务启动并构建新 Segment,必须先确定 Segment 标识符:追加型任务(如 Kafka 任务、append 模式的 index 任务)通过 Overlord 的 "allocate" API 为已有 Segment 集合追加新分区;覆盖型任务(如 Hadoop 任务、非 append 模式的 index 任务)则锁定时间区间并创建新版本号与新 Segment 集合。
  2. 若为实时任务(如 Kafka 任务),此时 Segment 立即可查询(可用但未发布)。
  3. 任务读完后,将 Segment 推送到 Deep Storage,并通过向元数据存储写入记录完成发布。
  4. 实时任务为确保数据持续可查,会等待 Historical 加载该 Segment 后再退出;非实时任务则立即退出。

Coordinator / Historical 侧流程:

  1. Coordinator 周期性(默认每 1 分钟)轮询元数据存储,发现新发布的 Segment。
  2. 发现"已发布、标记使用但尚不可用"的 Segment 后,选择一个 Historical 并指示其加载。
  3. Historical 加载 Segment 并开始提供服务。
  4. 若摄入任务正在等待移交,此刻退出。

Segment 标识符

Segment 采用四部分标识符:

  • Datasource 名称
  • 时间区间(时间块对应区间,即摄入时指定的segmentGranularity
  • 版本号(通常为 Segment 集合开始创建时的 ISO8601 时间戳)
  • 分区号(datasource+interval+version 内唯一的整数,未必连续)

示例(datasourceclarity-cloud0,时间块2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z,版本2018-05-21T15:56:09.909Z,分区号 1):

clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z_1

分区号为 0(chunk 内第一个分区)的 Segment 会省略分区号:

clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z

Segment 版本与 MVCC

版本号提供了一种多版本并发控制(MVCC)机制,以支持批量覆盖写入。纯追加场景下,每个时间块只有一个版本;而覆盖写入时,Druid 会创建一组具有相同 datasource、相同时间区间但更高版本号的新 Segment——这对系统其余部分是一个信号:旧版本应从集群移除,由新版本替换。切换对用户几乎是瞬时完成的:Druid 先加载新数据(暂不允许查询),待全部加载完成后,将所有新查询切换到新 Segment,几分钟后再丢弃旧 Segment。

Segment 生命周期与可用性状态

每个 Segment 的生命周期涉及三个区域:

  1. 元数据存储:Segment 构建完成后,其元数据(通常只有几 KB 的小 JSON)写入元数据存储,该动作称为发布(publishing)。元数据记录中的布尔标志used控制 Segment 是否应可查询。
  2. Deep Storage:Segment 构建完成后即被推送至 Deep Storage(紧接在发布元数据之前)。
  3. 查询可用性:Segment 可在某些数据服务器上被查询——实时任务上、直接从 Deep Storage、或 Historical 服务上。

可以通过 Druid SQL 的sys.segments表 检查活跃 Segment 的状态标志:

  • is_published:Segment 元数据已发布到元数据存储且used为 true。
  • is_available:Segment 当前可查询(在实时任务或 Historical 上)。
  • is_realtime:Segment 仅在实时任务上可用。实时摄入的 datasource 通常先为true,发布移交后变为false
  • is_overshadowed:Segment 已发布(used为 true)且被其他已发布 Segment 完全遮蔽。通常为瞬态,此后used会被自动置为 false。

可用性与一致性

Druid 在架构上分离了摄入与查询。摄入侧,主要摄入方式均为拉取式(pull-based)且提供事务性保证(全有或全无地发布):

  • 受监管的 seekable-stream 摄入(Kafka、Kinesis):流偏移与 Segment 元数据在同一事务中提交到元数据存储,保证精确一次(exactly-once)发布;未发布数据可回滚,失败后从最后提交的偏移处继续摄入。
  • Hadoop 批量摄入:每个任务在单个事务中发布全部 Segment 元数据。
  • Native 批量摄入:并行模式下,子任务完成后由 supervisor 任务在单事务中发布全部元数据;简单(单任务)模式下由单个任务在完成后单事务发布。

部分摄入方式还提供幂等性保证:Kafka/Kinesis 因偏移与元数据同步更新而幂等;Hadoop 批量摄入在"输入源不是正在写入的 datasource"时幂等;Native 批量摄入在appendToExisting为 false 且输入源不是同一 datasource 时幂等。

查询侧,Broker 负责保证单次查询涉及一致的 Segment 集合:查询开始时根据当前可用情况选择恰当的 Segment 版本集合,并通过**原子替换(atomic replacement)**让用户视角的查询从旧数据瞬时切换到新数据,无一致性或性能影响。原子替换逐时间块进行;其基础是 core set 概念:时间块被覆盖时,创建更高版本号的新 core set,且必须全部可用后 Broker 才会用其替换旧集合,每个时间块每个版本只有一个 core set,每个时间块同时只使用一个版本。此外,实验性的 segment 锁定模式(将任务 context 中的forceTimeChunkLock设为 false)允许在同一版本下创建多个原子更新组,实现子集原子替换与"替换+追加"并行。

若多个 Historical 同时离线(超过副本因子),查询会只包含仍可用的 Segment,并在后台尽快在其他 Historical 上重载。

Segment 文件结构:字典、值列表与位图

Segment 文件是列式的:每列的数据布局在独立的数据结构中。通过单独存储每列,Druid 只扫描查询实际需要的列,从而降低查询延迟。列分为三种基本类型:时间戳(timestamp)、维度(dimension)与指标(metric):

时间戳与指标列是使用 LZ4 压缩的整数或浮点值数组。查询确定要选择哪些行后,解压这些列、取出相关行并应用聚合算子;若查询不需要某列,Druid 直接跳过该列数据。

维度列支持过滤与 group-by 操作,因此每个维度需要三种数据结构:

  • 字典(Dictionary):将值(始终按字符串处理)映射为整数 ID,使列表与位图值能够紧凑表示。
  • 值列表(List):使用字典编码的列值,供 GroupBy 与 TopN 查询使用;仅基于过滤器聚合指标的查询可以不用访问值列表。
  • 位图(Bitmap):列中每个不同值对应一个位图,指示哪些行包含该值。位图便于快速做 AND/OR 运算,因此能实现快速过滤,即倒排索引(inverted index)。

以示例数据中的 "Page" 列为例:

1: Dictionary { "Justin Bieber": 0, "Ke$ha": 1 } 2: List of column data [0, 0, 1, 1] 3: Bitmaps value="Justin Bieber": [1,1,0,0] value="Ke$ha": [0,0,1,1]

注意位图与字典、列表的不同:字典与列表随数据量线性增长,而位图区的大小是"数据量 × 列基数"的乘积——每个不同的列值都有一个位图。列数据列表中的每一行,只有一个位图具有非零项,这意味着高基数列的位图极其稀疏、因而高度可压缩。Druid 利用这一点,采用专为位图设计的压缩算法(如 Roaring 位图压缩)。

Null 值处理

默认情况下,Druid 以 SQL 兼容的 null 处理模式存储 Segment:String 列始终将 null 存为 ID 0(字典首位)并在位图索引中关联条目用于过滤 null;数值列也存储 null 值位图索引,用于聚合的 null 检查与过滤 null 匹配。

Druid 还保留了一种使用默认值代替 null 的传统模式(Druid 28.0.0 之前的默认行为,现已弃用并将在未来版本移除),可通过设置druid.generic.useDefaultValueForNull=true启用。传统模式下,摄入时创建的 Segment 具有如下特征:

  • String 列无法区分''null,二者等价;
  • 数值列无法表示 null 行,而是存储0

传统模式下数值列没有 null 值位图,Segment 尺寸可能略小,某些涉及数值列的查询因无需检查 null 位图而略有性能提升。

不同 schema 的 Segment 共存

同一 datasource 的不同 Segment 可以有不同的 schema。若某 String 列(维度)存在于一个 Segment 而不存在于另一个,涉及两者的查询仍然可用:默认模式下,缺少该维度的 Segment 表现得像该维度只含空白值;SQL 兼容模式下表现得像只含 null 值。同理,若某数值列(指标)缺失,对该指标聚合时表现得像指标不存在。

列格式与多值列

每列存储为两部分:Jackson 序列化的ColumnDescriptor,以及该列的二进制数据。ColumnDescriptor是 Druid 内部 ColumnDescriptor 类的 Jackson 序列化实例,借助 Jackson 的多态反序列化,可以以最小代码影响引入新的序列化方式;它包含列的一些元数据(如类型、是否多值)以及一组可反序列化其余二进制的序列化/反序列化逻辑。

多值列(multi-value column)允许单行在某一列包含多个字符串,可视为字符串数组。此时数据结构发生变化:某行的值列表项可能是一个数组(如[0,1]),且一个包含 n 个值的行在位图中对应 n 个非零项。

压缩

Druid 默认对 String、long、float、double 列的值块使用LZ4压缩,对 String 列与数值 null 的位图使用Roaring压缩。官方建议除非针对自身数据与查询模式做过实验、且证据表明非默认选项更优,否则使用默认值。Druid 也支持 Concise 位图压缩:对 String 列位图而言,Roaring 与 Concise 的差异在高基数列上最明显——Roaring 在匹配大量值的过滤上明显更快,但某些情况下 Concise 因 Roaring 格式开销而占用更小(匹配大量值时仍更慢)。压缩在Segment 级别配置(而非逐列),参见 IndexSpec。

Segment 组件

一个 Segment 包含以下文件:

文件说明
version.bin4 字节整数表示当前 Segment 格式版本。例如 v9 Segment 的值为 0x0, 0x0, 0x0, 0x9
meta.smoosh关于其他 smoosh 文件内容的元数据(文件名与偏移量)
XXXXX.smoosh拼接的二进制数据。文件合并减少了访问数据时必须打开的文件描述符数量;单文件不超过 2 GB,以保持在 Java 内存映射ByteBuffer的限制内。其中包含:每列各自的文件(含指向 Segment 时间戳的__time列),以及存放附加 Segment 元数据的index.drd文件

在代码层面,Segment 具有内部格式版本,当前为v9

Segment 大小建议

为了在高查询负载下运行良好,Segment 文件大小应保持在推荐区间300–700 MB。若 Segment 文件超出该范围,可考虑:调整 Segment 时间区间的粒度(segmentGranularity),或对数据做分区并/或调整partitionsSpec中的targetRowsPerSegment(该参数的合理起点约为 500 万行)。若同一时间区间存在多个 Segment(来自不同摄入作业),可使用 Compaction 将其合并为每个时间区间一个 Segment 以获得最佳性能;更多分区指导参见 Batch ingestion 文档的 Partitioning specification。

分片(Sharding)与查询完整性

同一时间区间与 datasource 可以有多个 Segment,它们构成该时间区间的一个block。根据分片所用的shardSpec类型,Druid 查询可能要求 block 完整才能完成。例如,若一个 block 由以下三个 Segment 组成:

sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0 sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1 sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2

则三个 Segment 必须全部加载后,针对2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z区间的查询才能完成。

Linear shard spec 是例外:它不强制"完整性",即使分片未完全加载,查询也可以完成。例如实时摄入用 linear shard spec 创建了三个 Segment,若只加载了两个,查询会返回这两个 Segment 的结果。

Segment 更新与替换的影响

Druid 用版本化实现 MVCC(注意与 Segment 格式版本相区分)。跨多个 Segment 区间的更新,只在各自区间内原子,而非整个更新原子。例如:

foo_2015-01-01/2015-01-02_v1_0 foo_2015-01-02/2015-01-03_v1_1 foo_2015-01-03/2015-01-04_v1_2

v2Segment 构建完成后即加载进集群,并在与v1重叠的时间段内替换v1。在v2完全加载前,集群可能混合存在v1v2

foo_2015-01-01/2015-01-02_v1_0 foo_2015-01-02/2015-01-03_v2_1 foo_2015-01-03/2015-01-04_v1_2

此时查询可能命中v1v2的混合集合。若以新 schema 重新索引数据,Druid 会为新 Segment 分配新版本 ID。

核心服务的工作原理

Coordinator:Segment 管理与均衡

Coordinator 主要负责 Segment 管理与分发:向 Historical 下发加载/卸载指令、加载新 Segment、淘汰过期 Segment、确保 Segment 按配置的副本数复制到多个 Historical 节点,并通过在节点间移动 Segment 保持负载均衡(参见 Coordinator 文档)。

  • 运行周期:Coordinator 周期性运行(间隔可配置),每次运行先评估集群当前状态再决定行动。它连接 ZooKeeper 获取集群信息,连接数据库获取 "used" Segment 信息与加载规则。
  • 分配策略:在分配未指派 Segment 前,会按容量对每个 tier 的 Historical 排序,容量最小的服务器优先级最高,未指派 Segment 总被分配给容量最小的服务以维持均衡。Coordinator 不直接与 Historical 通信,而是在 Historical 的 load queue 路径下创建临时信息,Historical 看到请求后加载 Segment 并开始服务。
  • 清理遮蔽 Segment:每次运行对比数据库中的 used Segment 与集群实际服务的 Segment,向 Historical 发送卸载请求;被遮蔽(版本过旧、数据已被新 Segment 替换)的 Segment 被标记为 unused,在下一次运行中从 Historical 卸载。此外还会清理满足特定条件(以 -INF 或 INF 结尾的 tombstone 段、不与任何被遮蔽 Segment 重叠、0 个 core 分区)的非遮蔽永恒 tombstone Segment。
  • Segment 可用性:若某 Historical 重启或不可用,Coordinator 会将其服务的所有 Segment 视为已丢弃;每个被丢弃的 Segment 会有一个带生命周期(lifetime)的过渡数据结构,在该生命周期内 Coordinator 不会重新指派它,从而避免短暂离线的服务数据被集群重新分布。
  • 均衡负载:每次运行计算每个 Historical 服务的 Segment 总大小,对每个 tier 找出利用率最高与最低的服务,计算两者利用率百分比差,若超过阈值则将若干 Segment 从最高利用率服务移动到最低利用率服务(每次运行的移动数量可配置上限;被移动 Segment 随机选择,且仅当移动后高低差确实降低时才移动)。
  • FAQ 要点:客户端从不直接联系 Coordinator;Historical 与 Broker 也完全不感知 Coordinator(分别通过 ZooKeeper 路径与元数据通信)。Coordinator 是否先于其他服务启动无关紧要——即使所有 Coordinator 全部宕机,集群仍可继续工作,只是数据拓扑不再变化。

Overlord:任务协调与黑名单

Overlord 负责接受任务、协调任务分发、围绕任务创建锁,并向调用方返回状态。它有两种运行模式(默认 local):

  • local 模式:Overlord 同时负责创建执行任务的 Peon,因此必须同时提供全部 MiddleManager 与 Peon 配置;适合简单工作流。
  • remote 模式:Overlord 与 MiddleManager 作为独立服务运行,可部署在不同服务器;若打算将 indexing service 作为全部 Druid 索引的单一入口,官方推荐此模式。

黑名单机制:若某 MiddleManager 的任务失败数超过阈值,Overlord 会将其列入黑名单(被黑名单的 MiddleManager 不超过 20%,并会周期性移出黑名单)。相关配置:

druid.indexer.runner.maxRetriesBeforeBlacklist druid.indexer.runner.workerBlackListBackoffTime druid.indexer.runner.workerBlackListCleanupPeriod druid.indexer.runner.maxPercentageBlacklistWorkers

自动扩缩容:若启用 autoscaling,任务在 pending 状态过久时可新增 MiddleManager;一段时间内未运行任何任务的 MiddleManager 可被终止。

Broker:查询路由与缓存

Broker 在分布式集群中路由查询:它解析 ZooKeeper 中发布的 Segment 分布元数据,据此路由查询,并合并各服务的返回结果(参见 Broker 文档)。

  • 路由原理:大多数查询包含表示时间范围的 interval 对象;Segment 也按时间区间组织并分布在集群中。Broker 首先基于 ZooKeeper 信息构建"世界视图"——为每个 datasource 建立 Segment 及其服务者的时间线(timeline);收到针对某 datasource 与区间查询时,在时间线中查找该区间对应的服务并转发查询。ZooKeeper 维护 Historical 与流式摄入 Peon 及其服务的 Segment 信息。
  • 缓存:Broker 采用 LRU 失效策略的缓存,按 Segment 粒度缓存结果。缓存可本地化,也可用 memcached 等外部分布式缓存共享。每次收到查询,Broker 先映射到 Segment 集合,命中缓存的 Segment 结果直接取出;未命中的转发给 Historical,返回后写入缓存。实时 Segment 从不缓存——实时数据持续变化,缓存结果不可靠。

Historical:Segment 存储与内存映射

Historical 负责历史数据(含已在系统中提交的流式数据)的存储与查询。它在本地磁盘(segment cache)缓存 Segment,并从该缓存与内存缓存提供查询。

  • 加载流程:Historical 从 Deep Storage 拉取 Segment 到本地 segment cache(位置与大小由druid.segmentCache.locations配置)。它不与其他 Historical 或 Coordinator 直接通信:Coordinator 在 ZooKeeper 的 load queue 路径创建临时条目,Historical 监听该路径;发现新条目后检查自身缓存,若无则从 ZooKeeper 获取 Segment 元数据(Deep Storage 位置、解压处理方式等),拉取处理后通过 ZooKeeper 的 served segments 路径宣告 Segment 可查,Broker 据此获知可用数据。启动时 Historical 会扫描本地缓存并立即宣告其中 Segment,以尽快提供查询。
  • 内存映射缓存:segment cache 使用内存映射(mmap),从操作系统底层消耗内存,使 Historical 可将部分 Segment 文件驻留内存以提升查询性能(受 JVM 堆、堆外/直接内存缓冲及其他服务影响)。查询时,若所需部分在内存映射缓存(page cache)中则直接复用;否则从磁盘读取(可能挤掉其他 Segment 数据)。空闲系统内存越接近druid.server.maxSize,Segment 数据越可能驻留内存、查询越快。该缓存独立于 查询级缓存。

服务启动命令

各服务的启动入口统一为org.apache.druid.cli.Main server <service>,例如:

org.apache.druid.cli.Main server coordinator org.apache.druid.cli.Main server broker org.apache.druid.cli.Main server historical org.apache.druid.cli.Main server middleManager

如何进一步学习

若想继续深入,官方文档建议按以下路径展开:

  • 通过 Quickstart 快速开始 亲手启动并体验 Druid;
  • 阅读 存储组件 与 Segments 了解数据存储细节;
  • 阅读 查询处理 获取 Druid 查询处理流程的高层概览;
  • 针对各服务的配置与调优,参见 Coordinator 配置、Overlord 配置、Broker 配置、Historical 配置、MiddleManager 与 Peons 配置,以及 基础集群调优;
  • 通过 服务状态 API 参考 查看各服务的 HTTP 端点;
  • 在数据建模前阅读 schema design。

总结来说,Druid 的价值在于把列式存储、位图索引、时间分区、并行处理与预聚合这五件事在分布式架构下系统地组合起来:它牺牲了"主键级低延迟更新"与"大表 join"两类能力,换来了事件型数据上的高摄入吞吐、秒级聚合查询与近乎无停机的运维体验。理解了服务分工、Deep Storage/元数据存储/ZooKeeper 三大外部依赖,以及 Segment 从创建、发布到移交的完整生命周期,就掌握了 Druid 设计与排障的两条主线。

  • 数据库
  • OLAP
  • 大数据
  • 后端

【免费下载链接】druid

Apache Druid: a high performance real-time analytics database.

项目地址:https://gitcode.com/gh_mirrors/druid6/druid
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 14:04:08

智能卷宗柜按需生产厂家、口碑好的智能卷宗柜厂家实力公司推荐

在政务与司法办公数字化转型的浪潮中&#xff0c;智能卷宗物证柜已经成为各级法院、政务单位规范卷宗管理、保障材料安全的刚需设备。不少负责采购的工作人员&#xff0c;都在网上搜索靠谱的智能卷宗柜实力供应企业&#xff0c;想要找到口碑好、产能足的合作方&#xff0c;也会…

作者头像 李华
网站建设 2026/9/23 14:02:46

GEO优化实战:从SEO到生成式引擎的内容引用策略

1. GEO到底在优化什么&#xff1a;从SEO到生成式引擎的范式迁移1.1 一个被误读的概念&#xff1a;GEO不是SEO的换皮很多人第一次听到GEO&#xff08;生成式引擎优化&#xff0c;Generative Engine Optimization&#xff09;&#xff0c;第一反应是"这不就是SEO换了个马甲吗…

作者头像 李华
网站建设 2026/9/23 14:00:59

电源防反接电路设计:PMOS/NMOS方案对比与选型指南

做硬件的朋友&#xff0c;恐怕都有过把电源插反的经历。我第一块独立开发的板子&#xff0c;就是在5V/3A的DC输入口上忘了加电源防反接电路&#xff0c;结果一次误插直接让板载电源芯片冒了烟。后来我把市面上常见的电源防反接电路方案挨个试了一遍&#xff1a;二极管串联、整流…

作者头像 李华
网站建设 2026/9/23 13:59:21

compromise-dates 插件全解析:用自然语言解析日期、时间与时长

compromise-dates 插件全解析&#xff1a;用自然语言解析日期、时间与时长 【免费下载链接】compromise modest natural-language processing 项目地址: https://gitcode.com/gh_mirrors/co/compromise compromise-dates 是 compromise 生态中最具实用价值的插件之一&am…

作者头像 李华
网站建设 2026/9/23 13:58:56

DPP是什么,哪些企业需要注册DPP,中国企业怎么注册DPP?

数字产品护照DPP&#xff1a;中国企业如何完成欧盟注册合规 2026年7月20日&#xff0c;欧盟数字产品护照&#xff08;Digital Product Passport&#xff0c;简称DPP&#xff09;中央注册系统正式上线运行。这意味着&#xff0c;DPP不再停留在政策讨论层面&#xff0c;而是进入了…

作者头像 李华
网站建设 2026/9/23 13:58:47

短剧APP开发核心技术解析与架构设计

1. 短剧行业现状与市场机会最近两年&#xff0c;短剧市场呈现爆发式增长。根据行业数据显示&#xff0c;2023年短剧市场规模已突破百亿&#xff0c;用户日均观看时长达到45分钟以上。这种介于短视频和长视频之间的内容形式&#xff0c;凭借其紧凑的剧情节奏和沉浸式观看体验&am…

作者头像 李华