InfoQ 推荐 ( ) • 2024-04-18 08:23

这是我们在2023年10月份旧金山QCon上的演讲"摘要,在这次演讲中,我们讨论了高可用性设置,并考虑了Cloudflare围绕系统的每个组成部分必须要做出的权衡。我们会探讨将数据库基础设施放到比以往更靠近边缘时所面临的一些性能挑战,并深入介绍已实现的解决方案。

什么是边缘?

在分布式系统中,边缘(Edge)指的是在地理位置上更靠近客户端的服务。这包括DNS解析、内容交付和IP防火墙等任务。在传统上,会由中心化的数据中心来托管关系型数据库等服务,它们距离客户端比较远。与客户端的距离远近决定了度量指标,通过在更靠近客户端的地方为请求提供服务,可以减少网络延迟和成本。

控制平面

Cloudflare保护着超过2700万个互联网属性,平均每秒钟管理4600万个HTTP请求。网络覆盖全球250个接入点(Points of Presence),在最繁忙的PostgreSQL集群上,峰值时每秒处理超过5500万次行操作,所有集群的数据量超过50TB。

 

Cloudflare的控制平面需要编排数量众多的微服务,用来支撑仪表盘。它管理数据路径上关键网络服务的规则和配置。数据库集群存储着每个消费者的数据,比如DNS记录的变更、防火墙规则、DDoS防护、API网关路由以及内部服务信息(如计费权限和用户认证)。应用团队经常利用PostgreSQL来实现特定的目的,使用存储过程来执行具有事务一致性的业务逻辑。PostgreSQL还可以用作发件箱(outbox)队列,捕获领域事件,比如在中心化数据中心由流量分析生成的DDoS规则。

 

有一个单独的守护进程(daemon)会从数据库中检索这些事件,并通过Kafka将其传递给边缘服务,从而使对延迟要求较高的服务能够绕过对数据库的直接访问,并且能够从PostgreSQL和Kafka提供的持久性中受益。

 

Cloudflare在裸机上运行软件和服务,需要处理机架式(rack-mounted)服务器、高带宽网卡和运维维护。内部自建(on-premise)的数据存储提供了最大的灵活性,允许团队对固态RAID配置进行微调,并利用开源集群管理系统对系统进行增强。而Amazon RDS等托管云数据库不可能实现集群的这种透明度和细粒度控制。

 

令人遗憾的是,这意味着当负载增加时,没有立即可用的自动扩展按钮"来提高集群的容量,这也是采用内部自建的方式运行整个技术栈所面临的固有挑战。

架构

我们来讨论一下Cloudflare的边缘数据库架构,它需要每秒处理数百万笔交易。

 

在设计系统时,团队优先考虑到了高可用性,将服务等级目标(Service-Level Objective,SLO)设定为5个9,整个技术栈每年最多停机五秒钟。事务性工作负载偏重于读取操作。基础设施必须以最小的延迟处理高速的读写操作,并保持容错性。Cloudflare在内部使用了一个任播(anycast)BGP网络",客户端会自然地在PgBouncer代理实例之间实现负载平衡。

 

BGP Anycast可优化客户端附近的读取查询,而写入查询则会转发到主数据库实例所在的主区域。

图1:Cloudflare数据库架构——图片来源"

 

在顶部,PgBouncer"为应用程序客户端管理数据库服务器连接池。然后,HAProxy在多个数据库集群实例之间平衡负载查询,防止过载。在典型的配置中,一个主实例会将数据复制到多个副本,以实现高速的读取查询,它们是由etcd支撑的stolon"高可用集群管理器来进行管理的。在此环境中,etcd"用来实现分布式集群领导共识和配置一致性。

 

主备(active-standby)模式可确保跨区域的数据冗余,位于Portland的主区域负责入站查询,而位于Luxembourg的备用区域则随时准备进行流量重定向或故障转移。

 

现在,我们将会对每一层进行分析,探讨各个组件及其权衡。

边缘上的持久化

构建高度分布式和关系型边缘数据的决定推动团队思考基本的架构原则,尤其是CAP理论"。因为应用程序是分布式的,并且依赖于PostgreSQL这样的开源软件,所以必须优先考虑一致性或高可用性,两者只能选其一,人们通常会倾向于后者。

 

