Druid:A Real-time Analytical Data Store

Druid是一个开源的数据存储,专为大型数据集的实时探索性分析而设计。该系统结合了面向列的存储布局、分布式的、无共享的体系结构和高级索引结构,允许任意探索具有次秒延迟的10亿行表。在本文中,我们描述了Druid的架构,并详细说明了它是如何支持快速聚合、灵活过滤和低延迟数据吸收的。 

近年来,随着互联网技术的普及,机器生成的事件激增。单独来看,这些事件包含的有用信息很少,价值也很低。考虑到从大型事件集合中提取意义所需的时间和资源,许多公司愿意放弃这些数据。虽然已经建立了处理基于事件的数据的基础设施(如IBM的Netezza[37]、惠普的Vertica[5]和EMC的Greenplum[29]),但它们大多以高价出售,而且只针对那些有能力提供服务的公司。

几年前,谷歌引入了MapReduce[11]作为他们利用普通硬件对互联网进行索引和分析日志的机制。Hadoop[36]项目很快跟进,并在很大程度上模仿了来自最初的MapReduce论文的见解。Hadoop目前部署在许多组织中,用于存储和分析大量日志数据。Hadoop在帮助企业转换其低价值方面做出了很大贡献将事件流转化为各种应用程序(如商业智能和a - b测试)的高价值聚合。

与许多伟大的系统一样,Hadoop让我们看到了一个新的问题领域。具体地说,Hadoop擅长于存储和提供对大量数据的访问,但是,它并不能保证访问数据的速度。此外,尽管Hadoop是一个高可用的系统,但在沉重的并发负载下性能会下降。最后,虽然Hadoop可以很好地存储数据,但它并没有在吸收数据和使数据立即可读方面进行优化。

在Metamarkets产品开发的早期,我们遇到了这些问题,并意识到Hadoop是一个很棒的后台、批处理和数据仓库系统。然而,作为一个在高并发环境(1000多个用户)中拥有关于查询性能和数据可用性的产品级保证的公司,Hadoop无法满足我们的需求。我们在这个领域探索了不同的解决方案,并且在尝试了关系数据库管理系统和NoSQL架构之后,我们得出了这样的结论:在开源世界中,没有任何东西可以完全满足我们的需求。我们最终创建了Druid,一个开源、分布式、面向列的实时分析数据存储。在许多方面,Druid与其他OLAP系统[30,35,22]、交互式查询系统[28]、主存数据库[14]以及众所周知的分布式数据存储[7,12,23]有相似之处。分布和查询模型也借鉴了当代搜索基础设施的思想[25,3,4]。

本文描述了Druid的体系结构,探讨了在创建支持托管服务的始终在线生产系统时所做的各种设计决策,并试图帮助遇到类似问题的任何人了解解决该问题的潜在方法。德鲁伊已经在几家科技公司投入生产。本文的结构如下:我们首先在第2节描述问题。接下来,在第3节中,我们将从数据如何流经系统的角度详细介绍系统架构。然后我们将在第4节讨论如何以及为什么将数据转换为二进制格式。我们将在第5节简要描述查询API,并在第6节给出性能结果。最后,我们在第7节结束了我们在生产中运行德鲁伊的经验,并在第8节结束了相关的工作。

2. PROBLEM DEFINITION

Druid最初被设计用来解决有关摄取和探索大量事务事件(日志数据)的问题。这种形式的时间序列数据在OLAP工作中很常见流动和数据的性质往往是非常附加的。例如,考虑表1中显示的数据。表1包含Wikipedia上发生的编辑数据。每当用户在Wikipedia中编辑页面时,就会生成一个事件,其中包含关于该编辑的元数据。这个元数据由3个不同的组件组成。首先,有一个时间戳列指示进行编辑的时间。接下来,有一组维列指示关于编辑的各种属性,比如被编辑的页面、进行编辑的用户和用户的位置。最后,有一组度量列,其中包含可以聚合的值(通常为数字),例如在编辑中添加或删除的字符数。

