kischn

本篇译自 Druid 项目白皮书部分内容( https://github.com/apache/incubator-druid/tree/master/publications/whitepaper/druid.pdf),如果有兴趣可看细看原 pdf【初次翻译多多包涵】

  一个 Druid 集群包含多种特定功能的节点, 我们相信这种设计能够分散业务并且简化整个系统的复杂性。不同节点类型相互独立得运行,它们之间有很小的干扰。因此,集群内部的通信故障对数据的可用性影响很小。

  为了解决复杂数据分析的问题,不同类型的节点共同组成一个完整可用的系统。Druid 这个名字来自许多角色扮演游戏中的德鲁伊角色类:它是一个形状移位器,能够采用多种不同的形式来实现一组中的各种不同角色的组成和流动。Druid 集群中的数据如图所示:

 

1. 实时节点 Real-Time Nodes

  实时节点封装了用于摄取和查询事件流的功能,事件经这些节点建立索引后可以立即用于查询。节点仅关注一小段时间范围内的事件,然后周期性的将这段小时间范围内的不可变的一批事件交给 Druid 集群内的其他专门用于处理一批批不可变事件的节点。实时节点利用 ZooKeeper 与其他节点进行协调,节点在 ZooKeeper 中声明他们的在线状态和他们提供的数据。

  实时节点维护一个在内存中的索引缓冲用于所有即将发生的事件。这些索引随着事件的摄取会增量式的增加,而且索引可以直接用于查询。Druid 在 JVM 的堆缓存中查询时表现为一个行存储,为了避免堆溢出,实时节点会周期性的或者在行数超过最大限制后将内存中的索引持久化到磁盘中。这个持久化程序会将内存中的缓冲数据转换成一个面向列存储的数据格式。每一个持久化后的索引都是不可变的然后实时节点加载持久化后的数据到非堆内存中这样数据仍然可以在实时节点中被查询到。

  每一个实时节点都会周期性的调度一个后台任务去查找所有持久化到本地的索引,然后把一段时间内实时节点生成的索引合并到一起形成一个包含所有被合并索引的所有事件的块,我们把这种块称作 段(segment) 。实时节点在将数据交出阶段会将 segment 上传到被称作 deep storage 的永久备份存储中,一般是一个分布式的存储系统比如 Amazon S3 或者 HDFS。

 

 

  上图展示了实时节点的操作顺序,节点在 13:37 时启动然后只接受当前小时内和下个小时的事件数据。当事件被摄取后节点就声明它正在提供一个 13:00 到 14:00 的 segment,每隔10分钟(持久化周期可配置)节点都会将内存中的索引冲刷并持久化到磁盘上。在接近当前小时的结尾时节点可能会收到 14:00 到 15:00 的数据,当此事发生时节点会准备提供下一个小时的数据并创建一个新的内存索引,然后节点会声明它又提供了一个 14:00 到 15:00 的 segment。此时,节点不会立即把 13:00 到 14:00 的索引给合并,而是会等待一个可配置的窗口期来接收比较慢的那些 13:00 到 14:00 的数据。这窗口周期最小化了延迟传递过来的数据的丢失的风险。在窗口周期的末期,实时节点合并所有的 13:00 到 14:00 的持久化的索引文件到一个不可以变的 segment 中然后交给其他节点,当该 segment 在 Druid 集群的其他节点被加载和可被查询后,实时节点将清掉节点中保存的该 segment 的信息并且声明不再提供此数据。

可用性和扩展性

  实时节点作为数据的消费者,需要对应的生产者来提供数据流。通常,为了数据的稳定性我们在数据生产者和实时节点中间架设一个消息总线如 Kafka, 实时节点通过从消息总线读取事件来摄取数据,事件从生成到消费所用的时间一般会在毫秒级。

  消息总线的用途一般分为两个方面。首先,消息总线为即将到来的事件作为一个缓冲。像 Kafka 这样,一个消息总线维护一个 offset 来表示一个消费者(实时节点)消费了多少的事件数据流,实时节点每次在将数据持久化到磁盘上时更新 offset。当遇到失败或者恢复这种场景时,如果节点没有丢失自己的数据它就可以重新从磁盘加载所有的持久化的索引然后从上次的提交的 offset 那里开始继续读取数据。从上次提交的 offset 那里开始摄取数据这种方式大大减少了一个节点恢复的时间,在实践中我们看到一个节点从类似的失败中恢复只用了几秒。

  消息总线用途的第二方面就是扮演多个实时节可以读取事件数据的端点服务。多个实时节点可以通过消息总线获取同样的数据用来创建一个事件副本,当遇到一个节点硬盘坏掉以至于完全坏掉时,事件副本可以保障没有数据丢失。一个摄取节点支持数据流分区这样多个实时节点每个节点可以摄取数据流的一个分区,还可以非常简单的添加额外的实时节点,在实践中该模式允许一个最大的 Druid 集群能够以大约 500 MB/s (150,000 events/s or 2 TB/hour) 的速度消费原始数据。

2. 历史节点 Historical Nodes

  历史节点封装了加载和提供不可变 segment 的查询功能。在许多真实工作流程中,多数加载进 Druid 的数据都是不可变的,因此,历史节点是是 Druid 集群里面典型的主工作节点。历史节点采用了无共享架构,多个节点之间没有单点竞争,每个节点都不用感知其他节点的存储而且操作简单,他们只知道如何加载、卸载和提供不可变 segment。

  和实时节点一样,历史节点也在 ZooKeeper 中声明它们的上线状态和提供的数据。历史节点加载和卸载数据的指令也是通过 ZooKeeper 来发送,指令信息中包含了 segment 在 deep storage 的位置以及如何解压和处理 segment。在一个历史节点下载一个特定的 segment 之前它首先会检查本地缓存查看已加载的 segment,如果要加载的 segment 不在缓存中,历史节点将会去 deep storage 中下载 segment。一旦过程完毕,segment 就会被在 ZooKeeper 中声明出来然后 segment 就可被查询。本地缓存也使得历史节点可以快速的更新和重启,历史节点在重启时会检查本地缓存然后立即提供所有它找到的数据。

  历史节点支持读一致性因为他们只处理只读的数据,不可变数据也使得一个简单的并发模型成为可能:历史节点可以并发的扫描和聚合不可变的块而不被阻塞。

层 (tiers)

  历史节点可以通过一个有识别性的 tier 进行分组,可以为每个 tier 分配不同性能和错误容忍性的参数。层级化后的节点用途可以使得不同高低优先级的 segment 根据重要程序被分布到不同的节点。例如,我们可以给一个拥有多核心和大内存容量的节点贴上 "hot" 级,然后 "hot" 级的节点集群就可以被配置为下载那些更频繁访问的数据。一个并行的 "cold" 节点集群就可以使用性能略差一些的硬件来搭建,用来存储不经常被访问的 segments。

可用性

  历史节点依赖 ZooKeeper 来执行 segment 的加载和卸载指令,如果 ZooKeeper 变为不可用状态,历史节点便不能再提供新数据或者丢弃掉旧数据了。但是由于历史节点的查询服务是通过 HTTP 提供的,所以历史节点依然可以响应那些他们当前还在提供服务的数据的查询请求,这意味着 ZooKeeper 的停机不会影响历史节点当前服务的数据的可用性。

 

3. 代理节点 Broker Nodes

  Broker 节点扮演一个在历史节点和实时节点之上的路由角色,它们能够解析 ZooKeeper 中发布的元数据信息,其中包含了哪些 segment 是可以被查询的以及对应的位置。Broker 节点负责将查询路由到正确的历史节点或实时节点上,然后还会把历史节点和实时节点返回的部分数据合并成最终的整理后的结果返回给调用者。

缓存 caching

  Broker 节点包含一个带 LRU 失效策略的缓存,该缓存能够使用本地的堆内存或者外部的分布式的 key/value 键值存储,例如 Memcached。Broker 节点每次收到查询请求时,它首先会把查询映射到系列的 segment 上,某些 segment 上的查询结果可能已经在缓存中存在了所以不用重新计算。对于那些在缓存中没有的结果,Broker 节点会直接去对应的历史节点和实时节点中计算,当历史节点返回结果之后 Broker 节点会把结果按照 segment 进行缓存方便将来使用。实时节点的查询结果永远不会被缓存因此查询实时节点数据的请求会一直到达实时节点,实时节点的数据一直在改变,如果缓存查询结果的话该结果是不可信的。

  缓存也充当一个数据可靠性的附加层,当历史节点都挂掉后在缓存中存在的查询结果依然可以被查询到。

可用性 Availability

  在 ZooKeeper 停机后,数据依然可以被查询。当 Broker 节点无法与 ZooKeeper 通信时,Broker 节点会使用它们上次记录的集群的信息来继续把查询发送给实时节点和历史节点。Broker 节点会假设 ZooKeeper 停机前后集群的结构是一样的。在实践中,这种可用性模式使我们的集群在我们诊断出 ZooKeepr 停机后仍然为查询提供了相当长时间的服务。

 

4. 协调节点 Coordinator Nodes

  Druid 协调器节点主要负责历史节点上的数据管理和分发。协调节点告知历史节点加载新数据、删除过期数据、复制数据和移动数据来达到负载均衡。Druid 使用多版本并发控制交换协议来管理不可变的 segments 从而维护一个稳定的视图。如果一个不可变 segments 包含的数据在一个新的 segments 中被标记删除,这个过时的 segment 会被集群丢弃。协调节点进行一个领导选举来决定哪个节点行使协调功能,剩余的节点用于冗余备份。

  一个协调节点周期性的运行来断定当前的集群状态,它通过对比当前时间集群的状态和期望状态来做决定。与 Druid 所有节点一样,协调节点维护一个 ZooKeeper 连接用于获取当前集群的信息,协调节点也会维护一个 包含额外操作参数和配置的MySQL 数据库的连接。MySQL 数据库中保存的重要信息中其中一个就是包含了所有应该被历史节点处理的 segments 列表的数据库表,该表可以被任何生成 segments 的服务(如实时节点)所更新。MySQL 数据库同样包含一个用于管理如何创建、销毁和做副本的规则表。

规则 Rules

  Rules 管理着如何从集群加载、丢弃历史节点的 segments,指示着 segments 应该如何被分配到不同的历史节点的 tier 中以及一个 tier 中应该保存多少个 segment 的副本,甚至 Rules 还指示着 segments 何时会被整体从集群丢弃掉。一般情况下 Rules 会被用在一段时间内,例如,一个用户可以使用 rules 来把一个月内的 segments 加载到 "hot" 集群中,把一年内的 segments 加载到 "cold" 集群中,然后把更老的数据丢弃掉。

  协调节点从 MySQL 数据库的 rule 表中加载一系列的规则,数据源可以关联一些特定或者默认的规则。协调节点会遍历所有可用的 segment 然后匹配每一个 segment 的第一个规则。

负载均衡 Load Balancing

  在典型的生产环境中,一个查询经常会命中数十个甚至上百个 segment,限于每个历史节点的资源限制,segments 非常必要在集群中分发以保证集群的负载不至于太过于倾斜确定。最佳负载分布需要一些有关查询模式 和速度的知识。通常情况下,查询会覆盖跨越单个数据源的最近连续时间间隔的段,平均而言,访问更小 segme-nt 的查询会更快一些。

  这种查询模式建议以更高的速率复制最近的历史片段(segment),分散时间上接近的大片段(segment) 到不同的历史节点,并共同定位来自不同数据源的段 (segments)。为了在群集中最佳地分配和平衡段,我们开发了基于成本的优化过程,该过程考虑了段数据源,新近度和大小,该算法的确切细节超出了本文范围可能会在未来的文献中讨论。

副本 Replication

  协调节点可以告诉不同的历史节点加载同一段的副本,历史节点的每个层(tier)中的副本数是完全可配置的,需要高级别容错的设置可以配置为具有大量副本,副本段的处理方式与原始段相同,并遵循相同的负载分配算法。通过复制段,单个历史节点故障在Druid集群中是透明的,我们使用此属性进行软件升级,我们可以无缝地使历史节点脱机更新它和进行备份,并为群集中的每个历史节点重复此过程。 在过去两年中,我们从未在 Druid 集群中更新软件而占用停机时间。

可用性 Availability

  Druid 协调节点含有 ZooKeeper 和 MySQL 的额外依赖,协调节点依赖 ZooKeeper 来确定集群中都已经存在了哪些历史节点,如果 ZooKeeper 变为不可用协调节点便不能再发送指派、平衡和丢弃 segment 的指令了。无论如何,这些操作都根本不会影响到数据的可用性。

  响应 MySQL 和 ZooKeeper 失败的设计原则是相同的:如果负责协调的外部依赖失败,则集群维持现状。 Druid 使用 MySQL 存储操作管理信息并分析有关群集中应存在哪些段的元数据信息。 如果 MySQL 发生故障,协调器节点将无法使用此信息。 但是,这并不意味着数据本身不可用。 如果协调节点无法与 MySQL 通信,它们将停止分配新的段以及删除过时的段等操作。 在MySQL中断期间,代理,历史和实时节点仍可查询。

相关文章: