理“ Druid 元数据”之乱
vivo 互联网大数据团队-Zheng Xiaofeng
一、背景
Druid 是一个专为大型数据集上的高性能切片和 OLAP 分析而设计的数据存储系统。
由于 Druid 能够同时提供离线和实时数据的查询,因此 Druid 最常用作为 GUI 分析、业务监控、实时数仓的数据存储系统。
此外 Druid 拥有一个多进程,分布式架构,每个 Druid 组件类型都可以独立配置和扩展,为集群提供最大的灵活性。
由于 Druid 架构设计和数据(离线,实时)的特殊性,导致 Druid 元数据管理逻辑比较复杂,主要体现在 Druid 具有众多的元数据存储介质以及众多不同类型组件之间元数据传输逻辑上。
本文的目的是通过梳理 Druid 元数据管理这个侧面从而进一步了解 Druid 内部的运行机制。
二、 Druid 元数据相关概念
2.1 Segment
Segment 是 Druid 管理数据的最基本单元,一个 Datasource 包含多个 Segment,每个 Segment 保存着 Datasource 某个时间段的数据,这个特定时间段的数据组织方式是通过 Segment 的 payload(json)来定义的,payload 内部定义了某个 Segment 的维度,指标等信息。
同一个 Datasource 的不同 Segment 的 payload 信息(维度、指标)可以不相同,Segment 信息主要包含下面几部分:
【时间段(Interval)】:用于描述数据的开始时间和结束时间。
【DataSource】: 用字符串表示,指定 segment 隶属于哪个 Datasource。
【版本(Version)】:用一个时间表示,时间段(Interval)相同的 Segment,版本高的 Segment 数据可见,版本低的 Segment 会被删除掉。
【Payload 信息】:主要包含了此 Segment 的维度和指标信息,以及 Segment 数据存在 DeepStorage 位置信息等等。
segment 主要组成部分
segment 内部数据样例
2.2 Datasource
Datasource 相当于关系型数据库的表,Datasource 的 Schema 是根据其可用的 Segment 动态变化的,如果某个 Datasource 没有可用的 Segment(used=1),在 druid-web 的 Datasource 列表界面和查询界面看不到这个 Datasource。
元数据库中 druid_dataSource 表并没有保存 Schema 信息,只保存了该 Datasource 对应 实时任务消费数据的偏移量信息,都说 Druid 的 Datasource 相当于关系型数据库的表,但是 Druid 中表(Datasource)Schema 信息,并不是定义在 druid_dataSource 元数据表里。
那么在 druid-web 页面上看到的 Datasource 的 Schema 信息是怎么来的呢?
其实它是实时根据该 Datasource 下所有 Segment 元数据信息合并而来,所以 DataSource 的 Schema 是实时变化的,
这样设计的好处是很好的适应了 Datasource 维度不断变化的需求在 :
Schema 的合并过程
2.3 Rule
Rule 定义了 Datasource 的 Segment 留存规则,主要分两大类:Load 和 Drop。
Load 表示 Segment 保留策略。
Drop 表示 Segment 删除策略。
Load/Drop 规则均有三个子类,分别是 Forever Load/Drop,Interval Load/Drop 以及 Period Load/Drop,一个 Datasource 包含 1 个或多个 Rule 规则,如果没有定义 Rule 规则就使用集群的 Default Rule 规则。
Datasource Rule 规则列表是有序的(自定义规则在前面,集群默认规则在后面),在运行 Run 规则时,会对该 Datasource 下所有可用的 Segment 信息,按照 Run 规则的先后顺序进行判断,只要 Segment 满足某个 Rule 规则,后面的规则 Rule 就不再运行(如图:Rule 处理逻辑案例)。Rule 规则主要包含下面几部分信息:
【类型】:类型有删除规则和加载规则。
【Tier 和副本信息】:如果是 Load 规则,需要定义在不同 Tier 的 Historical 机器副本数。
【时间信息】:删除或加载某个时间段的 Segment。
Rule 样例如下:
Rule 处理逻辑案例
2.4 Task
Task 主要用于数据的摄入(本文主要讨论实时摄入 kafka 数据的任务),在 Task 的运行过程中,它会根据数据时间列产生一个或者多个 Segment,Task 分为实时和离线任务。
实时任务(kafka)是 Overload 进程根据 Supervisor 定义自动生成;
离线任务(类型:index_hadoop,index_parallel)则需要外部系统通过访问接口方式提交。
每个任务主要包含下面几部分信息:
【dataSchema】:定义了该任务生成的 Segment 中有哪些维度(dimensionsSpec),指标(metricsSpec),时间列(timestampSpec),Segment 粒度(segmentGranularity),数据聚合粒度(queryGranularity)。
【tuningConfig】:任务在摄入数据过程中的优化参数(包括 Segment 生成策略,索引类型,数据丢弃策略等等),不同的任务类型有不同的参数设置。
【ioConfig】:定义了数据输入的源头信息,不同的数据源配置项有所不同。
【context】:关于任务全局性质的配置,如任务 Java 进程的 option 信息。
【datasource】:表示该任务为那个 Datasource 构造 Segment。
实时任务生成 Segment 案例
2.5 Supervisor
Supervisor 用于管理实时任务,离线任务没有对应的 Supervisor,Supervisor 与 Datasource 是一对一的关系,在集群运行过程中 Supervisor 对象由 Overlord 进程创建,通过 Overlord 接口提交 Supervisor 信息后,会在元数据库(MySQL)中持久化,Supervisor 内容与 Task 相似,可以认为实时 Task 是由 Supervisor 克隆出来的。
三、Druid 整体架构
前面笼统地介绍了 Druid 元数据相关概念,为了深入的了解 Druid 元数据,先从宏观的角度认识一下 Druid 的整体架构。
可以形象地把 Druid 集群类比为一家公司,以 Druid 不同组件类比这家公司中不同类型员工来介绍 Druid 集群,Druid 组件大体可以分为三类员工:领导层,车间员工和销售员工,如下图:
Druid 组件分类
领导层: 领导根据外部市场需求(Overlord 接收外部摄入任务请求),然后把生产任务下发到对应的职业经理人(MiddleManager),职业经理人管理团队(MiddleManager 启动 Peon 进程),下发具体生产任务给不同类型的员工(Peon 进程)。
车间员工: 生产员工(Peon) 负责生产产品(segment),仓库管理员(Coordinator)负责把生产出来的产品(segment)分配到仓库(Historical)中去。
销售员工: 销售员(Broker)从生产员工(Peon)获取最新的产品(segment),从仓库中获取原来生产的产品(segment),然后把产品整理打包(数据进一步合并聚合)之后交给顾客(查询用户)。
上面通过类比公司的方式,对 Druid 集群有了初步的整体印象。
下面具体介绍 Druid 集群架构,Druid 拥有一个多进程,分布式架构,每个 Druid 组件类型都可以独立配置和扩展,为集群提供最大的灵活性。
一个组件的中断不会立即影响其他组件。
下面我们简要介绍 Druid 各个组件在集群中起到的作用。
Druid 架构
Overlord
Overlord 负责接受任务、协调任务的分配、创建任务锁以及收集、返回任务运行状态给调用者。当集群中有多个 Overlord 时,则通过选举算法产生 Leader,其他 Follower 作为备份。
MiddleManager
MiddleManager 负责接收 Overlord 分配的实时任务,同时创建新的进程用于启动 Peon 来执行实时任务,每一个 MiddleManager 可以运行多个 Peon 实例,每个实时 Peon 既提供实时数据查询也负责实时数据的摄入工作。
Coordinator
Coordinator 主要负责 Druid 集群中 Segment 的管理与发布(主要是管理历史 Segment),包括加载新 Segment、丢弃不符合规则的 Segment、管理 Segment 副本以及 Segment 负载均衡等。如果集群中存在多个 Coordinator Node,则通过选举算法产生 Leader,其他 Follower 作为备份。
Historical
Historical 的职责是负责加载 Druid 中非实时窗口内且满足加载规则的所有历史数据的 Segment。每一个 Historical Node 只与 Zookeeper 保持同步,会把加载完成的 Segment 同步到 Zookeeper。
Broker
Broker Node 是整个集群查询的入口,Broker 实时同步 Zookeeper 上保存的集群内所有已发布的 Segment 的元信息,即每个 Segment 保存在哪些存储节点上,Broker 为 Zookeeper 中每个 dataSource 创建一个 timeline,timeline 按照时间顺序描述了每个 Segment 的存放位置。
每个查询请求都会包含 dataSource 以及 interval 信息,Broker 根据这两项信息去查找 timeline 中所有满足条件的 Segment 所对应的存储节点,并将查询请求发往对应的节点。
四、 Druid 元数据存储介质
Druid 根据自身不同的业务需要,把元数据存储在不同的存储介质中,为了提升查询性能,同时也会将所有元数据信息缓存在内存中。把历史数据的元数据信息保存到元数据库(MySQL),以便集群重启时恢复。
由于 Druid 拥有一个多进程,分布式架构,需要使用 Zookeeper 进行元数据传输,服务发现,主从选举等功能,并且历史节点会把 Segment 元数据信息存储在本地文件。
那么历史节点(Historical)为什么会把该节点加载的 Segment 元数据信息缓存在自己节点的本地呢?
是因为在历史节点发生重启之后,读取 Segment 的元数据信息不用去 Mysql 等其他元数据存储介质进行跨节点读取而是本地读取, 这样就极大地提升了历史节点数据的恢复效率。
下面分别介绍这些存储介质(内存、元数据库、Zookeeper、本地文件)里的数据和作用。
4.1 元数据库(MySQL)
MySQL 数据库主要用于长期持久化 Druid 元数据信息,比如 segment 部分元数据信息存在 druid_segments 表中,历史的 Task 信息存在 druid_tasks,Supervisor 信息存储在 druid_supervisors 等等。
Druid 部分服务进程在启动时会加载元数据库持久化的数据,如:Coordinator 进程会定时加载表 druid_segments 中 used 字段等于 1 的 segment 列表,Overlord 启动时会自动加载 druid_supervisors 表信息,以恢复原来实时摄入任务等等。
MySQL 元数据库表
4.2 Zookeeper
Zookeeper 主要存储 Druid 集群运行过程中实时产生的元数据,Zookeeper 数据目录大概可以分为 Master 节点高可用、数据摄入、数据查询 3 类目录。
下面介绍 Druid 相关 Zookeeper 目录元数据内容。
Zookeeper 元数据节点分类
4.2.1 Master 节点高可用相关目录
${druid.zk.paths.base}/coordinator: coordinator 主从高可用目录,有多个临时有序节点 编号小的是 leader。
${druid.zk.paths.base}/overlord: overlord 主从高可用目录,有多个临时有序节点 编号小的是 leader。
4.2.2 数据查询相关目录
${druid.zk.paths.base}/announcements:只存储 historical,peon 进程的 host:port,没有 MiddleManager,broker,coodinator 等进程信息,用于查询相关节点服务发现。
${druid.zk.paths.base}/segments:当前集群中能被查询到的 segment 列表。目录结构:historical 或 peon 的 host:port/${segmentId},Broker 节点会实时同步这些 Segment 信息,作为数据查询的重要依据。
4.2.3 数据摄入相关目录
${druid.zk.paths.base}/loadQueue: Historical 需要加载和删除的 segment 信息列表(不止只有加载),Historical 进程会监听这个目录下自己需要处理的事件(加载或删除),事件完成之后会主动删除这个目录下的事件。
${druid.zk.paths.indexer.base}=${druid.zk.paths.base}/indexer:关于摄入任务数据的 base 目录。
${druid.zk.paths.indexer.base}/announcements:保存当前存活 MiddleManager 列表,注意 historical,peon 列表不在这里,这里只存储摄入相关的服务信息,用于数据摄入相关节点服务发现。
${druid.zk.paths.indexer.base}/tasks Overlord 分配的任务信息放在这个目录(MiddleManager 的 host:port/taskInfo),等任务在 MiddleManager 上运行起来了,任务节点信息将被删除。
${druid.zk.paths.indexer.base}/status:保存任务运行的状态信息,Overlord 通过监听这个目录获取任务的最新运行状态。
4.3 内存
Druid 为了提升元数据访问的效率会把元数据同步到内存,主要通过定时 SQL 查询访问方式同步 MySQL 元数据或者使用 Apache Curator Recipes 实时同步 Zookeeper 上的元数据到内存如下图。
每个进程中的元数据不一样,下面一一介绍一下各个角色进程缓存了哪些数据。
Druid 进程元数据同步方式
4.3.1 Overlord
实时同步 Zookeeper 目录(${druid.zk.paths.indexer.base}/announcements)下的数据,使用变量 RemoteTaskRunner::zkWorkers(类型:Map)存储,每 ZkWorker 对应一个 MM 进程,在 ZkW orker 对象中会实时同步 Zookeeper 目录(${druid.zk.paths.indexer.base}/status/${mm_host:port})任务信息,使用 RemoteTaskRunner::runningTasks 变量存储。
默认每分钟同步数据库中 druid_tasks active = 1 的数据,使用变量 TaskQueue::tasks(类型:List )存储,在同步时会把内存中的 Task 列表与最新元数据里的 Task 列表进行比较,得到新增的 task 列表和删除的 task 列表,把新增的 Task 加到内存变量 TaskQueue::tasks,清理掉将要被删除的 task
4.3.2 Coordinator
默认每 1 分钟同步元数据库中 druid_segemtns 中列 used=1 的 segment 列表到变量 SQLMetadataSegmentManager::dataSourcesSnapshot。
默认每 1 分钟同步元数据库 druid_rules 表信息到 SQLMetadataRuleManager::rules 变量
使用 CoordinatorServerView 类(后面会介绍)实时同步 ${druid.zk.paths.base}/announcements,${druid.zk.paths.base}/segments 的数据,用于与元数据库中的 segment 对比,用来判断哪些 segment 应该加载或删除。
4.3.3 Historical
会实时同步 ${druid.zk.paths.base}/loadQueue/${historical_host:port} 下的数据,进行 segment 的加载与删除操作,操作完成之后会主动删除对应的节点。
Historical 通过上报 segment 信息到 ${druid.zk.paths.base}/segments 来暴露 segment。
4.3.4 MiddleManager
会实时同步 ${druid.zk.paths.indexer.base}/tasks/${mm_host:port}的数据,进行任务(peon)进程的启动,启动完成之后会主动删除对应的节点。
MiddleManager 上报 segment 信息到 ${druid.zk.paths.base}/segments 来暴露 segment。
4.3.5 Broker
使用 BrokerServerView 类实时同步 ${druid.zk.paths.base}/announcements,${druid.zk.paths.base}/segments 的数据,构建出整个系统的时间轴对象(BrokerServerView::timelines) 作为数据查询的基本依据。同步过程中类的依赖关系如下图。
下层的类对象使用监听上层类对象的方式感知 sement 的增删改,并做相应的逻辑处理, 会同时监听 ${druid.zk.paths.base}/announcements 和 ${druid.zk.paths.base}/segments 的数据的数据变化,通过回调监听器的方式通知到下层类对象。
zk 中 segment 同步到 Druid 进程过程中对象之间的监听关系
4.4 本地文件
本地文件的元数据主要用于恢复单个节点时读取并加载。
例如:Historical 节点第一个数据目录下的 info_dir 目录(如:/data1/druid/segment-cache/info_dir),保存了该节点加载的所有 segment 信息,在 Historical 进程重启时会读取该目录下的 segment 元数据信息,判断本地是否有该 segment 的数据,如果没有就去深度存储系统(hdfs)下载,数据下载完成后会上报 segment 信息到 Zookeeper(路径:${druid.zk.paths.base}/segments)。
五、Druid 元数据相关业务逻辑
由于 Druid 组件类型比较多,业务逻辑比较复杂,从整体到局部方式,从宏观到细节,循序渐进地了解 Druid 的业务逻辑,以便了解 Druid 元数据在业务逻辑中发挥的作用。
5.1 Druid 元数据整体业务逻辑
前面从整体了解了 Druid 集群各个组件的协作关系,下面分别从摄入任务管理、数据摄入、数据查询三个方面的业务逻辑来梳理元数据在 Druid 集群所起的作用。
5.1.1 摄入任务管理
摄入数据之前需要用户提交摄入任务,Overlord 根据任务的配置会相应命令 MiddlerManager 启动该任务的相关进程(peon 进程)用于摄入数据,具体流程如下图中数据序号顺序执行。
任务提交与管理
下面分别按照上图中数字序号顺序介绍 Druid 内部关于任务管理的业务逻辑:
① Overlord 进程收到任务提交请求之后,会把任务信息写入 druid_tasks 表,此时字段 active 等于 1。
② Overlord 分配任务给特定的 MiddleManager 节点,并把 task 信息写入 Zookeeper 目录(${druid.zk.paths.indexer.base}/tasks )下。
③ MiddleManager 进程监听当前节点在 Zookeeper 目录(${ruid.zk.paths.indexer.base}/task)需要启动的 task 信息。
④ MiddleManager 会以 fork 的方式启动 Peon 进程(task)此时 Peon 进程开始摄入数据,并把任务 Running 状态写入 Zookeeper 目录(${ruid.zk.paths.indexer.base}/status)。
⑤ Overlord 会实时监听 Zookeeper 目录(${ruid.zk.paths.indexer.base}/status)获取任务运行最新状态。
⑥ 任务完成后 Overlord 会把 task 状态信息更新到数据库表 druid_tasks,此时字段 active=0。
5.1.2 数据摄入逻辑
Druid 数据摄入逻辑
下面分别按照上图中数字序号顺序介绍 Druid 内部关于数据摄入的业务逻辑:
① Peon 进程在本地生产 segment 之后,会上传 segment 数据到深度存储 Hdfs。
② 插入一条 segment 元数据信息到元数据 druid_segments 表中,包括 segment 数据 hdfs 地址,Interval 信息,注意此时 used 字段为 1。
③ Coordinator 进程定时拉取 druid_segments 表中 used 为 1 的数据。
④ Coordinator 进程把 segment 分配信息写入 Zookeeper 目录:${druid.zk.paths.base}/loadQueue。
⑤ HIstorical 进程监听当前节点在 Zookeeper 目录(${druid.zk.paths.base}/loadQueue)获取需要加载的 segment 信息。
⑥ 从 Hdfs 下载 segment 数据,加载 segment。
⑦把已加载的 segment 的元数据信息同步到 Zookeeper 目录(${druid.zk.paths.base}/segments)。
5.1.3 数据查询逻辑
数据查询主要涉及到 Peon、Historical,Broker 三个角色,Broker 会根据 client 的查询请求中包含的 dataSource 和 interval 信息,筛选出需要查询的 segment,然后 Broker 作为客户端从 Peon 获取实时数据,从 Historical 获取历史数据,再根据查询要求,将两部分数据进一步聚合,如下图:
Druid 数据查询逻辑
5.2 Druid 元数据具体业务逻辑
有了前面对 Druid 集群整体认识之后,下面更为细致的探讨 Druid 元数据在各个组件之间发挥的作用。
如下图虚线箭头表示元数据的传输,下面按照图中数字序号介绍每个虚线箭头两端组件与元数据存储介质(MySQL、Zookeeper)之间的元数据,每条具体从组件对元数据存储介质包含读和写两方面来介绍,如下:
Druid 元数据业务逻辑
① 写:启动任务时写入 task 信息,提交实时任务时写入 supervisor 信息。读:broker 调用 overlord 接口时会查询不同状态下的 task 信息,进程重启时恢复 supervisor 信息。
② 写:分配任务到 MiddleManager 时,写入任务信息。读:同步正在运行任务的状态信息。
③ 写:写入当前节点任务状态信息到 Zookeeper,读:读取带启动或终止任务信息。
④ 写:任务启动后上报实时 segment 信息。
⑤ 读:coordinator 定时读取字段 used=1 的 segment 列表信息。
⑥ 写:coordinator 分配的 segment 信息,读:已分配的 segment 列表信息。
⑦ 写:已加载完成的 segment 信息,读:需要加载的 segment 信息。
⑧ 读:加载完成的 segment 信息,作为数据查询的依据。
六、总结
前面以整体到局部、抽象到细节的方式从四个方面(Druid 元数据基本概念、Druid 整体架构、Druid 元数据存储介质 Druid 元数据相关业务逻辑)介绍了 Druid 元数据在 Druid 集群中扮演的角色。
Druid 拥有一个多进程,分布式架构,每个组件只关注自己的业务逻辑和元数据,通过 RPC 通信或 Zookeeper 进行组件之间的解耦,每个 Druid 组件类型都可以独立配置和扩展,极大提供集群的灵活性,以至于一个组件的中断不会立即影响其他组件,下面对 Druid 元数据做一个总结:
Druid 元数据存储介质有内存、元数据库(MySQL)、Zookeeper、本地文件。
元数据库(MySQL)和本地的元数据起到备份、持久化的作用。Zookeeper 主要起到元数据传输桥梁,实时保存元数据的作用,同时把元数据同步到内存,极大提升了 Druid 数据查询和数据摄入的性能,而本地文件的元数据主要用于恢复单个节点时快速读取并加载到内存。
在 Druid 组件进程中会把 Zookeeper 和元数据库(MySQL)里的元数据分别通过实时同步和定时拉取的方式同步到进程的内存中,以提高访问效率。
保存在各个组件进程中内存的元数据才是当前集群中最新最全的元数据。
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/acae3741072dd3a5e2fdce297】。文章转载请联系作者。
评论