我们的目标是快速计算钻取和聚集这些数据。我们想要回答的问题是,在旧金山,有多少男性对贾斯汀·比伯的页面进行了编辑?来自卡尔加里的人在一个月的时间里平均添加了多少字符?. 我们还希望对任意维度组合的查询返回次秒级延迟。

现有的开源关系数据库管理系统(RDBMS)和NoSQL键/值存储无法为交互式应用程序[40]提供低延迟的数据摄入和查询平台,这促进了对Druid的需求。在Metamarkets的早期,我们专注于构建一个托管仪表板,允许用户任意地探索和可视化事件流。支持仪表板的数据存储需要足够快地返回查询,以便在其上构建的数据可视化能够为用户提供交互式体验。

除了查询延迟需求外,系统还必须是多租户和高可用性的。Metamarkets产品是在高度并发的环境中使用的。停机时间是昂贵的,如果系统在面临软件升级或网络故障时不可用,许多企业无法承受等待的代价。对于缺乏适当内部运营管理的初创企业来说,停机时间可以决定企业的成败。

最后,Metamarkets在早期面临的另一个挑战是让用户和警报系统能够实时做出业务决策。从创建事件到该事件可查询的时间决定了感兴趣的各方能够多快地对其系统中潜在的灾难性情况作出反应。流行的开源数据仓库系统,如Hadoop,无法提供我们所需的亚秒级数据吸收延迟。

数据探索、摄取和可用性的问题跨越多个行业。自2012年10月Druid开源以来,它被部署为多个公司的视频、网络监控、运营监控和在线广告分析平台。

3. ARCHITECTURE

A Druid cluster 由不同类型的节点组成,每一种节点类型都被设计用来执行一组特定的任务。我们认为这种设计分离了关注点,并简化了整个系统的复杂性。不同的节点类型操作相当独立,它们之间的相互作用很小。因此,集群内部通信故障对数据可用性的影响很小。为了解决复杂的数据分析问题,不同类型的节点聚在一起形成一个完整的工作系统。德鲁伊这个名字来自于许多角色扮演游戏中的德鲁伊类:它是一种变形者,能够以许多不同的形式在一个群体中完成各种不同的角色。图1显示了德鲁伊集群中数据的组成和流。

3.1 Real-time Nodes

实时节点封装了获取和查询事件流的功能。通过这些节点索引的事件可以立即用于查询。这些节点只关心一些小时间范围内的事件,并定期将它们在这个小时间范围内收集的不可变批次事件传递给德鲁伊集群中专门处理不可变批次事件的其他节点。实时节点利用ZooKeeper[19]与Druid集群的其他节点进行协调。节点宣布自己的在线状态和在ZooKeeper中提供的数据。

实时节点在内存中为所有传入的事件维护一个索引缓冲区。这些索引在接收事件时递增填充,索引也可以直接查询。对于存在于这个基于JVM堆的缓冲区中的事件查询,Druid的行为就像行存储一样。为了避免堆溢出问题,实时节点定期或在达到某个最大行限制后将其内存中的索引持久化到磁盘。这个持久化过程将存储在内存缓冲区中的数据转换为第4节中描述的面向列的存储格式。每个持久索引都是不可变的,实时节点将持久索引加载到堆外内存中,以便仍然可以查询它们。这个过程在[33]中有详细描述,如图2所示。

每个实时节点将定期调度一个后台任务,搜索所有本地持久化的索引该任务将这些索引合并在一起,并构建一个不可变的数据块,该数据块包含实时节点在一段时间内接收到的所有事件。我们把这个数据块称为段。在切换阶段,实时节点将该段上传到永久备份存储,通常是分布式文件系统,如S3[12]或HDFS [36], Druid将其称为深度存储。摄取、持久化、合并和移交步骤是流动的;在任何过程中都不会有数据丢失。

