故障转移集群 PostgreSQL + Patroni。 实施经验

在本文中,我将告诉您我们如何解决 PostgreSQL 容错问题、为什么它对我们变得如此重要以及最终发生了什么。

我们拥有高负载的服务:全球有 2,5 万用户,每天有超过 50 万活跃用户。 服务器位于爱尔兰一个地区的 Amazone:100 多台不同的服务器持续工作,其中近 50 台带有数据库。

整个后端是一个大型的整体式有状态 Java 应用程序,它与客户端保持持续的 Websocket 连接。 当多个用户同时在同一块板上工作时,他们都会实时看到更改,因为我们将每个更改写入数据库。 我们的数据库每秒约有 10K 个请求。 在 Redis 的峰值负载下,我们每秒写入 80-100K 请求。
故障转移集群 PostgreSQL + Patroni。 实施经验

为什么我们从 Redis 切换到 PostgreSQL

最初,我们的服务使用 Redis,这是一种将所有数据存储在服务器 RAM 中的键值存储。

Redis 的优点:

  1. 响应速度高,因为一切都存储在内存中;
  2. 易于备份和复制。

Redis 对我们来说的缺点:

  1. 没有真实的交易。 我们尝试在应用程序级别模拟它们。 不幸的是,这并不总是能很好地工作,并且需要编写非常复杂的代码。
  2. 数据量受到内存量的限制。 随着数据量的增加,内存也会增长,最终我们会遇到所选实例的特征,这在AWS中需要停止我们的服务来更改实例的类型。
  3. 有必要不断保持低延迟水平,因为。 我们有大量的请求。 我们的最佳延迟水平是 17-20 毫秒。 在 30-40 毫秒的水平上,我们会收到对应用程序请求的长响应和服务降级。 不幸的是,这种情况在 2018 年 2 月发生在我们身上,当时 Redis 的一个实例由于某种原因收到的延迟是平时的 XNUMX 倍。 为了解决该问题,我们在中午停止了服务以进行计划外维护,并更换了有问题的 Redis 实例。
  4. 即使代码中有很小的错误,也很容易出现数据不一致的情况,然后花费大量时间编写代码来纠正这些数据。

我们考虑到了缺点,并意识到我们需要转向更方便的方式,进行正常的交易并减少对延迟的依赖。 进行了研究,分析了许多选项并选择了 PostgreSQL。

我们已经迁移到新数据库 1,5 年了,并且只迁移了一小部分数据,所以现在我们同时使用 Redis 和 PostgreSQL。 有关在数据库之间移动和切换数据的阶段的更多信息,请参阅 我同事的文章.

当我们第一次开始迁移时,我们的应用程序直接与数据库一起工作并访问主Redis和PostgreSQL。 PostgreSQL 集群由一个主服务器和一个异步复制的副本组成。 数据库方案如下所示:
故障转移集群 PostgreSQL + Patroni。 实施经验

实施 PgBouncer

当我们搬家的时候,产品也在发展:使用 PostgreSQL 的用户数量和服务器数量增加,我们开始缺乏连接。 PostgreSQL 为每个连接创建一个单独的进程并消耗资源。 您可以将连接数增加到某个点,否则有可能获得次优的数据库性能。 在这种情况下,理想的选择是选择位于底座前面的连接管理器。

我们有两个连接管理器选项:Pgpool 和 PgBouncer。 但第一个不支持使用数据库的事务模式,因此我们选择了 PgBouncer。

我们设置了以下工作方案:我们的应用程序访问一个 PgBouncer,其后面是 PostgreSQL master,每个 master 后面是一个异步复制的副本。
故障转移集群 PostgreSQL + Patroni。 实施经验

同时,我们无法将全部数据存储在 PostgreSQL 中,并且使用数据库的速度对我们来说很重要,因此我们开始在应用程序级别对 PostgreSQL 进行分片。 上面描述的方案对此相对方便:当添加新的 PostgreSQL 分片时,更新 PgBouncer 配置就足够了,应用程序可以立即使用新分片。

PgBouncer 故障转移

