数据库复制与故障转移图:主库、副本与脑裂防护

复制架构图应该说明写入去哪里、读取可能有多旧、由谁决定故障转移,以及如何防止出现两个主库。本文以 PostgreSQL 为例。

· 创始人兼开发者

发布于

数据库复制与故障转移图:主库、副本与脑裂防护

大多数复制架构图画的是一个主库、两个副本以及它们之间的箭头。这张图没错,但在故障处理时几乎没用,因为它说明不了此刻哪个节点在接收写入、哪个副本可能返回旧数据,也说明不了主库不再响应时会发生什么。

一张有用的复制架构图要回答四个问题。本文用 PostgreSQL、Patroni 和 etcd 逐一回答这些问题,然后把结果画出来。同样的问题也适用于 MySQL、其他故障转移管理工具以及托管服务。

每张复制架构图都必须回答的四个问题

  1. 写入去哪里? 必须恰好有一个主库,而且客户端需要有办法在故障转移后找到它。
  2. 读取可能有多旧? 每个副本要么是同步的,要么是异步的,异步副本会有延迟。
  3. 由谁决定故障转移? 必须有某个组件来检测故障、挑选副本并把它提升为主库。
  4. 如何防止出现两个主库? 被隔离的主库必须在另一个节点被提升之前停止接收写入。

同步复制与异步复制

PostgreSQL 的流复制默认是异步的。主库在备库收到数据之前就确认提交,所以一旦崩溃,尚未送达的事务就可能丢失。PostgreSQL 文档说得很直白:丢失数据的多少与故障转移时的复制延迟成正比。

同步复制会让提交等待 synchronous_standby_names 中列出的一个或多个备库。等待到什么程度,取决于 synchronous_commit:

synchronous_commit 同步备库达到以下状态后,提交才返回
remote_apply 已应用,备库上的查询能看到它
on(默认) 已刷写到持久存储
remote_write 已写入操作系统,但尚未刷盘
local 不等待备库,只等待本地刷盘
off 什么都不等,连本地磁盘也不等

代价是延迟:每次提交都包含一次到备库的往返,所以要把同步备库放在邻近的可用区,而不是另一个区域。而且,如果唯一的同步备库消失了,提交可能会一直等下去。请指定多个候选备库,或者让故障转移管理工具替你修改这个设置。

只读副本与读取路由

只要代码知道数据可能有延迟,异步副本就很适合承担报表和读取密集的接口。在主库上,pg_stat_replication 视图会显示每个备库的 write_lag、flush_lag 和 replay_lag;replay_lag 大致表示一次提交要过多久才能在副本上可见。

按查询逐一决定哪些可以读到旧数据。刚保存完表单又刷新页面的用户应该从主库读取。仪表盘则可以从副本读取。

故障转移:由谁决定

PostgreSQL 自身既不检测主库故障,也不提升备库;它的文档把这件事交给外部工具。Patroni 是常见的选择。每个节点上运行一个 Patroni 代理,代理之间通过 etcd 这样的分布式配置存储共享状态:

  • 主库持有一个带 TTL 的领导者键,并在每个循环中续期(loop_wait,默认 10 秒)。
  • 如果它在 ttl 秒内(默认 30 秒)无法续期,键就会过期,符合条件的副本开始竞选领导者。
  • 最合适的候选者拿到键并把自己提升为主库,其他副本转而跟随它。

客户端通过代理找到新的主库。Patroni 的 REST API 只在持有领导者键的节点上对 GET /primary 返回 HTTP 200,所以 HAProxy 可以把写入发给回答“是”的那个节点:

listen primary
    bind *:5000
    option httpchk OPTIONS /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg-a 10.0.1.10:5432 check port 8008
    server pg-b 10.0.2.10:5432 check port 8008
    server pg-c 10.0.3.10:5432 check port 8008