图3演示了实时节点的操作  节点从13:37开始,只接受当前一小时或下一小时的事件。当接收事件时,节点宣布它正在为一段数据提供服务,时间间隔从13:00到14:00。每隔10分钟(持久化时间可配置),节点将刷新并将其内存中的缓冲区持久化到磁盘在一小时结束时,节点可能会看到14:00到15:00的事件。当发生这种情况时,节点准备为下一个小时提供数据,并创建一个新的内存索引。然后节点宣布它也服务于14:00到15:00的一段时间节点不会立即在13:00到14:00之间合并持久化索引,而是等待一个可配置的窗口期来处理13:00之间的散乱事件。这个窗口期将由于事件交付延迟而造成的数据丢失风险降到最低。在窗口结束时,节点将13:00到14:00之间的所有持久化索引合并到一个单一的不可变段中,并将该段传递出去。一旦这个段在Druid集群的其他地方被加载并可查询,实时节点将刷新它在13:00到14:00收集的所有数据的信息,并不通知它正在服务这些数据。

3.1.1 Availability and Scalability

实时节点是数据的消费者,需要相应的生产者提供数据流。通常,为了数据持久性的目的,消息总线(如Kafka[21])位于生产者和实时节点之间,如图4所示。实时节点通过从消息总线读取事件来获取数据。从事件创建到事件使用的时间通常是几百毫秒。图4中消息总线的目的有两个。首先,消息总线充当传入事件的缓冲区。像Kafka这样的消息总线维护着一个位置偏移量,用来指示消费者(一个实时节点)在事件流中读取了多远的数据  消费者可以通过编程方式更新这些偏移量。 在故障和恢复场景中,如果一个节点没有丢失磁盘,它可以从磁盘重新加载所有持久索引,并继续从它提交的最后一个偏移量读取事件。从最近提交的偏移量中获取事件大大减少了节点的恢复时间。在实践中,我们看到节点在几秒钟内就能从这种故障场景中恢复过来。

消息的第二个目的是多个实时节点可以从总线接收同一组事件,创建事件的复制。在一个节点完全故障并丢失磁盘的情况下,复制的流可以确保没有数据丢失。单个摄取端点还允许对数据流进行分区,这样多个实时节点每个摄取流的一部分。这允许无缝地添加额外的实时节点。在实践中,该模型允许最大的生产集群之一的Druid集群能够以大约500mb /s (150,000 events/s或2tb /hour)的速度消耗原始数据。

3.2 Historical Nodes

历史节点封装了加载和服务由实时节点创建的不可变数据块(段)的功能。在许多真实的工作流中,装载在Druid集群中的大多数数据是不可变的,因此,历史节点通常是Druid集群的主要工作节点。历史节点遵循无共享架构,节点之间不存在单点争用。节点之间互不认识,而且操作简单;它们只知道如何加载、删除和服务不可变段。

与实时节点类似,历史节点也会在ZooKeeper中公布自己的在线状态和服务数据。加载和删除段的指令通过ZooKeeper发送,并包含关于段在深层存储中的位置以及如何解压和处理段的信息。在历史节点从深层存储下载特定段之前,它首先检查本地缓存,该缓存维护关于节点上已经存在哪些段的信息。如果一个段的信息在缓存中不存在,历史节点将继续从深层存储下载该段。这个过程如图5所示。一旦处理完成,段会在ZooKeeper中宣布。此时,段是可查询的。本地缓存还允许快速更新和重启历史节点。在启动时,节点检查它的缓存并立即提供它找到的任何数据。

历史节点可以支持读取一致性,因为它们只处理不可变的数据。不可变数据块也支持simevents实时节点1的并行化模型:历史节点可以并发地扫描和聚合不可变块而不阻塞。

3.2.1 Tiers

每个层可以设置不同的性能和容错参数分级节点的目的是根据优先级的重要性来分配更高或更低的优先级段。例如,可以旋转具有高核数和大内存容量的历史节点的热层。可以配置热集群,下载访问频率更高的数据。并行冷集群也可以用功能弱得多的后备硬件创建。冷集群将只包含访问频率较低的段

3.2.2 Availability