这个方案一直有效,直到唯一的 PgBouncer 实例死亡为止。 我们在 AWS 中,所有实例都在定期失效的硬件上运行。 在这种情况下,实例只需转移到新硬件并再次工作即可。 PgBouncer 也发生过这种情况,但它变得不可用。 今年秋天的结果是我们的服务有 25 分钟不可用。 AWS建议针对这种情况使用用户端冗余,而当时我国还没有实施这种方式。

之后,我们认真考虑了 PgBouncer 和 PostgreSQL 集群的容错能力,因为我们的 AWS 账户中的任何实例都可能发生类似的情况。

我们构建的PgBouncer容错方案如下:所有应用服务器都访问网络负载均衡器,其后面有两个PgBouncer。 每个 PgBouncer 都会查看每个分片的同一个 PostgreSQL master。 如果 AWS 实例再次崩溃,所有流量都会通过另一个 PgBouncer 进行重定向。 网络负载均衡器故障转移由 AWS 提供。

此方案可以轻松添加新的 PgBouncer 服务器。
故障转移集群 PostgreSQL + Patroni。 实施经验

创建 PostgreSQL 故障转移集群

在解决这个问题时,我们考虑了不同的选择:自写故障转移、repmgr、AWS RDS、Patroni。

自写脚本

他们可以监视主服务器的工作,如果发生故障,则将副本提升为主服务器并更新 PgBouncer 配置。

这种方法的优点是最简单,因为您自己编写脚本并准确了解它们的工作原理。

缺点:

  • master可能并没有死掉,而是可能出现了网络故障。 故障转移在不知道这一点的情况下,会将副本提升为主服务器,而旧主服务器将继续工作。 结果,我们将获得两台充当主服务器角色的服务器,并且我们不知道其中哪一台拥有最新的数据。 这种情况也称为裂脑;
  • 我们没有得到任何回应。 在我们的配置中,master和replica,切换后,replica上移到master,我们就不再有replica了,所以我们必须手动添加一个新的replica;
  • 我们需要对故障转移操作进行额外的监控,而我们有 12 个 PostgreSQL 分片,这意味着我们必须监控 12 个集群。 随着分片数量的增加,您还必须记住更新故障转移。

自写的故障转移看起来非常复杂,需要不简单的支持。 对于单个 PostgreSQL 集群,这将是最简单的选择,但它无法扩展,因此不适合我们。

雷普格

Replication Manager for PostgreSQL Cluster,可以管理 PostgreSQL 集群的操作。 同时,它没有开箱即用的自动故障转移功能,因此在工作中,您需要在已完成的解决方案之上编写自己的“包装器”。 所以一切都可能比自己编写的脚本更复杂,所以我们甚至没有尝试 Repmgr。

云数据库

支持我们需要的一切,知道如何进行备份并维护连接池。 它具有自动切换功能:当master死亡时,副本成为新的master,AWS将dns记录更改为新的master,而副本可以位于不同的AZ。

缺点包括缺乏精细调整。 作为微调的一个例子:我们的实例对 tcp 连接有限制,不幸的是,这在 RDS 中无法完成:

net.ipv4.tcp_keepalive_time=10
net.ipv4.tcp_keepalive_intvl=1
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_retries2=3

此外,AWS RDS的价格几乎是普通实例价格的两倍,这也是放弃该解决方案的主要原因。

帕特罗尼

这是一个用于管理 PostgreSQL 的 python 模板,具有良好的文档、自动故障转移和 github 上的源代码。

帕特罗尼的优点:

  • 每个配置参数都有描述,其工作原理一目了然;
  • 自动故障转移开箱即用;
  • 用python写的,而且由于我们自己也用python写了很多,所以我们处理问题会更容易,甚至可能对项目的开发有帮助;
  • 完全管理 PostgreSQL,允许您一次更改集群所有节点上的配置,如果需要重新启动集群以应用新配置,则可以使用 Patroni 再次完成此操作。

缺点:

  • 文档中并不清楚如何正确使用 PgBouncer。 虽然很难称之为减号,因为Patroni的任务是管理PostgreSQL,如何连接到Patroni已经是我们的问题了;
  • 大量实施 Patroni 的例子很少,而从头开始实施的例子很多。