5001 端口上的第二个监听器检查 /replica?lag=16MB,WAL 落后超过 16 MB 的副本会被摘除。HAProxy 要部署在多台机器上,否则它本身就会成为你想要消除的单点故障。

脑裂:如何防止出现两个主库

两个同时接收写入的主库意味着两段分叉的历史,其中一段最终会被丢弃。PostgreSQL 文档把对策称为 STONITH:必须告诉旧主库,它已经不再是主库了。Patroni 叠加了多层防线:

  • 租约。 无法更新领导者键的主库会停止接收写入:Patroni 会停止 PostgreSQL 或将其降级。如果存储本身在超过 retry_timeout 的时间内都无法访问,Patroni 会将主库降级,除非开启了它的 DCS 故障保护模式。
  • 看门狗。 如果 Patroni 进程卡住,Linux 看门狗会重置这台机器。Patroni 会把它设为在 TTL 到期前 5 秒触发。
  • 法定人数(quorum)。 etcd 需要多数成员(三个中的两个)同意才能接受写入。三个成员分布在三个可用区,可以承受一个可用区的丢失,而被网络隔开的少数派无法选出任何人。
  • 只有一个进程说了算。 只能由 Patroni 启动 PostgreSQL。崩溃后自行重启 Postgres 的系统服务,可能会让第二个主库重新出现。

旧主库恢复后,必须以副本身份重新加入。pg_rewind 会让它的数据目录与新主库重新同步,而不复制没有变化的数据块;开启 use_pg_rewind 时,Patroni 会自动运行它。

# Patroni dynamic configuration (patronictl edit-config)
ttl: 30
loop_wait: 10
retry_timeout: 10
synchronous_mode: on        # promote only a node that has every acknowledged commit
postgresql:
  use_pg_rewind: true
  parameters:
    wal_log_hints: "on"

# Local patroni.yml tags
# pg-a: noloadbalance: true            (failover target, no reads)
# pg-c: nosync: true, nofailover: true (reads only)

开启 synchronous_mode 后,Patroni 只会提升它确认拥有全部已报告成功提交的备库。代价是可用性:如果主库在没有任何同步备库连接时宕机,Patroni 不会提升任何节点,写入会一直中断,直到有人介入。

复制不是备份

一条 DROP TABLE 会在几毫秒内传到所有副本。请保持持续的 WAL 归档和定期的基础备份,它们能让你恢复到基础备份之后的任意时间点,并按计划测试恢复。

图

三个放有 PostgreSQL 节点的可用区。HAProxy 把订单服务的写入发给主库 pg-b,把读取发给异步副本 pg-c。pg-b 以同步方式向 pg-a、以异步方式向 pg-c 传送 WAL,在 etcd 中持有领导者键,并把 WAL 归档到对象存储。

11 个组件 · 8 个连接

在 Archboard 中打开
图表内容
  • Order service
  • HAProxy
  • AZ a
  • AZ b
  • AZ c
  • pg-a · sync standby
  • pg-b · primary
  • pg-c · async replica
  • etcd
  • WAL archive + base backups
  • SQL
  • writes :5000
  • reads :5001
  • WAL (sync)
  • WAL (async)
  • leader key, TTL 30 s
  • races if key expires
  • archived WAL
一个由 Patroni 管理、跨三个可用区的 PostgreSQL 集群:写入经 HAProxy 到达主库,同步备库用于故障转移,异步副本承担读取,领导者键保存在 etcd 中,WAL 归档用于时间点恢复。

这张图的读法如下:

  • 写入。 订单服务连接 HAProxy。5000 端口通往 pg-b,它是唯一对 /primary 返回 200 的节点。
  • 读取。 5001 端口通往 pg-c,一个位于另一个可用区的异步副本。pg-a 不承担读取。
  • 复制。 pg-b 以同步方式向 pg-a 传送 WAL(实线),以异步方式向 pg-c 传送(虚线)。
  • 故障转移。 pg-b 在 etcd 中为领导者键续期。如果键过期,同步备库 pg-a 会去争取它。pg-c 带有 nofailover 标签。
  • 恢复。 pg-b 把 WAL 和基础备份归档到对象存储。
  • 目标。 左上角的注释写明了 RPO、RTO、延迟预算和备份保留期。