历史节点段负载依赖于ZooKeeper,如果ZooKeeper不可用,历史节点将不再能够提供新的数据或丢弃过时的数据,但是,由于查询是通过HTTP服务的,历史节点仍然能够响应当前服务数据的查询请求。这意味着ZooKeeper的宕机不会影响历史节点上当前数据的可用性。

3.3 Broker Nodes

他们了解发布在ZooKeeper中的元数据,了解哪些段是可查询的,以及这些段位于哪里。代理节点路由传入的查询,以便查询命中正确的历史或实时节点。代理节点还会合并历史节点和实时节点的部分结果,然后将最终的合并结果返回给调用者。

3.3.1 Caching

代理节点包含一个具有LRU[31,20]失效策略的缓存,缓存可以使用本地堆内存,或者外部的分布式K/V。每当代理节点接收到查询时,它首先将查询映射到一组段。某些段的结果可能已经存在于缓存中,不需要重新计算它们对于缓存中不存在的任何结果,代理节点将把查询转发到正确的历史和实时节点。一旦历史节点返回它们的结果,代理将在每个段的基础上缓存这些结果,以便将来使用。这个过程如图6所示。实时数据永远不会被缓存,因此对实时数据的请求总是被转发到实时节点。实时数据永远在变化,缓存结果是不可靠的。缓存还充当数据持久性的一个额外级别。在所有历史节点失败的情况下,仍然可以查询结果(如果这些结果已经存在于缓存中)。

3.3.2 Availability

在ZooKeeper总宕机的情况下,仍然可以查询数据。如果代理节点无法与ZooKeeper通信,它们将使用最后一个已知的集群视图,并继续将查询转发给实时和历史节点。代理节点假设集群的结构与停机前相同。在实践中,这个可用性模型允许我们的Druid集群在诊断ZooKeeper宕机的同时,在很长一段时间内继续提供查询服务。

3.4 Coordinator Nodes

德鲁伊协调节点主要负责历史节点上的数据管理和分发。协调节点告诉历史节点加载新数据、删除过时数据、复制数据并将数据移动到负载平衡。为了维护稳定的视图,Druid使用了一个多版本并发控制交换协议来管理不可变的段。如果任何不可变段中包含的数据被新段完全废弃,那么过期的段将从集群中删除。协调器节点经历一个leader-election过程,这个过程决定运行协调器功能的单个节点。其余的协调节点充当冗余备份

协调节点定期运行以确定集群的当前状态。它通过比较运行时集群的预期状态与集群的实际状态来做出决策。与所有的德鲁伊节点一样,协调节点为当前的集群信息维护一个ZooKeeper连接。协调器节点还维护到MySQL数据库的连接,该数据库包含其他操作参数和配置。MySQL数据库中的一个关键信息是一个表,其中包含了历史节点应该服务的所有段的列表。该表可以被任何创建段的服务更新,例如实时节点。MySQL数据库还包含一个规则表,用于管理在集群中如何创建、销毁和复制段。

3.4.1 Rules

规则控制如何从集群加载和删除历史片段。规则指出段应该如何分配给不同的历史节点层,以及每个层中应该存在多少个段的复制。规则还可以指示何时片段应该完全从集群中删除。规则通常会设定一段时间。例如,用户可以使用规则将最近一个月的段加载到热集群中,将最近一年的段加载到冷集群中,并删除任何较旧的段。协调节点从MySQL数据库中的规则表加载一组规则。协调器节点将遍历所有可用的段,并将每个段与适用于它的第一个规则匹配。

3.4.2 Load Balancing

在典型的生产环境中,查询经常涉及几十个甚至数百个段。由于每个历史节点资源有限,必须将段分配到集群中,以确保集群负载不会太不均衡。确定最佳负载分配需要一些关于查询模式和速度的知识。通常,查询覆盖单个数据源的最近的跨越连续时间间隔的段。平均而言,访问较小段的查询速度更快。

