Bootstrap

PostgreSQL正强势回归!

作者:咖啡

三十多年前发布第一个released以来,PostgreSQL正强势回归。PostgreSQL是现今增长最快的数据库,比MongoDB、Redis、MySQL和其它数据库的增长速度都要快。PostgreSQL功能上也已经成熟和扩展,这得益于核心的维护者和不断增长的社区。

然而,PostgreSQL有一个遭人诟病的地方,水平扩展能力差。数位PostgreSQL贡献者已经为OLTP负载和时序负载开发了水平扩展功能选项,而我们研究的,是另外的问题。

问题:时序负载差异

简单来说,时序负载与传统数据库(OLTP)负载不一样。有几个原因:写入即插入、无大规模更新数据、插入数据的实时性、连续时间范围内的读取,而非随机读取、读写分离,一个事务中很少既有写又有读。相比传统数据库,时序数据的数据量要大很多,压缩率也高很多。因此,扩展写、读和存储是时序数据库的关注点。

单个TSDB节点可以扩展到每秒200万个指标和10兆兆字节的数据存储,可以满足大部分的需求。但随着工作量的增大和软件服务的发展,总会追求更强大的性能,这就需要一个基于PostgreSQL的时许数据库的分布式系统。

解决:分块,而非分片

我们新的分布式架构:可扩展到每秒超过1000万个指标,存储pb级的数据,通过更好的并行化处理获得更快的查询速率。从本质上讲,系统可以随着您和您的时序负载的工作量增长而增长。

所有数据库系统水平扩展到多个节点,都是依赖于对数据进行单个维度的切割分片,然后把分片数据存到不同的节点上。

TSDB不通过传统分片的方法作水平扩张,取而代之,我们采用了单节点体系结构的核心概念:块。块是通过按多个维度(其中一个维度是时间)自动对数据进行分区而创建的。创建块是一个细粒度的操作,在单个节点上,一个数据集就可以包含上千个块。

分片通常只支持水平扩展,与分片不同,分块支持广泛,功能强大。例如:

  • 纵向扩展(同一个节点)和水平扩展(多个节点)

  • 弹性扩张:通过让数据增长到新节点并淘汰旧节点从而实现添加和删除节点

  • 分区灵活性:在不停机的情况下更改块大小或分区尺寸(例如,考虑到增加的插入率或额外的节点)

  • 数据保存策略:删除超过阈值的块

  • 数据堆叠:将旧的数据块从更快、更昂贵的存储空间转移到更便宜、更慢的存储空间

  • 数据重排:基于写入模式以一种顺序(例如,按时间)写入数据,然后根据查询模式以另一种顺序(例如,device_id)重写数据

基准测试

早期基准测试结果,表明我们的分布式系统架构能够维持高写速率。如下,增加到9个节点时,系统达到每秒超过1200万个指标的插入率:

TSDB运行开源的Time Series Benchmarking Suite,m5.2xlarge数据节点和m5.12xlarge访问节点运行部署在AWS上,两者都具有标准的EBS gp2存储。

数据库伸缩的5个目标

基于我们到经验与用户到交互,我们确定了将数据库扩展到时序负载的五个目标:

  • 总存储卷: 管理扩展更大的数据量

  • 插入速率: 提供更高的行或数据点每秒的摄入速率

  • 查询并发性: 提供更大规模的并发查询,有时通过数据副本实现

  • 查询延迟: 通过并行化查询以减少单个大数据量查询的延迟

  • 容错: 存储相同数据到多个服务/磁盘, 在出现故障时,可以自动进行故障转移

TSDB利用PostgreSQL流复制来实现主/从集群:一个主节点接受所有写操作,然后将其数据(更具体地说,是预写日志)流到一个或多个副本。

使用PostgreSQL流复制的TSDB要求每个副本存储数据集的完整副本,架构最大摄取速率为主端写速率,最大查询延迟为单个节点的CPU/IOPS速率。

设计规模

在计算机科学中,解决大问题的关键是把它们分解成更多子问题,然后解决每个子问题,最好能并行解决。

在TSDB中,分块是处理PostgreSQL时序负载的一种机制。TSDB在同一个实例上自动地跨多个块对一个表进行分区,既可以在同一个磁盘上也可以在不同的磁盘上。但是管理大量的块(即“子问题”)也是一项艰巨的任务,所以我们提出了超表,使分区表易于使用和管理。超表可以自然地扩展到多个节点:我们现在不在同一个实例上创建块,而是将它们放在不同的实例中。

