在演示了大规模运行德鲁伊的挑战后,我想提出我对下一代开源时序存储的看法,应该不会有德鲁伊的固有问题。

   “开源”是问题陈述的重要部分,因为提议的设计本质上是专有的Google BigQuery的简化版本。我主要从Dremel的论文和帖子“引擎盖下的BigQuery”中获得了关于BigQuery架构的信息,也从许多其他来源获得了一些信息。

  其他目标和自律:

  时序存储可以扩展到单个集群中的Pb级压缩数据和100k处理核心。云优先:利用云的优势。从数十TB的数据和一千个处理核心开始,它是经济高效的。在一个合理规模的集群中,处理小于5 TB数据的查询应该在3秒内运行(p99延迟)——涵盖交互式广告分析用例。高查询延迟:不管集群中并行运行的查询是什么,相似的查询应该总是花费相同的时间来完成。新获得的数据应该立即可用。仔细想想:建议的设计预计在3-5年内会变得越来越重要,而不是越来越不重要。非目标:

  本地部署。小规模的成本效益。随机更新和删除旧数据的效率,尽管这些事情应该是可能的。对于任何一个小的查询,即使在空载的系统中,p99的等待时间也不到半秒。易于首次部署和更新软件。最后一个介绍性的说明:本文基于在Metamarkets中大规模运行Druid的经验和理论研究,但是所描述的设计还没有在生产中实现和测试。这篇文章中的一些陈述是错误的。如有任何意见或修改,请在本帖下评论!

  本文总结了具有三个解耦子系统的时序存储器的设计。浅蓝色线条代表未压缩的面向行的数据流;深蓝色线条压缩的柱状数据;红线-查询结果。

  该系统由三部分组成,它们之间有严格的职责分离:流处理系统、存储和计算树。

  流处理系统接收数据(接受“写入”),对其进行分区,将每个时间间隔内的数据转换为压缩列格式,并将其写入存储。流处理系统的工作人员还负责计算一些最新数据的查询结果。

  计算树有多级节点:最低一级节点从存储中下载特定分区和间隔的数据,并为其计算部分结果。如果查询区间包括最新的数据,第二层的节点合并特定分区的所有分区的结果,接受最底层的节点和流处理系统的工作程序的接受。第三层节点合并或归并第二层节点每个时间间隔的结果,并包含每个时间间隔的查询结果的缓存。这些节点还可能负责集群平衡和低级计算树的自动扩展。

  该设计的主要原则:

  以及计算和存储的分离。这个想法来自于BigQuery。在我关于Druid的文章中,我解释了Druid中缺乏这种分离是如何使查询延迟不可预测的,因为查询会相互干扰。

  使计算树中的节点(几乎)无状态,这意味着它们是“一次性的”。它们可能是亚马逊的EC2或谷歌的抢先实例,比普通实例便宜好几倍。类似地,计算树可以在几分钟内被放大和缩小,因此可以例如.当查询负载较低时,每晚和周末按比例减少。

  数据摄取(在流处理系统中)与存储分离。这个想法其实已经在德鲁伊身上实现了,有实时节点。这种关注点的分离可以保持存储非常简单,不需要为提取、列压缩、查询处理等分配资源。它只专注于从磁盘读取字节块,通过网络发送到计算中的节点和树。

  流系统也可能比支持写操作的存储更具动态性。流处理系统可以根据数据摄入强度的变化而放大或缩小,数据摄入强度通常在晚上和周末较低。流式传输系统可能具有难以在存储中实现的功能,例如动态重新分区。

  是网络的瓶颈。如果查询的下载量没有使存储的出站网络带宽饱和,则网络对总查询延迟的贡献是恒定的,并且与查询大小无关。如果云对象存储被用作存储(参见下面的“云对象存储”部分),或者如果系统中的查询负载相对于存储中的历史数据量不成比例地小,则可以授予该权限。

  如果这两个条件都不适用,可以使用存储来托管一些下载频率较低的非时序数据,从而人为增加存储集群的规模,进而增加其出站网络带宽。

  否则,在建议的设计中,存储树和计算树之间的网络吞吐量可能会成为限制查询延迟的一个因素。有几种方法可以缓解这种情况:

  与只生成一个表的典型SQL查询不同,对该系统的查询应该由所有子查询组成,这些子查询在分析界面的单个屏幕上是必需的。分析界面通常包括至少几个,有时几十个表格、图表等。它们是相同时间序列数据的子查询的结果。将查询结果大量缓存在第三级计算树中,以减少重复相同计算的负载。投影下推:只从存储中下载查询处理所需的列子集。按维度键分区(最常出现在查询过滤器中)只下载和处理所需的分区-谓词下推。由于许多实际数据维度中的关键频率是泊松分布、齐普夫分布或其他不均匀分布,理想情况下,流处理系统应该支持“部分”分区,如下图所示。由于这种分区的基数较低,在每个分区变得太小而无法以列格式有效压缩和处理之前,可以将数据划分为多个维度。

  部分分区可以实现不均匀的密钥分配。每个盒子都是一个分区。具有“其他值”的分区可能有数千个“长尾”值。

  一般来说,数据段(分区)的元数据应该包括所有维度的信息,这些维度似乎只有一个(或几个)键填充在该分区中,从而受益于一个“意外的”分区。列压缩应该强烈支持压缩率,而不是解压缩或处理速度。应将列从存储流式传输到计算树中的部分。

  点,并且一旦所有必需列的第一个块到达计算节点,就开始子查询处理。这样可以使网络和CPU的贡献在总查询延迟中尽可能地重叠。要从中受益,将列从存储发送到计算树的顺序应该比仅在存储中的磁盘上排列列的顺序或列名称按字母顺序排列的顺序更聪明。列也可以按小块以交错顺序发送,而不是逐列发送。一旦部分结果准备就绪,就递增计算最终查询结果,并将增量结果流式传输到客户端,以使客户端感知查询运行得更快。

  在本文的后面,我将详细介绍系统的每个部分。

  存储

  在本节中,我想讨论一些存储的可能实现。它们可以作为可互换的选项共存,就像在Druid中一样。

  云对象存储

  它是Amazon S3,Google云存储(GCS),Azure Blob存储以及其他云提供商的类似产品。

  从概念上讲,这正是设计的时间序列存储中应使用的存储方式,因为GCS由名为Colossus的系统提供支持,并且它也是BigQuery的存储层。

  云对象存储比我将在下面讨论的选项便宜得多,所需的管理工作少得多,并且吞吐量几乎不受限制,因此上面的整个“网络是瓶颈”一节在很大程度上是不相关的(理论上)。

  云对象存储API不够完善,不足以在单个请求中支持多个字节范围的下载(用于多列的投影下推),因此每列的每次下载应是一个单独的请求。我怀疑这不是BigQuery的工作方式,它与Colossus的集成更紧密,可以实现适当的多列投影下推。

  在我看来,“云对象存储”选项的主要缺点可能是其p99延迟和吞吐量。一些基准测试表明,GCS和S3在100 ms的延迟中具有p99延迟(这是可以接受的),并且吞吐量仅受下载端VM功能的限制,但是如果在并发100个负载的情况下仍然如此,我将感到非常惊讶一个节点的请求,以及整个集群中一百万个并发请求的规模。请注意,所有云提供商都没有针对对象存储延迟和吞吐量的SLA,对于GCS,公认吞吐量是“相当多的变量”。

  (注意:之前,在上面的部分中,我提到了Cloud Object Storage API不支持范围请求,这是不正确的,尽管它们仍然不支持(截至2019年10月)单个请求中的多个范围下载,因此并发查询放大系数不会消失。)

  HDFS中Parquet格式的数据分区

  此选项的主要优点是与Hadoop生态系统的其余部分很好地集成-计算树甚至可以“附加”到某些已经存在的数据仓库中。大型联接或多步查询等不适用于时间序列范式的复杂查询可以由同一HDFS群集顶部的Spark,Impala,Hive或Presto之类的系统处理。

  同样重要的是,旨在部署设计的时间序列存储的组织可能已经具有非常大的HDFS集群,该集群具有较大的出站网络带宽,并且如果时间序列存储使用此HDFS集群存储其数据分区,则它可能会工作围绕网络的可扩展性问题。

  但是,库存HDFS通过单个NameNode路由所有读取请求。100k并发读取请求(假设只需要一个读取请求就可以在计算树中的一个节点上下载数据分区)接近NameNode的绝对可伸缩性限制,因此,如果HDFS集群实际上忙于处理某些内容,则超出该限制与时间序列存储无关的操作。

  此外,当HDFS用作“远程”分布式文件系统时,即使对于Parquet格式的文件,它也不支持投影下推,因此整个数据分区应由计算树中的节点下载。如果时间序列数据中有数百列,并且通常只使用一小部分进行查询,则效果将不佳。正如云对象存储所建议的那样,使每个数据分区的每一列都成为一个单独的文件,由于扩大了文件和读取请求的数量,因此施加了更大的可扩展性限制。NameNode将无法处理一百万个并发请求,并且HDFS并未针对小于10 MB的文件进行优化,假设最佳数据分区的大小约为一百万,则数据分区的各个列将具有的大小行。

  但是,在某些情况下(例如,存在大量未充分利用的HDFS集群)并且在某些使用情况下,HDFS似乎是最经济高效的选择,并且运行良好。

  Apache Kudu

  Apache Kudu是一种列式数据存储,旨在在许多情况下替换HDFS + Parquet对。它结合了节省空间的列式存储以及快速进行单行读写的能力。设计的时间序列系统实际上不需要第二部分,因为写入是由Stream处理系统处理的,而我们希望使Storage更加便宜并且不浪费CPU(例如用于后台压缩任务),每个Storage节点上的内存和磁盘资源支持单行读取和写入。此外,在Kudu中对旧数据进行单行写入的方式要求在Kudu节点上进行分区解压缩,而在建议的时间序列存储设计中,只有压缩后的数据应在存储和计算树之间传输。

  另一方面,Kudu具有多种功能,这些功能吸引了时间序列系统,而HDFS没有:

  类似于RDBMS的语义。Kudu中的数据以表格的形式组织,而不仅仅是一堆文件。Kudu中的平板电脑服务器(节点)比HDFS中的服务器更独立,从而可以在进行读取时绕过查询主节点(Kudu等效于NameNode),从而大大提高了读取可扩展性。投影下推。它是用C ++编写的,因此尾部延迟应该比用Java编写并且会出现GC暂停的HDFS更好。

  Kudu论文提到,从理论上讲,它可能支持可插拔的存储布局。如果实施的存储布局放弃了Kudu对提取单行写入和旧数据写入的支持,但更适合于时间序列存储设计,则Kudu可能会成为比HDFS更好的存储选项。

  Cassandra或Scylla

  每个数据分区可以存储在类似Cassandra的系统中的单个条目中。从Cassandra的角度来看,列具有二进制类型,并存储数据分区的压缩列。

  该选项与Kudu共享许多优点,甚至具有更好的优点:出色的读取可伸缩性,极低的延迟(尤其是如果使用ScyllaDB),表语义,仅下载所需列的能力(投影下推式)。

  另一方面,类似Cassandra的系统并非设计用于多个MB的列值和大约100 MB的总行大小,并且在填充此类数据时可能开始遇到操作问题。而且,它们不支持在单行甚至单行中的单列级别上进行流读取,但可以在这些系统中相对容易地实现。

  Cassandra旨在承受高写入负载,因此使用类似LSM的存储结构和大量内存,在时间序列系统中用作存储时将浪费资源。

  与我上面讨论的其他选项相比,该选项最快,但成本效益最低。

  将计算树的节点重用为存储(已在2019中添加)

  请参阅此处的想法说明。https://github.com/apache/druid/issues/8575

  流处理系统

  如上所述,Druid已经将数据摄取与所谓的索引子系统或实时节点中的存储区分开了。但是,尽管该索引子系统实现了完整的分布式流处理系统的功能的子集,但它并未利用其中的任何功能,甚至也没有利用Mesos或YARN之类的资源管理器,并且一切都在Druid源代码中完成。Druid的索引子系统的效率要比现代流处理系统低得多,因为对其进行的开发工作少了数十倍。

  同样,时间序列数据通常在Druid之前的其他流处理系统中进行组合或丰富。例如,沃尔玛(Walmart)通过Storm来做到这一点,而Metamarkets将Samza用于类似目的。从本质上讲,这意味着两个独立的流处理系统正在数据管道中一个接一个地运行,从而阻止了映射运算符与Druid的提取终端运算符的融合,这是流处理系统中的常见优化。

  这就是为什么我认为在下一代时间序列中,数据提取应充分利用某些现有的流处理系统。

  流处理系统与其余时间序列存储之间需要紧密集成,例如允许计算树中的节点查询流处理系统中的工作程序。这意味着与Storage的情况不同,它可能很难支持多个流处理系统。应该只选择一个,并将其与时间序列系统集成。

  Flink,Storm和Heron都是可能的候选人。很难判断当前哪个技术更合适,或者说在哪个技术上更合适,因为这些项目可以快速相互复制要素。如果设计的时间序列系统实际上是在某个组织中创建的,则选择可能取决于该组织中已使用的流处理系统。

  阅读Druid Development邮件列表中的该线程,以获取有关此主题的更多信息。

  计算树

  对于系统的这一部分的外观,我并不太费劲。上面的“设计概述”部分介绍了一些可能的方法。

  这种方法至少存在一个问题:如果需要缓存太多查询结果,则计算树的第三(最高)级别的多个节点将无法有效地处理对特定时间序列(表)的查询。为了始终将相似的子查询(仅在总体查询间隔上不同的子查询)路由到相同的节点并捕获缓存的结果,应将具有多个子查询的一个“复合”查询分解为多个独立的查询,进而使用网络存储和计算树之间的效率较低:请参见上面的“网络是瓶颈”部分,该列表中的第一项。

  但是,可以在垂直方向上扩展第三级计算树中的节点,以使其足够大,从而能够处理所有查询并容纳任何单个时间序列(甚至最繁忙的时间序列)的整个缓存。

  垂直扩展意味着第三级计算树中的一个节点应处理大量并发查询。这就是为什么我认为如果从头开始构建计算树的原因之一,它应该选择异步服务器体系结构而不是阻塞(Go风格的绿色线程也可以)。其他两个原因是:

  第一层计算树中的节点通过存储执行大量的网络I / O。这些节点上的计算取决于来自Storage的数据到达,并具有不可预知的延迟:来自Storage的数据请求通常会得到重新排序的响应。计算树所有级别的节点都应支持增量查询结果计算,并可能以很长的间隔返回同一查询的多个结果。如上文“网络是瓶颈”一节所述,它使系统更具容错能力(在我的第一篇文章中讨论了运行Druid的挑战),并使其变得更快。平台

  理想情况下,构建计算树的编程平台应具有以下特征:

  支持运行时代码生成,以使查询更快地完成并提高CPU利用率。这篇有关Impala中运行时代码生成的博客文章对此进行了很好的解释。出于相同的原因,生成的机器代码应该是“最佳”的,并在可能的情况下进行矢量化处理。较低的堆/对象内存开销,因为内存昂贵,因此使计算树中的节点更便宜。始终较短的垃圾回收暂停(对于具有托管内存的平台),以支持设计的时间序列存储的“一致查询延迟”目标。

  从纯技术角度来看,C ++是赢家,它可以满足所有这些要求。选择C ++与性能无关的缺点也是众所周知的:开发速度,可调试性,使用插件体系结构扩展系统都很困难等。

  JVM仍然是一个不错的选择,我相信该系统的效率可能比使用C ++内置的系统低不超过20%:

  JVM允许搭载JIT编译器以达到与运行时代码生成目标相同的效果。对于时间序列处理,主要在列解压缩期间以及在数据上运行特定聚合时需要代码矢量化。两者都可以在JNI函数中完成。当为数十千字节的解压缩数据支付一次时,JNI的开销相对较小(我们可能希望以这种大小的块进行处理以适合L2缓存中的所有解压缩数据)。巴拿马项目将使此开销更小。如果将数据存储在堆外内存中并进行处理,则垃圾回收的JNI含义也很小或根本不存在。可以通过将所有网络IO,数据存储,缓冲和处理都放在堆外内存中,从而使堆内存很小,从而仅对每个查询分配一些堆。使用Shenandoah GC可以缩短垃圾收集的暂停时间。如果核心处理循环中使用的所有数据结构都是非堆分配的,则堆内存的读取和写入障碍不会对CPU利用率造成太大影响。

  据我所知,尽管Go或Rust目前不支持运行时代码生成,尽管添加这种支持可能不需要太多的黑客操作:请参阅gojit项目以及有关Rust的StackOverflow问题。对于其他条件,Go的运行时和生成的代码可能效率较低,但是出于某些非技术性原因,它比Rust更有效。

  提议的时间序列系统的缺点该系统感觉不像是一个单一的“数据库”,它具有三个独立的子系统,其中活动部件的总数很高,这使其在小规模上效率不高,难以部署和更新。将系统与现有的说SQL的接口有效地集成可能是一个挑战,因为系统需要对同一张表运行带有许多独立子查询的“复合”查询。该系统不适用于需要对查询的响应速度超过一秒的用例。系统的性能高度依赖于部署它的数据中心中的网络性能。在某些用例中,无法在第三级计算树中水平缩放节点可能是主要的可伸缩性瓶颈。