这些查询模式建议以更高的速率复制最近的历史段,将时间上接近的大段分散到不同的历史节点,并将来自不同数据源的段放在一起。为了在集群中优化分配和平衡段,我们开发了一个基于成本的优化过程,该过程考虑了段数据源、最近发生的情况和大小。算法的具体细节超出了本文的讨论范围,可以在以后的文献中讨论。

3.4.3 Replication

协调器节点可以告诉不同的历史节点加载同一段的副本。历史计算集群的每一层中的复制数量是完全可配置的。需要高容错性的设置可以配置为具有大量副本。复制段被视为与原始段相同,并遵循相同的负载分配算法。通过复制段,单个历史节点故障在Druid集群中是透明的。我们使用此属性进行软件升级。我们可以无缝地将历史节点脱机、更新、恢复,并对集群中的每个历史节点重复这个过程。在过去的两年里,我们的德鲁伊集群从来没有停机进行软件升级。

3.4.4 Availability

德鲁伊协调节点外部依赖ZooKeeper和MySQL。协调节点依赖于ZooKeeper来确定集群中已经存在哪些历史节点。如果zookeeper变得不可用,协调器将不再能够发送指令分配,平衡和删除段。然而,这些操作根本不会影响数据可用性。

MySQL和ZooKeeper故障响应的设计原则是一样的:如果一个负责协调的外部依赖失败,集群将维持现状。Druid使用MySQL来存储操作管理信息和段元数据信息,这些信息是关于哪些段应该存在于集群中。如果MySQL宕机,协调节点将无法获得该信息。然而,这并不意味着数据本身不可用。如果协调节点不能与MySQL通信,它们将停止分配新的段并删除过时的段。在MySQL中断期间,代理节点、历史节点和实时节点仍然是可查询的。

4. STORAGE FORMAT

Druid中的数据表(称为数据源)是时间戳事件的集合,并被划分为一组段,每个段通常是5-10百万行。在形式上,我们将段定义为跨越一段时间的数据行的集合。段代表德鲁伊的基本存储单元,复制和分发都是在段级别完成的。

Druid总是需要一个时间戳列作为简化数据分发策略、数据保留策略和第一级查询剪枝的方法Druid将数据源划分为定义良好的时间间隔,通常是一个小时或一天,并可能进一步对其他列的值进行分区,以实现所需的段大小。划分段的时间粒度是数据量和时间范围的函数。时间戳分布在一年的数据集最好按天进行分区,时间戳分布在一天的数据集最好按小时进行分区。

段由数据源标识符、数据的时间间隔和版本字符串惟一地标识,版本字符串在创建新段时增加。版本字符串表示段数据的新鲜度;与旧版本的段相比,新版本的段具有更新的数据视图(在某个时间范围内)。这个段元数据被系统用于并发控制;读操作总是访问特定时间范围内具有该时间范围最新版本标识符的段中的数据。

Druid的片段以列的方向存储。鉴于Druid最适合用于聚合事件流(进入Druid的所有数据都必须有一个时间戳),将聚合信息存储为列而不是行的优势在[1]中得到了很好的说明。列存储允许更有效地使用CPU,因为实际上只加载和扫描需要的内容。在面向行的数据存储中,必须作为聚合的一部分扫描与行关联的所有列。额外的扫描时间会导致显著的性能下降。

Druid有多个列类型来表示各种数据格式。根据列类型的不同,可以使用不同的压缩方法来降低将列存储在内存和磁盘上的成本。在表1中给出的示例中,page、user、gender和city列只包含字符串。直接存储字符串的开销是不必要的,字符串列可以用字典编码代替。字典编码是压缩数据的常用方法,已经在其他数据存储中使用,如PowerDrill[17]。在表1的示例中,我们可以将每个页面映射到唯一的整数标识符。

4.1 Indices for Filtering Data

在许多现实世界的OLAP工作流中,在满足了一组维度规范的情况下,会对一组指标的聚合结果发出查询。一个查询示例是:旧金山的男性用户编辑维基百科的次数是多少?该查询基于维值的布尔表达式过滤表1中的维基百科数据集。在许多现实世界的数据集中,维度列包含字符串,度量列包含数值。Druid为字符串列创建额外的查找索引,以便只扫描属于特定查询过滤器的那些行。