在单数据中心的场景中,典型的架构包括一个主数据库、一个同步的副本以及多个异步的副本。这种拓扑结构可以跨多个数据中心复制。半自动复制的配置可以确保异步副本出现故障时,不会对应用程序造成重大的影响。同样的权衡也适用于跨区域的复制,这样能够允许一个区域的应用程序在另一个区域宕机时继续工作。但是,如果同步副本出现故障,整个应用程序会停止运行,以避免出现不一致。这种半自动复制的拓扑结构有效地平衡了各种需求。

 

PostgreSQL起源于90年代的伯克利",最初设计时采用的是单体架构。为了将其转变成分布式系统,我们在很大程度上依赖了两个方面的复制,即逻辑复制(logical replication)和流复制(streaming replication)。第三个选项是级联复制,它构建在逻辑复制和流复制之上。

 

在深入研究复制在创建分布式集群中的作用之前,我们先简单介绍一下预写式日志(write-ahead logs)(WAL)"。在包括MySQL和Oracle的所有关系型数据库中,ACID的持久性都是通过WAL实现的。对数据库的变更最初保存在易丢失的内存中,然后异步刷新到磁盘,这些更改会首先按顺序记录到基于磁盘的WAL文件中。这样就能在数据库崩溃时通过恢复确保数据的持久性。

 

实践证明,这一特性在构建复制系统时至关重要,可以方便地捕获和复制工作单元(即每个日志条目)。

 

流复制"模式非常简单:每个副本建立一个TCP连接,然后将日志条目从主副本按照流的方式传输到另一个副本。它的显著优势在于性能,每秒钟可以捕获1TB的数据变更,并且延迟极小,我们会在本文后续的内容中介绍潜在的延迟原因。需要注意的是,这是一种“全有或全无(all-or-nothing)”的方式,由于其文件系统级别具有块(block)复制的特点,所以会复制每个Postgres对象的所有变更。

 

逻辑复制"是一个更新的版本,它在SQL层面运行,提供在表、行和列级别复制数据的灵活性。不过,它无法复制DDL变更,需要自定义的工具,而且在可扩展性方面面临着挑战,尤其是在TB级的规模时。

集群管理

我们进行集群管理的主要原因之一是解决数据库故障。不管是逻辑故障(如数据损坏)还是更严重的故障(如自然灾害导致的硬件故障),它们都是难以避免的。在这种情况下,采用手动方式进行故障转移都是非常繁琐的。此外,大约有5%的Cloudflare商品服务器可能在任何时间点出现故障,这意味着上千台服务器和多个数据中心随时都会面临故障。自动化的集群管理系统对于高效处理这些多样且关键的故障是至关重要的。

 

团队选择了使用stolon",这是一个用Go编写的开源集群管理系统,在PostgreSQL集群之上运行,是很薄的一层,它具有PostgreSQL原生的接口,支持多站点冗余。不管是在单个区域内,还是跨多个区域,我们都可以跨多个PostgreSQL集群部署单个Stolon集群。Stolon的特性包括稳定的故障转移(误报率极低)以及将Keeper节点作为父进程管理PostgreSQL的变更。Sentinel节点作为编排者,监控Postgres组件的健康状况并做出决策,如启动新主副本节点的选举。Proxy节点负责处理客户端连接,确保只有一个写入主副本,避免出现多主副本的情况。

 

图2:stolon的架构

连接池

数据库连接是有限的资源,鉴于其固有的开销,所以需要对其进行有效的管理。

 

PostgreSQL连接依赖于TCP协议,其中包括三次握手和SSL握手以确保通信安全。每个连接都需要一个独立的操作系统级别的进程,这就要求主postmaster进程必须进行fork操作,从而导致CPU时间的消耗和内存的占用。随着上千的并发连接被打开,分配给事务处理的套接字描述符和CPU时间都会相应的减少。

 

服务器端连接池可以管理固定数量的数据库连接,同时向客户端提供与PostgreSQL兼容的接口。作为一个中介,当客户通过它连接到数据库时,会循环使用这些连接,从而减少打开的连接数。

 

这样可以集中控制租户资源,使团队能够调节每个上游应用服务分配的连接数。 Cloudflare对PgBouncer的增强"包括了受TCP Vegas"启发的特性,TCP Vegas是一种TCP拥塞避免算法,通过限制租户的总吞吐量来实现更严格的多租户资源隔离。

 