尽管如此,在大规模操作时,分布式超级表在管理和可用性方面提出了新的挑战,为了让超表好用,我们围绕以下原则仔细设计了系统。

设计原则

  • 使用已有的抽象:超表和块可以自然地扩展到多个节点。基于现有的这些抽象和PostgreSQL已有到功能,我们为水平扩展集群提供了一个健壮的常见的基础设施。

  • 透明度:从用户的角度,使用分布式超表应该和使用常规超表类似,例如,熟悉环境、命令、功能、元数据和表。用户不需要知道他们正在与分布式系统交互,并且在交互时不需要采取特殊的操作(例如,应用程序感知分片管理。

  • 访问存储分离:鉴于访问和存储需求因工作负载(和时间)而异,系统应该能够独立地扩展访问和存储。一种实现方法是通过使用两种数据库节点,一种用于外部访问(“访问节点”),另一种用于数据存储(“数据节点”)。

  • 容易运作:一个实例应该可以作为访问节点或者作为数据节点,或者同时兼任两种角色。根据元数据和服务发现,各节点根据需要作为对应角色运行。

  • 数据位置灵活性: 设计要考虑数据放置的灵活性和可复制. 可以支持并置的JOIN优化、异构节点、数据分层、支持AZ-aware等等. 作为访问节点的实例也应该能够作为数据节点,并存储非分布式表。

  • 支持生产部署:设计应支持高可用部署,数据跨多个服务备份,系统自检并从故障节点中恢复。

介绍分布式超表

按上述设计原则,构建一个更大规模和更高性能的,多节点数据库系统,超表可以跨多个节点,用户与分布式超级表的交互方式与与常规超级表(其本身看起来就像常规Postgres表)非常相似。因此,在分布式超表中插入数据或从超表中查询数据看起来与在标准表中插入数据是相同的。

用下列schema设置一个表:

对time和device_id列进行分区,表变成了一个分布式超级表:

输入命令后,表仍然正常运行,可以进行插入、查询、修改schema等操作。用户不必担心元组路由、块(分区)创建、负载平衡和故障恢复,系统会自动透明的处理这些问题。实际上,用户可以无缝地将独立的TSDB实例合并到集群中来将现有的超级表转换为分布式超级表。

架构:访问节点和数据节点

分布式数据库架构包含访问节点(客户机连接到这些节点)和数据节点(分布式超级表的数据驻留在这些节点中)。

两种节点使用不通的配置运行相同的TSDB/PostgreSQL堆栈。访问节点需要元数据(例如,目录信息)在数据库集群中追踪定位,例如集群中存储数据(块存储)的节点,在匹配块的节点中插入数据,避免在不匹配块的节点中进行查询。尽管访问节点有大量的分布式信息,但数据节点比较“单纯”,本质上是一个单节点。用简单的admin方法可以从节点中添加和删除数据节点。

创建分布式超级表与创建常规超级表的主要区别是,建议使用二级“空间”分区维。虽然这不是一个严格的要求,但是附加的“空间”维度确保当一个表经历(大致)按时间顺序的插入时,数据均匀地分布在所有数据节点上。

上图说明了多维分布式超表的优点。对于只使用时间的分区,两个时间区间的块(t1和t2),会顺序的在节点DN1和DN2上创建。使用多维区分,每个时间间隔在不同节点上沿着空间维度创建块。因此,对t1的插入分布在多个节点上,而不仅仅是其中一个节点。

问题: 这不就是分片吗?

不完全是,虽然是通过传统(单一维度)的“数据库分片”方法实现的(其中分片的数量与服务器的数量成比例),但分布式超表是为使用大量块(从100块到10000块)进行多维分块设计,在集群上分布更灵活。另一方面,传统的分片通常是预先创建的,并从一开始就绑定到各个服务器。因此,向分片系统添加新服务器通常是一个困难且具有破坏性的过程,可能需要重新分发(和锁定)大量数据。

相比之下,TSDB的多维分块自动创建块,将最近的数据块保存在内存中,并提供面向时间的数据生命周期管理(例如,数据保留、重新排序或分层策略)。

在多节点上下文中,细粒度的分块也是如此:

  • 通过在多个节点和磁盘上并行操作,提高磁盘总IOPS

  • 节点之间水平负载查询

  • 弹性扩展到新的数据节点

  • 备份数据以实现容错和负载均衡

块可以基于最新的分区配置自动创建和分配大小。一个新块的大小和划分可以与之前的块不一样,两者可以在系统内共存。这使得一个超标可以无缝地扩张到一个新节点,使用最新的分区配置而非新节点上的配置,创建块,而不会影响到现有的数据或者需要漫长的锁。连同最终丢弃旧块的保留策略,集群将随着时间重新平衡,如下图所示:

与类似的分片系统相比,破坏性更小,因为读锁每次持有的块的数据更小。

有人可能会认为分块给应用程序和开发人员增加了额外的负担。然而,TSDB中的应用程序并不直接与块交互(因此不需要知道这个分区映射本身,不像在一些分片系统中),系统也不公开块与整个超表不同的功能(例如,在许多其他存储系统中,执行事务可以在分片内,但不能跨分片)。

工作原理:请求的生命周期(插入和查询)

在了解了如何创建分布式超级表和底层架构之后,让我们看看“请求的生命周期”,以便更好地理解访问节点和数据节点之间的交互。

插入数据

在下面的例子中,我们将继续使用前面介绍的“measurements”表。要将数据插入到这个分布式超级表中,一个客户端连接到访问节点,并按正常方式插入一批值。使用一批值比逐行插入更好,这样可以获得更高的吞吐量。这样的批处理是一个非常常见的架构习惯做法,例如,从Kafka、Kinesis、IoT hub或Telegraf插入数据到TSDB。

因为“measurements”是一个分布式超表,所以访问节点不会像常规超表那样在本地插入这些行。相反,它使用它的目录信息(元数据)来最终确定应该存储数据的数据节点集。对于要插入的新行,它首先使用分区列的值(例如,time和device_id)将每一行映射到一个块,然后确定每个行集应该插入到的块,如下图所示:

如果某些行还没有合适的块,TSDB将在插入事务中创建新的块,然后将每个新块分配给至少一个数据节点。如果存在“空间”维度(device_id),则访问节点将沿着该维度创建并分配新的块。因此,每个数据节点只负责设备的一个子集,但它们都将在相同的时间间隔内进行写操作。

在访问节点写入每个数据节点之后,执行二段提交,将这些小批处理提交到相关的数据节点,以便将属于原始插入批处理的所有数据原子地插入到一个事务中。这还确保在插入某个数据节点失败时(例如,由于数据冲突或失败的数据节点),可以回滚所有的小批处理。

下面显示了单个数据节点接收的SQL查询部分,它是原始插入语句中的行的子集。

利用PostgreSQL EXPLAIN信息

TSDB与PostgreSQL查询器紧密结合的一个好处是,它公开了EXPLAIN信息。您可以EXPLAIN任何请求(如上面的INSERT请求)并获得完整的计划信息:

在PostgreSQL中,像上面这样的计划是树,当计划执行时,每个节点产生一个元组(数据行)。从本质上讲,来自根节点的父节点会请求新的元组,直到不能产生更多的元组为止。在这个特定的插入计划中,元组起源于ValueScan叶节点,每当ChunkDispatch父节点请求一个元组时,该节点就从原始插入语句生成一个元组。每当ChunkDispatch从它的子节点读取一个元组时,它就会将该元组“路由”到一个块,并在必要时在数据节点上创建一个块。然后将元组提交给DataNodeDispatch树,DataNodeDispatch如前面步骤中路由到的块所提供的那样,在每个节点的缓冲区中缓冲元组(每个块都有一个或多个负责该块的关联数据节点)。DataNodeDispatch将为每个数据节点(可配置)缓冲最多1000个元组,直到它使用给定的远程SQL刷新缓冲区。EXPLAIN中显示了所涉及的服务器,但并非所有服务器最终都能接收数据,因为计划器无法在计划时知道元组在执行期间将如何路由。

应该注意的是,分布式超表还支持COPY,以便在插入期间进一步提高性能。使用COPY的插入不会执行计划,就像上面INSERT所示的那样。相反,元组直接从客户机连接读取(在COPY模式下),然后路由到相应的数据节点连接(也是在COPY模式下)。因此,元组以很小的开销流到数据节点。虽然COPY适合批量数据加载,但有某些功能不支持,例如RETURNING从句,因此限制copy的使用场景。

查询数据

分布式超表上的读取查询遵循从访问节点到数据节点的类似路径。客户端向访问节点发出一个标准的SQL请求:

在分布式超表上实现此查询性能依赖于三种策略:

  • 限制工作量

  • 最优分配和下推工作到数据节点上

  • 跨数据节点并行执行

TSDB旨在实现这些策略。然而,鉴于本文到目前为止的篇幅,我们将在下一篇文章中讨论这些主题。

如果你对以上内容感兴趣且需要帮助的话,可以登录了解更多 TSDB 产品详情。