因此,我们选择 Patroni 来创建故障转移集群。

帕特罗尼实施流程

在 Patroni 之前,我们有 12 个 PostgreSQL 分片,采用异步复制的一主一副本配置。 应用程序服务器通过网络负载均衡器访问数据库,网络负载均衡器后面是两个带 PgBouncer 的实例,后面都是 PostgreSQL 服务器。
故障转移集群 PostgreSQL + Patroni。 实施经验

为了实现 Patroni,我们需要选择分布式存储集群配置。 Patroni 与分布式配置存储系统配合使用,例如 etcd、Zookeeper、Consul。 我们市场上只有一个成熟的 Consul 集群,它与 Vault 结合使用,但我们不再使用它。 这是开始使用 Consul 来实现其预期目的的一个重要原因。

Patroni 如何与 Consul 合作

我们有一个Consul集群,它由三个节点组成,还有一个Patroni集群,它由一个领导者和一个副本组成(在Patroni中,主节点称为集群领导者,从节点称为副本)。 Patroni 集群的每个实例不断向 Consul 发送有关集群状态的信息。 因此,从 Consul 中你总是可以了解 Patroni 集群当前的配置以及当前谁是领导者。

故障转移集群 PostgreSQL + Patroni。 实施经验

要将Patroni连接到Consul,研究一下官方文档就足够了,它说你需要指定一个http或https格式的主机,具体取决于我们如何与Consul合作,以及连接方案,可选:

host: the host:port for the Consul endpoint, in format: http(s)://host:port
scheme: (optional) http or https, defaults to http

看起来很简单,但陷阱就从这里开始了。 使用 Consul,我们通过 https 建立安全连接,我们的连接配置将如下所示:

consul:
  host: https://server.production.consul:8080 
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

但这是行不通的。 启动时,Patroni 无法连接到 Consul,因为它无论如何都会尝试通过 http。

Patroni 的源代码有助于解决这个问题。 好东西是用 python 写的。 原来是没有对host参数进行任何解析,必须在scheme中指定协议。 这就是我们使用 Consul 的工作配置块的样子:

consul:
  host: server.production.consul:8080
  scheme: https
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

领事模板

因此,我们选择了配置的存储。 现在我们需要了解当 Patroni 集群中的 Leader 发生变化时,PgBouncer 将如何切换其配置。 文档中没有这个问题的答案,因为。 原则上,这里没有描述与 PgBouncer 的工作。

在寻找解决方案时,我们找到了一篇文章(不幸的是我不记得标题了),其中写道 Сonsul-template 在配对 PgBouncer 和 Patroni 方面帮助很大。 这促使我们研究 Consul-template 是如何工作的。

原来,Consul-template 不断监控 Consul 中 PostgreSQL 集群的配置。 当领导者发生变化时,它会更新 PgBouncer 配置并发送命令来重新加载它。

故障转移集群 PostgreSQL + Patroni。 实施经验

模板的一大优点是它存储为代码,因此当添加新分片时,只需进行新的提交并自动更新模板即可,支持基础设施即代码原则。

Patroni 的新架构

结果,我们得到了以下工作方案:
故障转移集群 PostgreSQL + Patroni。 实施经验

所有应用服务器都访问平衡器 → 后面有两个 PgBouncer 实例 → 在每个实例上,启动 Consul-template,它监视每个 Patroni 集群的状态并监视 PgBouncer 配置的相关性,该配置向当前领导者发送请求每个簇的。

手动测试

我们在小型测试环境上启动该方案之前运行了该方案并检查了自动切换的操作。 他们打开了木板,移动了贴纸,就在那一刻,他们“杀死”了集群的领导者。 在 AWS 中,这就像通过控制台关闭实例一样简单。

故障转移集群 PostgreSQL + Patroni。 实施经验

贴纸在 10-20 秒内返回,然后再次开始正常移动。 这意味着 Patroni 集群工作正常:它更改了领导者,将信息发送到 Сonsul,然后 Сonsul-template 立即获取此信息,替换 PgBouncer 配置并发送重新加载命令。