Cloudflare选择了使用PgBouncer作为连接池,这是因为它与PostgreSQL协议能够兼容,即客户端可以连接到PgBouncer上并像往常一样提交查询,这简化了数据库切换和故障转移的处理。PgBouncer是一个轻量级的单进程服务器,以异步方式管理连接,因此能比PostgreSQL处理更多的并发连接。

图3:PgBouncer的运行原理

 

PgBouncer引入了客户端连接,与直接的PostgreSQL服务器连接区分开来,并采用了高效的非阻塞网络I/O模型,利用单线程和操作系统提供的epoll"机制进行事件监控。

 

图4:PgBouncer进程

 

与PostgreSQL每个连接对应一个线程(thread-per-connection)的模型不同,PgBouncer的单线程事件循环避免了多个线程栈,从而能够最大限度地减少了内存使用量,并通过在一个线程中为所有客户端的请求提供服务来提高CPU利用率,防止闲置连接线程浪费CPU时间。

 

为了解决让多个单线程PgBouncer进程充分利用一台机器上所有CPU内核的难题,我们在操作系统中找到了解决方案:团队添加了SO_REUSEPORT套接字选项",允许多个进程套接字同时监听同一个端口。

负载均衡

Cloudflare使用HAProxy"负载均衡器在多个PostgreSQL服务器之间平均分配传入的数据库查询,防止单个服务器过载。与PgBouncer类似,HAProxy通过将故障服务器上的流量重定向到健康服务器上,提供了高可用性和容错性,从而减少因数据库实例性能下降而造成的停机时间。HAProxy通过使用内核的splice"系统调用,能够以最小的开销在第4层有效地处理TCP连接。入站和出站的TCP流均能在内核中转换,无需将数据复制到用户空间就可以实现数据传输。

挑战和解决方案