4.2 Storage Engine

Druid的持久性组件允许插入不同的存储引擎,类似于Dynamo[12]。这些存储引擎可以将数据存储在完全内存中的结构中,比如JVM堆或内存映射结构中。交换存储引擎的能力允许德鲁伊根据特定的应用程序规格进行配置。内存中存储引擎在操作上可能比内存映射存储引擎更昂贵,但如果性能很关键,它可能是一个更好的替代方案。默认情况下,使用内存映射存储引擎。

当使用内存映射存储引擎时,Druid依赖于操作系统将段分页入内存或分页出内存。考虑到段只能在它们被加载到内存中时进行扫描,内存映射存储引擎允许最近的段保留在内存中,而从不被查询的段则被换出。使用内存映射存储引擎的主要缺点是,当查询需要分页到内存中的段比给定节点的容量更多时。在这种情况下,查询性能将受到内存中分页段的开销的影响。

5. QUERY API

Druid有自己的查询语言,并将查询作为POST请求接受。代理、历史节点和实时节点都共享相同的查询API。POST请求的主体是一个JSON对象,其中包含指定各种查询参数的键值对。一个典型的查询将包含数据源名称、结果数据的粒度、感兴趣的时间范围、请求类型以及聚合的指标。结果还将是一个JSON对象,其中包含一段时间内的聚合指标。

大多数查询类型也将支持筛选器集。筛选器集是维名和值对的布尔表达式。可以指定任何尺寸和值的数字和组合。当提供筛选器集时,只会扫描属于该筛选器集的数据子集。处理复杂的嵌套过滤集的能力使德鲁伊能够钻入任何深度的数据。确切的查询语法取决于查询类型和所请求的信息。一个关于一周数据的计数查询示例如下:

Druid支持多种类型的聚合,包括浮点和整数类型的和、最小值和最大值,以及复杂的聚合,比如基数估计和近似分位数估计。聚合的结果可以组合成数学表达式,以形成其他聚合。全面描述查询API超出了本文的范围,但更多信息可以在网上找到

6. PERFORMANCE

Druid在几个组织的生产环境中运行,为了展示它的性能,我们选择分享一些2014年初在Metamarkets上运行的主要生产集群的真实数据。为了与其他数据库进行比较,我们还包括了TPC-H数据上合成工作负载的结果。

6.1 Query Performance in Production

根据发出的查询的不同,德鲁伊查询性能可能会有很大的不同。例如,根据给定的指标对高基数维的值进行排序比在一个时间范围内进行简单计数要昂贵得多。为了展示生产Druid集群中的平均查询延迟,我们选择了8个被查询最多的数据源,如表2所示。

大约30%的查询是涉及不同类型指标和过滤器的标准聚合,60%的查询是根据聚合的一个或多个维度进行排序分组的,10%的查询是搜索查询和元数据检索查询。聚合查询中扫描的列数大致呈指数分布。涉及单个列的查询非常频繁,涉及所有列的查询非常罕见。

查询延迟如图8所示,每分钟查询如图9所示。在所有不同的数据源中,平均查询延迟大约为550毫秒,90%的查询在不到1秒内返回,95%在不到2秒内返回,99%的查询在不到10秒内返回。我们偶尔会观察到延迟峰值,就像2月19日观察到的那样,Memcached实例上的网络问题与我们最大的数据源之一上的非常高的查询负载有关。

6.2 Query Benchmarks on TPC-H Data

9. 结论

在本文中,我们介绍了Druid,一个分布式的、面向列的实时分析数据存储。德鲁伊是为高性能应用程序而设计的,并为低查询延迟进行了优化。德鲁伊支持流数据摄取和容错。我们讨论了Druid基准测试,并总结了关键的体系结构方面,如存储格式、查询语言和一般执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值