如何在高负载下生存并最大限度地减少停机时间?

一切都很完美! 但有新的问题:它在高负载下如何工作? 如何快速、安全地推出生产中的一切?

我们进行负载测试的测试环境帮助我们回答了第一个问题。 它在架构方面与生产完全相同,并且生成的测试数据量与生产数据量大致相等。 我们决定在测试期间“杀死”一位 PostgreSQL master,看看会发生什么。 但在此之前,检查自动滚动非常重要,因为在此环境中我们有多个 PostgreSQL 分片,因此我们将在生产之前对配置脚本进行出色的测试。

这两项任务看起来都雄心勃勃,但我们有 PostgreSQL 9.6。 可以立即升级到11.2吗?

我们决定分两步进行:首先升级到 2,然后启动 Patroni。

PostgreSQL 更新

要快速更新 PostgreSQL 版本,请使用该选项 -k,其中在磁盘上创建硬链接,无需复制数据。 在300-400GB的基础上,更新需要1秒。

我们有很多分片,所以更新需要自动完成。 为此,我们编写了一个 Ansible 剧本来为我们处理整个更新过程:

/usr/lib/postgresql/11/bin/pg_upgrade 
<b>--link </b>
--old-datadir='' --new-datadir='' 
 --old-bindir=''  --new-bindir='' 
 --old-options=' -c config_file=' 
 --new-options=' -c config_file='

这里需要注意的是,在开始升级之前,必须使用参数来执行 - 查看以确保您可以升级。 我们的脚本还在升级期间替换配置。 我们的脚本在 30 秒内完成,这是一个非常好的结果。

启动帕特罗尼

解决第二个问题,只要看Patroni的配置即可。 官方存储库有一个 initdb 的示例配置,它负责在您第一次启动 Patroni 时初始化一个新数据库。 但由于我们已经有一个现成的数据库,我们只是从配置中删除了这一部分。

当我们开始在现有的 PostgreSQL 集群上安装 Patroni 并运行它时,我们遇到了一个新问题:两台服务器都作为领导者启动。 Patroni 对集群的早期状态一无所知,并尝试将两台服务器作为两个具有相同名称的独立集群启动。 要解决这个问题,需要删除slave上有数据的目录:

rm -rf /var/lib/postgresql/

这只需要在从站上完成!

当连接一个干净的副本时,Patroni 会创建一个 BaseBackup Leader 并将其恢复到副本中,然后根据 wal 日志赶上当前状态。

我们遇到的另一个困难是所有 PostgreSQL 集群默认都命名为 main。 当每个集群对另一个集群一无所知时,这是正常的。 但是当你想使用Patroni时,那么所有集群都必须有一个唯一的名称。 解决方案是更改 PostgreSQL 配置中的集群名称。

负载测试

我们启动了一项模拟用户在板上体验的测试。 当负载达到我们的平均每日值时,我们重复完全相同的测试,我们关闭了 PostgreSQL 领导者的一个实例。 自动故障转移按我们的预期进行:Patroni 更改了领导者,Consul-template 更新了 PgBouncer 配置并发送了重新加载的命令。 根据我们在 Grafana 中的图表,很明显,与数据库连接相关的服务器存在 20-30 秒的延迟和少量错误。 这是正常情况,这样的值对于我们的故障转移来说是可以接受的,并且绝对比服务停机要好。

将 Patroni 投入生产

因此,我们提出了以下计划:

  • 将Consul模板部署到PgBouncer服务器并启动;
  • PostgreSQL更新至11.2版本;
  • 更改集群名称;
  • 启动 Patroni 集群。

同时,我们的方案允许我们几乎在任何时候都可以提出第一点,我们可以依次将每个 PgBouncer 从工作中删除,并在其上部署并运行 consul-template。 所以我们做到了。

为了快速部署,我们使用了 Ansible,因为我们已经在测试环境中测试了所有 playbook,并且每个分片的完整脚本的执行时间为 1,5 到 2 分钟。 我们可以在不停止服务的情况下将所有内容依次部署到每个分片,但我们必须关闭每个 PostgreSQL 几分钟。 在这种情况下,数据在这个分片上的用户此时无法完全工作,这对我们来说是不可接受的。