接下来,我们探讨数据库基础架构面临的挑战,并分享一些在边缘实现高可用性的性能技巧。复制延迟(replication lag)在大流量和写密集型操作(如执行ETL作业)时会特别明显。所有的批写入操作(如数据迁移和GDPR合规删除),甚至自动存储压缩(autovacuum")也会扩大复制延迟。

 

我们的团队将复制延迟的SLO定为60秒,并据此向应用团队提出建议。为尽量减少复制延迟,SQL写入被分批打包成较小的块,以确保复制更加顺畅。通过缓存,或者在写入主副本或同步副本后直接读取,以保持写入后读取(read-after-write)的一致性。避免延迟的一种非常规方法是将所有副本从集群中删除,只留下主副本。虽然这种方法在实践中很有效,但需要深入了解查询工作负载和潜在的系统变化。你可以将其视为Rust中的unsafe关键字。

 

2020年,一次重大事故"严重影响了Cloudflare的数据库性能和可用性。级联故障导致API可用性下降了75%,仪表盘速度慢了80倍。主区域和备用区域的主数据库实例都执行了故障转移,但主区域的新主数据库在高负载下并没有完全恢复,这导致再次出现了故障。由于集群中没有同步副本可以对外发布,因此必须做出一个关键决定:要么同步一个异步副本,但这可能会造成数据丢失,要么将区域疏散到备用集群,但会造成额外的停机时间。

 

对我们来说,数据丢失是不可接受的,因此我们选择了后一种方案。进一步的调查发现,网络交换机出现了部分故障,处于降级状态,这阻断了跨区域的两个etcd集群之间的通信。

 

无法通信导致Raft协议进入死锁状态,进而造成了只读的集群。故障转移和重新同步暴露了PostgreSQL的低效率,特别是在重新同步的耗时方面。该配置能有效处理机器崩溃这样的全面故障,但当Raft集群中的节点开始提供相互冲突的信息时,就会出现难以预料的行为的问题。

 

例如,由于交换机故障,节点1、2和3的网络连接能力下降。节点1误以为节点3不再是领导者,从而导致一系列的领导者选举失败,并形成死锁。该死锁迫使群集进入只读模式,中断了区域间的通信,并触发了向主副本的故障转移。

图5:集群中的冲突信息

 

在故障转移的过程中,同步副本进行了升级,由于事务历史可能存在分歧,因此需要旧的主副本撤销已提交的事务。在解除存在分歧的历史后,同步副本必须从更正后的主副本接收并重播新事务,在我们的场景中,由于同步副本没有吸收新数据而发生了故障,因此导致了停机。

 

我们发现的问题"包括硬件故障、Raft罕见的拜占庭故障以及Postgres重新同步时间过长。团队优先通过优化Postgres来解决第三个问题。分析日志发现,大部分时间都花在了 rsync上,在重新同步过程中复制了超过1.5 TB的日志文件。

 

图6:pg_rewind"复制所有的日志,包括最后一个分歧点前的通用段

 

解决方案包括从内部优化PostgreSQL,只复制最后一个分歧点的必要文件,这将数据传输量减少到原始大小的5%。对PostgreSQL进行内部优化后,副本重建时间从2个多小时缩短到5分钟,速度提高了95%。

图7:修复后的pg_rewind仅从最后一个分歧点复制日志

 

从这次大规模故障中,我们吸取了如下的经验教训:

预测降级的状态,而不仅仅是完全故障的状态投资开源软件,以获取内部的专业技能。我们提交的开源补丁为PostgreSQL CommitFest"做出了贡献。在内部构建和修复软件的重要性。

从边缘访问数据

仅仅在一个区域维护单个集群是不够的。为了应对各种故障,包括拜占庭式硬件故障所带来的挑战性场景,我们需要将PostgreSQL集群分布到多个区域。从Cloudflare Workers"的角度来看,这种“集群的集群(cluster-of-clusters )”方式增强了韧性。

 

Cloudflare从基于美国的主区域扩展到欧洲和亚洲,形成了使用PostgreSQL流复制的中心辐射(hub-and-spoke)模式。

 

确保跨区域的同步至关重要,它可以处理复制延迟等问题:当面临高复制延迟时,将流量转回主区域有助于满足应用团队的SLA。这一策略增强了系统的健壮性,减轻了潜在故障的影响。

 

分布式集群具有显著的优势,尤其是在实施智能故障转移方面更是如此。基于当前的负载、可用容量或时区(“follow-the-sun”——指的是根据事件发生的时间,在全球范围内选择当前在办公时间的人员来及时处理对应的问题,参见该文"——译者注)等因素都策略性地影响如何选择主区域,我们根据全球不同地区的活跃时间来降低延迟。这种动态方法解决了PostgreSQL的单主限制。

 

容量方面的考虑也会影响选择:某些地区的容量可能更大,而物流方面的挑战(如COVID期间的硬件运输)也会影响决策。流量模式和合规性要求会进一步影响区域的选择,以便于团队能够高效地满足特定的监管需求。采用分布式方法使Cloudflare能够有效地应对这些挑战。

 

将PostgreSQL扩展到多个区域并实现快速故障转移(如前面讨论的pg_rewind"等工具)是非常高效的。但是,当考虑到应用程序的依赖性时,就会出现一定的复杂性。虽然数据库的迁移是可控的,但在整个应用生态系统(包括Kafka消息队列等分布式组件)上进行故障转移却是一项挑战。在切换区域时,了解完整的应用依赖关系图至关重要。

 

图8:完整的应用依赖图

 

尽管自动化相关的工作正在进行中,但来自不同组织的团队之间的协调变得至关重要。跨区域切换服务的顺序要考虑到Kubernetes集群中的数据库集群、应用服务(如身份认证和配置),以及它们对R2和SSL等中央基础设施的依赖性。

数据库的发展趋势

作为实践者,我们观察到关系数据库的一些主要趋势。首先,人们开始将数据嵌入到边缘中,在有些区域使用单体或PostgreSQL,并辅以SQLite等嵌入式数据库进行真正的边缘处理。保持边缘的SQLite与核心/中心位置的PostgreSQL数据同步至关重要。此外,确保SQLite与PostgreSQL的兼容对于无缝集成也非常重要。

 

另一个趋势涉及边缘的持久化,由Cloudflare Workers处理客户端数据。此外,存储和计算也在向协同分布(colocation)转变,智能放置(Smart Placement)"等特性就说明了这一点。

图9:Cloudflare Workers与数据库集群的协同分布

 

这样可以避免跨区域网络跳转到中心化的数据存储,从而减少延迟,并解决了许多客户端应用程序花费大量时间与数据库通信的问题。

 

最后,数据本地化也是一项挑战,尤其是在使用PostgreSQL的欧洲地区。逻辑复制是关键点,它提供了在行或列级别进行复制的灵活性,但目前仍处于探索阶段。预计逻辑复制将在应对这一挑战方面发挥重要作用。

 

原文链接:

 Relational Data at the Edge: How Cloudflare Operates Distributed PostgreSQL Clusters"