背景
Druid 是一款可以针对时间序列数据,提供亚秒级交互式分析的分布式、实时 OLAP 分析型数据库。具有对实时数据流与离线数据集合的良好支持,对原始事件数据有很高的压缩率,这使得其具有良好的查询性能与存储效率。
Druid 是一个计算与存储相结合的系统。为了提供更好的查询性能与存储效率,Druid 会对所有原始事件数据在摄入时就进行预聚合,这一计算过程可以有效提升数据压缩率,在合理配置的情况下可以减少上百倍数据量。
存储方面数据使用 append 模式,已经摄入的数据将合成不可变的数据分片,下沉到历史节点,提供查询,通过数据的不可变性,提升查询的并发性。
Druid 使用特有的实时任务来摄入实时数据流,并提供实时窗口内数据的查询服务。同时拥有一套完整的任务调度系统对集群中所有的实时任务进行管理,称为 Indexing-Service 服务。
本文将主要介绍滴滴在 Druid 大规模使用过程中,针对该服务的进行的一些实践探索工作。
Indexing-Service 服务
Druid 使用实时任务对实时数据流进行消费、预计算、构建内存增量索引、构建为不可变的数据分片 ( Segment ) 并下沉到历史节点,期间同时负责在实时节点上数据的查询。
实时任务采用数据先写内存,定期刷本地磁盘,周期性下沉存储的模式,以保证实时任务上不会累积过多数据而导致查询性能恶化,同时只有下沉到历史节点的 Segment 才具有高可用性(由HDFS保证),所以实时任务实现为周期性的任务,每个任务消费一段时间的实时数据流,然后下推数据,结束任务进程,随后由后继的实时任务接着消费后续的实时数据流。
Indexing-Service 服务是 Druid 中管理和调度实时数据摄入任务的服务。其主要的功能包括:
- 选择一个 MiddleManager(通常是一个配置了若干任务槽位的物理机)来启动实时任务
- 提供若干实时任务在消费数据时需要依赖的 Druid 元数据操作相关的 API 服务
Indexing-Service 可以视为一个类似 yarn 的简易任务调度系统 +Druid 元数据操作 API 服务。
Kafka-Indexing-Service 服务
Indexing-Service 服务管理了任务调度分配,但是却没有实现实时任务的生命周期管理,所以社区引入了 Kafka-Indexing-Service 这一特性,该特性使得 Druid 具有了开箱即用的 Kafka 数据接入能力。
该特性主要在 Indexing-Service 服务的基础上引入了 Supervisor 角色,负责对一个数据源的实时消费任务的生命周期的管理,包括实时任务的启动、数据消费、offset 管理、任务结束等。
性能瓶颈分析
Druid 大部分组件有较好的高可用与横向扩展设计,而 Overlord 作为实时数据摄入系统的 Master 节点,并没有考虑太多横向扩展能力,而这正是我们在 Druid 大规模实践中遇到的一个显著的问题。
Overlord 节点在 Indexing-Service 服务中,负责任务的分配、跟踪任务执行状态、提供元数据修改 API,在 Kafka-Indexing-Service 服务中需要运行 Supvisor 线程,管理实时任务的整个生命周期。
随着集群规模的增大,实时任务的增长,Overlord 的逐渐出现任务调度不及时,Segment 集中生成时元数据 API 响应缓慢阻塞实时任务数据消费等问题。
经过分析,瓶颈主要存在于一下几点:
- Overlord 跟踪实时任务运行状态是通过 ZK 的 watch 机制,实时任务状态变化时,会触发一些 Overlord 的任务状态持久化操作,而 Druid 的元数据大量使用 JSON 作为存储格式,大段的 JSON 序列化反序列化的过程中较为耗时,这使得这些元数据操作更为耗时,当大量事件发生时,将会导致 watcher 回调线程排队,从而导致任务调度的不及时。
- Segment 生成时需要调用 Overlord 的一些元数据修改 API,这些 API 操作涉及较多的数据库操作,并发性能有限,当有大量 Segment 需要生成时,如早 8 点(UTC 时间的 0 点),该 API 将会成为性能瓶颈,从而导致集群整体的数据消费阻塞住。
为了解决以上问题,我们首先对数据库进行了调优,针对慢查询优化索引、清理无用数据以限制单表的大小、扩大 Druid 连接元数据库的连接池大小,这些优化在实时任务数在 600 以下时,效果还是很明显的,除了“早 8 点”数据会有些许延时外,基本可以满足日常的调度需求。然而随着业务的发展,任务数的不断膨胀,问题又逐渐开始恶化。
曾经考虑过修改元数据的序列化反序列化方式,用比 JSON 更为高效的方式,但是感觉那样需要改动的地方太多,对引擎核心代码改动太大,所以放弃了,而针对 ZK 回调逻辑以及元数据修改过逻辑的优化逻辑十分复杂,一直没有很好的优化方案。为了快速解决线上问题,最终决定引入类似 HDFS Federation 的机制,称之为 Overlord-Federation。
Overlord-Federation
Overlord-Federation 机制的想法就是:既然由于程序逻辑的原因,单个 Overlord 无法管理调度那么多任务,那就启用多个 Overlord 来管理,相当于给 Overlord 增加一个水平扩展的能力。与此同时,多 Overlord 之后,也就引入了 Overlord 层级的隔离空间,在日常任务分配与集群升级时,Overlord 也成为了一个可以灰度的角色。
Overlord-Federation主要包含量阶段的工作:
- 第一阶段称之为 Overlord Sharding,即对 Overlord 管理的任务进行分离,每一部分任务分配给一个 Overlord 来管理
- 第二阶段就是 Router 开发,Router 的主要目的就是提供与原生 Overlord 相同的 API,在用户侧无感知的前提下,将用户请求转发到合适的 Overlord 上面
Overlord Sharding
Overlord 上管理的资源有两种:Supervisor 与 Task。
我们引入了一个 Namespace 的概念,将所有的 Supervisor 和 Task 划分到不同的 Namespace 中,然后每一个 Namespace 中拥有自己的 Overlord 主备节点,来管理这些资源。
任务启动与查询
如上图所示:Supervisor 或 Task 提交时,需指定 Namespace 信息,并提交到该 Namespace 下的 Overlord API 上,Supervisor 或 Task 由此被打上 Namespace 标签。
Supervisor 的 Namespace 标签,将继承给其创建的 Task 上。
Overlord 将会在 Supervisor 或 Task 提交后,将其 Namespace 信息持久化,当 Overlord Failover 时,新启动的 Overlord 将只会从元数据库中恢复自己 Namespace 下的 Supervisor 和 Task。
所有 Namespace 下的 Overlord 共享集群所有 MiddleManager 上的任务槽,任务如何分配仍由 Overlord 上 Druid 原生提供的 selectStrategy 决定,与 Namespace 无关,当然由于在 Task 接口中添加了 Namespace 信息,在自定义的任务分配策略中,可以使用该 Namespace 信息作为任务分配的条件。
对同一个 Supervisor 或 Task 的 URD 操作,需要始终提交到其所属的 Namespace 下的 Overlord 上。
任务状态同步
如图所示:
- Task 启动后,其在 ZK 上发布的所有状态事件中,都会包含 Namespace 标签,Overlord 只处理与自己 Namespace 相同的事件,其他事件则直接丢弃不做任何处理。
- 另外 Task 通过 ZK 的服务发现机制获取到所有 Overlord 服务的地址,选择自己所属 Namespace 的 Overlord 进行 API 调用
Overlord Leader 选举
同一个 Namespace 下的多个 Overlord 依赖 ZK 做 leader 选举,所以需要在原先的 ZK 目录结构基础上多增加一个层 Namespace 目录,各个 Namespace 下 Overlord 的 leader 选举相互独立。
经过以上改造,集群的 Task 和 Supervisor 就可以运行在两个不同的 Namespace 下,分别由不同的 Overlord 进行管理。 Overlord 具有了水平扩展的能力。
但是不同的 Namespace 下的 Overlord 完全无关,从 Overlord 视图看,原来的一个集群仿佛变成了多个, Namespace 的概念也暴露给了客户端,而这不应该客户端需要关心的事儿,为了统一集群视图,对客户端屏蔽多 Namespace 实现的细节,所以引入了一个 Router。
Router
Router 的主要目的就是对客户端屏蔽掉 Namespace 的细节,统一集群的视图。
Router 的主要功能包括:
- 对于新增的 Supervisor 和 Task,通过可配置的策略进行 Namespace 的路由
- 对于已经存在 Supervisor 和 Task 进行的查询、修改、删除请求,根据历史路由信息,进行正确的 Namespace 寻址与请求投递
- 提供与原生 Overlord 相同的 API,包括 Supervisor 和 Task 的各种 CURD 操作,已经汇总信息查询
- 提供些新的 API,对 Namespace 相关信息进行查询
Router 分为四个部分:
1.各个 Namespace 的 Overlord 状态视图,视图同步采用超时为 5s 的长轮询方式,需要同步的信息有:
-
- 是否 leader?
- 如果是 leader,管理的 Supervisor 有哪些?
- 如果是 leader,那么管理的 Task 有哪些?
2.Overlord 选择器(NamespacedOverlordSelector),实现在多个 Namespace 下的 Overlord 的选择,同一个 Namespace 下选择 leader Overlord,不同 Namespace 的选择依赖 NamespaceSelectStrategy,选择策略包括:
-
- random:在多个 Namespace 中随机选取
- round-ronbin:在多个 Namespace 下轮询选取
3.OvelordRouterServlet,请求转发 Servlet,是 Router 对客户端提供与 Overlord 一致的 API,包括:
-
- Supervisor 方面 API,Supervisor 的创建、更新、停止、重置、Payload 查询、Status 查询、Log 查询
- Supervisor 列表 API 将转发到 NamespacedOverlordResource,返回所有 Namespace 下的 supervisor 列表
- Task 管理 API,Task 的创建、停止、Payload 查询、Status 查询、Log 查询
4.Router 提供的特殊 API(NamespacedOverlordResource)包括:
-
- Namespace 与其包含的 Overlord 的相关信息
- 从各个 Overlord 返回信息的汇总信息,包括:
- 所有 Namespace 下的 Supervisor
- 所有 Namespace 下的 Task
Router 从设计上应当保持轻量级,为了尽可能减少其外部依赖,其不读取 Druid 元数据,不依赖 ZK 同步信息,而采用长轮询的方式,只依赖 Overlord API 构建 Overlord 及其资源视图,同时也是无状态的,可以支持多节点部署,以保证高可用。
引入 Router 后,原本割裂的 Overlord 集群视图统一了起来,而且保持了原来的客户端使用方式。
后续会进一步完善 Namespace 的选择策略,是任务分布更加平衡;另外也需要考虑任务在 Namespace 之间分布的 Rebalance 机制,用于对已经分布不均衡的情况进行 Rebalance。
总结
通过 Overlord-Federation 机制的改造,使得 Druid Overlord 节点具备了水平扩展能力,单集群实时任务数量突破了原有的瓶颈。
同时也为 Overlord 角色引入了资源隔离的可能性,避免异常任务影响 Overlord 性能从而导致整个集群的任务收到影响。
本文作者: 刘博宇
滴滴云-为开发者而生