解决这种情况的方法是每 3 个月进行一次计划维护。 这是计划工作的窗口,当我们完全关闭服务并升级数据库实例时。 距离下一个窗口期还有一周时间,我们决定等待并进一步准备。 在等待期间,我们还保护了自己:对于每个 PostgreSQL 分片,我们都会创建一个备用副本,以防无法保留最新数据,并为每个分片添加一个新实例,该实例应该成为 Patroni 集群中的新副本,以免执行删除数据的命令。 所有这些都有助于最大限度地减少错误风险。
故障转移集群 PostgreSQL + Patroni。 实施经验

我们重新启动了服务,一切正常,用户继续工作,但在图表上我们注意到 Consul 服务器上的负载异常高。
故障转移集群 PostgreSQL + Patroni。 实施经验

为什么我们在测试环境中没有看到这个? 这个问题很好地说明了有必要遵循基础设施即代码原则,完善从测试环境到生产的整个基础设施。 否则,很容易出现我们遇到的问题。 发生了什么? Consul首先出现在生产环境中,然后出现在测试环境中,因此,在测试环境中,Consul的版本高于生产环境。 就在其中一个版本中,使用 consul-template 时出现的 CPU 泄漏问题得到了解决。 因此,我们只需更新Consul,就解决了问题。

重启Patroni集群

然而,我们遇到了一个我们甚至没有想到的新问题。 更新Consul时,我们只需使用consul left命令从集群中删除Consul节点→Patroni连接到另一个Consul服务器→一切正常。 但是,当我们到达 Consul 集群的最后一个实例并向其发送 consul left 命令时,所有 Patroni 集群都会重新启动,并且在日志中我们看到以下错误:

ERROR: get_cluster
Traceback (most recent call last):
...
RetryFailedError: 'Exceeded retry deadline'
ERROR: Error communicating with DCS
<b>LOG: database system is shut down</b>

Patroni 集群无法检索有关其集群的信息并重新启动。

为了找到解决方案,我们通过 github 上的问题联系了 Patroni 作者。 他们建议改进我们的配置文件:

consul:
 consul.checks: []
bootstrap:
 dcs:
   retry_timeout: 8

我们能够在测试环境中复制该问题并在那里测试了这些选项,但不幸的是它们不起作用。

问题仍然没有解决。 我们计划尝试以下解决方案:

  • 在每个Patroni集群实例上使用Consul-agent;
  • 修复代码中的问题。

我们明白错误发生在哪里:问题可能是使用默认超时,该超时没有通过配置文件覆盖。 当最后一个Consul服务器从集群中移除时,整个Consul集群挂起超过一秒,正因如此,Patroni无法获取集群的状态并彻底重启整个集群。

幸运的是,我们没有再遇到任何错误。

使用 Patroni 的结果

成功启动 Patroni 后,我们在每个集群中添加了一个额外的副本。 现在,每个集群中都有一个仲裁的外观:一个领导者和两个副本,用于在切换时出现裂脑时提供安全网。
故障转移集群 PostgreSQL + Patroni。 实施经验

Patroni 已经投入制作三个多月了。 在这段时间里,他已经设法帮助我们了。 最近,其中一个集群的领导者在 AWS 中去世,自动故障转移工作正常,用户继续工作。 帕特罗尼完成了它的主要任务。

Patroni的使用小总结:

  • 易于配置更改。 在一个实例上更改配置就足够了,它将被拉动到整个集群。 如果需要重新启动才能应用新配置,Patroni 会通知您。 Patroni 只需一条命令就可以重启整个集群,也非常方便。
  • 自动故障转移有效并且已经成功帮助我们摆脱困境。
  • PostgreSQL 更新无需应用程序停机。 您必须首先将副本更新到新版本,然后更改 Patroni 集群中的领导者并更新旧的领导者。 在这种情况下,将进行必要的自动故障转移测试。

来源: habr.com

添加评论