数据存储设计主要包括哪些(结构化数据存储方式)
在论证了大规模运行druid的挑战之后,我想提出我对下一代开源时间序列存储的看法,这应该不会出现druid固有的问题。
“开源”是问题陈述的重要组成部分,因为提出的设计实质上是专有google bigquery的简化版本。我主要从dremel论文和帖子“ bigquery under the hood”中获取了有关bigquery体系结构的信息,还从许多其他来源中获取了一些信息。
其他目标和自我约束:
- 时间序列存储可扩展到单个群集中的pb级压缩数据和100k处理核心。
- 云优先:利用云的优势。
- 从数十兆兆字节的数据和一千个处理内核开始,具有成本效益。
- 在合理规模的群集中,处理少于5 tb数据的查询应在3秒以内(p99延迟)运行-涵盖交互式广告分析用例。
- 高度一致的查询延迟:相似的查询应始终花费相同的时间来完成,而不管集群中并行运行的查询是什么。
- 新摄取的数据应立即可查询。
- 仔细想想:提出的设计有望在3-5年内变得越来越重要,而不是不那么重要。
非目标:
- 本地部署。
- 小规模的成本效益。
- 随机更新和删除旧数据的效率,尽管这些事情应该是可能的。
- 对于任何小的查询,即使在没有负载的系统中,p99的等待时间也不到半秒。
- 易于首次部署和软件更新。
最后的介绍性说明:这篇文章基于在metamarkets大规模运行druid的经验和理论研究,但是所描述的设计尚未在生产中实施和测试。这篇文章中的某些陈述是错误的。如果您有任何意见或更正,请在此帖子下发表评论!
设计概述
具有三个解耦子系统的时间序列存储的设计。浅蓝色线表示未压缩的面向行的数据流;深蓝线-压缩的柱状数据;红线-查询结果。
该系统由三部分组成,各部分之间有严格的职责分离:流处理系统,存储和计算树。
流处理系统摄取数据(接受“写入”),对其进行分区,将每个时间间隔内的数据转换为压缩的列格式并将其写入storage。流处理系统的工作人员还负责计算最新数据的部分查询结果。
计算树具有多个级别的节点:最低级别的节点从storage中下载特定分区和间隔的数据,并为其计算部分结果。如果查询间隔包括最新数据,则第二层中的节点合并特定分区的所有分区的结果,并接受最低层中的节点和stream处理系统的工作程序的接受。第三级中的节点合并或合并第二级中节点的每个时间间隔结果,并包含每个时间间隔查询结果的缓存。这些节点还可能负责群集平衡和较低级别的计算树的自动缩放。
此设计的关键原则:
计算和存储的分离。这个想法来自bigquery。在我有关druid问题的文章中,我解释了druid中缺少这种分隔如何使查询延迟不可预测,因为查询之间会相互干扰。
使计算树中的节点(几乎)是无状态的,这意味着它们是“一次性”的。它们可能是亚马逊的ec2或google的可抢占实例,它们比普通实例便宜几倍。同样,计算树可以在数分钟之内放大和缩小,从而有可能e。g。在查询负载较低时,每晚和周末将其按比例缩小。
数据摄取(在流处理系统中)和存储分开。这个想法实际上已经在druid中实现,它具有实时节点。这样的关注点分离可以使storage保持非常简单,不需要分配资源来进行提取,列压缩,查询处理等。它只专注于从磁盘读取字节块并将其通过网络发送到计算中的节点和树。
流处理系统也可能比支持写操作的存储更动态。流处理系统可以根据数据摄取强度的变化而按比例放大或缩小,通常在晚上和周末较低。流处理系统可能具有在存储中难以实现的功能,例如动态重新分区。
网络是瓶颈
如果查询的下载量没有使storage的出站网络带宽饱和,则网络对总查询延迟的贡献是恒定的,并且与查询大小无关。如果将云对象存储用作存储(请参阅下面的“云对象存储”部分),或者相对于存储中的历史数据量,系统中的查询负载不成比例地较小,则可以授予此权限。
如果这两个条件都不适用,则可以使用storage托管一些非时间序列的,下载频率较低的数据,以便人为地增加storage群集的大小,从而增加其出站网络带宽。
否则,在存储和计算树之间的网络吞吐量可能将成为限制所提出设计中查询延迟的因素。有几种方法可以减轻这种情况:
- 与仅生成一个表的典型sql查询不同,对该系统的查询应组成所有子查询,而这些子查询是在分析界面的单个屏幕上所需的。analytics(分析)界面通常包括至少几个,有时是几十个表,图表等,它们是同一时间序列数据的子查询的结果。
- 在第三级计算树中慷慨地缓存查询结果,以减少重做相同计算的负载。
- 投影下推:仅从存储区下载查询处理所需的列子集。
- 按维度键分区(最常出现在查询过滤器中)仅下载和处理所需的分区-谓词下推式。由于许多实际数据维度中的密钥频率是poisson-,zipf-或其他不均匀分布的,因此理想情况下,stream处理系统应支持“部分”分区,请参见下图。由于这种分区的基数较低,因此可以在各个分区变得太小而无法以列格式和处理进行有效压缩之前,将数据按多个维度进行分区。
部分分区可实现密钥分配不均。每个盒子都是一个分区。具有“其他值”的分区可能具有数千个“长尾”值。
- 更一般而言,数据段(分区)的元数据应包括有关所有维度的信息,该维度似乎在此分区中仅填充了一个(或很少)键,从而可以从“意外”分区中受益。
- 色谱柱压缩应强烈支持压缩率,而不是减压或处理速度。
- 列数据应从存储流式传输到计算树中的节点,并且一旦所有必需列的第一个块到达计算节点,就开始子查询处理。这样可以使网络和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的*问题。对于其他条件,go的运行时和生成的代码可能效率较低,但是出于某些非技术性原因,它比rust更有效。
提议的时间序列系统的缺点
- 该系统感觉不像是一个单一的“数据库”,它具有三个独立的子系统,其中活动部件的总数很高,这使其在小规模上效率不高,难以部署和更新。
- 将系统与现有的说sql的接口有效地集成可能是一个挑战,因为系统需要对同一张表运行带有许多独立子查询的“复合”查询。
- 该系统不适用于需要对查询的响应速度超过一秒的用例。
- 系统的性能高度依赖于部署它的数据中心中的网络性能。
- 在某些用例中,无法在第三级计算树中水平缩放节点可能是主要的可伸缩性瓶颈。