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 旨在实现这些策略。然而,鉴于本文到目前为止的篇幅,我们将在下一篇文章中讨论这些主题。
如果你对以上内容感兴趣且需要帮助的话,可以登录https://www.deepexi.com/product-new/27了解更多 TSDB 产品详情。
版权声明: 本文为 InfoQ 作者【滴普科技2048实验室】的原创文章。
原文链接:【http://xie.infoq.cn/article/257665a768f3a15263368ca83】。文章转载请联系作者。
评论