复制架构图上应该标注什么

  • 每支复制箭头的模式(同步或异步),以及异步副本的延迟预算。
  • 每个副本的角色: 故障转移候选、只读副本,或两者兼有。
  • 领导者锁放在哪里以及它的 TTL,或者由哪个托管服务承担这个角色。
  • 客户端如何找到主库: 代理、DNS 名称或虚拟 IP。
  • 故障域: 用框架表示可用区,包括共识成员运行在哪里。
  • RPO 和 RTO 目标,让评审者可以据此检查设计。

托管服务替你做了什么

托管数据库回答的是同样的问题,只是有些答案是固定的。在 Amazon RDS 多可用区数据库实例中,备库是同步的,位于另一个可用区,不能承担读取。故障转移会把 DNS 记录切换到备库,通常需要 60 到 120 秒。多可用区数据库集群有两个可读的备库,使用半同步复制,故障转移通常在 35 秒以内完成。在图上,用终端节点代替 HAProxy,用 AWS 代替 Patroni 和 etcd。延迟、读取路由和目标仍然需要你自己标注。

测试故障转移

主动切换主库,例如在每个维护窗口用 patronictl switchover 切换一次。PostgreSQL 文档建议定期切换,因为这同时也是对故障转移机制的测试。测量写入失败持续了多久,确认 HAProxy 跟上了切换,并确认没有丢失任何已确认的提交。跨区域的故障转移请参阅 AWS 多区域架构图。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 为可用区添加三个基础节点包中的 框架,并在每个框架中放一个数据节点包中的 Postgres 节点:pg-a(同步备库)、pg-b(主库)和 pg-c(异步副本)。
  3. 在可用区上方,为订单服务添加一个 C4 模型节点包中的 容器,为 HAProxy 添加一个网络节点包中的 负载均衡器。
  4. 在下方,为 etcd 添加一个 C4 模型节点包中的 容器,为 WAL 归档添加一个数据节点包中的 对象存储库。
  5. 给连线加标签:“writes :5000”、“reads :5001”、实线的“WAL (sync)”、虚线的“WAL (async)”、从 pg-b 到 etcd 的“leader key, TTL 30 s”,以及一条表示领导者竞选、从 pg-a 到 etcd 的虚线。
  6. 添加一个基础节点包中的 Markdown 块,写明 RPO、RTO、延迟预算和备份保留期。

这个示例共用 11 个图表节点,在 Free 套餐的限制之内。要让图随着集群变化保持更新,请参阅不会过时的架构图;通用方法请参阅系统设计图示例。

常见问题

只读副本算备份吗? 不算。删除操作和出错的迁移同样会被复制。带 WAL 归档的备份可以让你恢复到出错之前的时间点。

同步复制就意味着零数据丢失吗? 对于同步备库已确认的提交,是的,前提是故障转移只提升这个备库。在没有任何同步备库连接时接受的提交,只存在于主库上。

为什么 etcd 要三个成员,而不是两个? etcd 需要多数成员同意才能接受写入。两个成员时任何一个都不能丢;三个成员时可以承受丢失一个,并且在网络分区时仍能在一侧保住多数。

可以用 DNS 代替代理来找到主库吗? 可以,托管服务就是这么做的。保持较短的 DNS TTL,并确保客户端不会长时间缓存解析结果;AWS 建议把 JVM 的 DNS 缓存 TTL 设为不超过 60 秒。

应该多久测试一次故障转移? 频率要足以让团队信任它,例如在每个维护窗口做一次计划内的切换,再在预发布环境中做一次计划外的故障测试。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。