PostgreSQL 复制方案(管够)

Posted DataFlow范式

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PostgreSQL 复制方案(管够)相关的知识,希望对你有一定的参考价值。

流感无情,人间有情,总会有雨过天晴的时候。希望大家都能够平平安安,一切顺利。

一些读者在公众号后台留言,希望笔者能对 PostgreSQL 的主备进行比较全面的介绍。正好春节前,笔者计划再写一篇文章,那就开始吧。

为了规范相关术语,约束如下:

  • 使用 Master 术语统一代表主数据库或主服务器

  • 使用 Standby 术语统一代表备数据库或备用服务器

数据复制

在使用 PostgreSQL 数据库的大部分应用场景中,为了获得更好的性能和扩展性,除了 Master 节点外,通常会启动更多的 PostgreSQL 实例(Standby 节点)来处理额外的负载,并从 Master 节点复制数据。在需要高可用性的情况下,这也是将数据持续地复制到 Standby 的典型解决方案,以便在 Master 崩溃时可以接管数据并提供服务能力。

事务日志

在讲解如何设置数据复制之前,我们快速看一下 PostgreSQL 如何在较低级别处理数据更改。

当 PostgreSQL 处理更改数据库中数据的命令时,它将新数据写入磁盘以使其持久化。默认情况下,PostgreSQL 数据库在磁盘上有两个写入数据的存储路径(不同操作系统存储路径会有所差异,这里以 CentOS 7.5 为参考):

  • 数据文件默认位于 /var/lib/pgsql/11/data/base 目录下面,存储数据包括表、索引、临时表和其他对象。该目录的大小受限于节点磁盘大小的限制。

  • 事务日志默认位于 /var/lib/pgsql/11/data/pg_wal 目录下面,数据文件中存储最新更改的日志。其大小受配置限制,默认情况下为 1 GB。

可能初学者会有疑问,为什么要在两个位置写入相同的数据?这似乎是多余的,但这其实是有原因的。

想象一下,假如有一个事务将 text 类型的值 test 的记录插入一个大表中,并且碰巧数据库服务在事务中间崩溃了,此时字母 te 已经写入磁盘,而其余部分丢失了。当再次启动数据库时,它将无法判断记录是否已损坏,因为它不知道该字段的值是单词 test 还是字母 te。

幸好,用户不用担心这种问题的发生,因为我们可以使用 checksum 机制来解决。那么数据库如何找到已经损坏的记录?如果每次意外重新启动后,验证整个数据库的 checksum 是非常昂贵的。

如果对传统数据库(比如 mysql/MariaDB)熟悉的话,读者应该知道相关的解决方案。解决方案就是,在将数据写入数据文件之前,PostgreSQL 始终将其先写入事务日志。事务日志(write-ahead log,也称为预写日志)是 PostgreSQL 对数据文件所做的更改的列表。事务日志显示为一组 16 MB 大小的文件(称为 WAL 文件),位于 PostgreSQL 数据库路径下的 pg_wal 子目录中。每个文件包含许多记录,这些记录表明了以哪种方式更改哪个数据文件。只有将事务日志保存在磁盘上时,数据库才会将数据写入数据文件。当事务日志已满时,PostgreSQL 将删除其最旧的段以重新使用磁盘空间,但是首先它得确保数据已经写入数据文件。

现在,假如数据库系统故障后,重新启动后将发生以下情况:

  • 如果在写入事务日志期间系统崩溃,则 PostgreSQL 会确定日志记录处于不完整状态,因为 checksum 出现不匹配。PostgreSQL 会丢弃此次事务日志记录,并对事务的写入数据执行回滚。

  • 如果在写入数据文件期间系统崩溃,但事务日志未损坏,则 PostgreSQL 将遍历事务日志,验证数据内容已写入数据文件,并在必要时更正数据文件。无需扫描所有数据文件,因为它从事务日志中知道应该更改哪个数据文件的哪一部分以及如何更改。

PostgreSQL 重放事务日志的过程称为恢复,这是数据库在意外重新启动后始终执行的操作。如果从初始化数据库服务器到当前时间都有完整的事务日志,则可以将数据库的状态恢复到过去的任何时间点。实际上,这是现实存在的情况。实际生产环境,笔者建议将 PostgreSQL 配置为在某处归档事务日志,而不是删除旧的 WAL 文件。然后,可以使用此归档文件在另一台计算机上执行数据库的基于时间点的恢复(point-in-time recovery)。

物理复制

PostgreSQL 的事务日志条目可以从一个数据库服务器(Master)获取,并应用于另一台数据库服务器(Standby)上的数据文件。在这种情况下,Standby 将具有 Master 数据库的完整的副本。传输事务日志条目并将其应用于另一台服务器的过程称为物理复制。之所以称其为物理复制,是因为事务日志恢复是在数据文件级别上起作用,并且 Standby 上的数据库副本将是 Master 的副本。

Standby 数据库可以配置为允许只读查询,在这种情况下,它称为 Hot Standby。

下面笔者介绍几种物理复制的方式。

日志传送复制

PostgreSQL 配置物理复制的一种方法是持续不断地将新的 WAL 文件从 Master 传送到 Standby,并将其应用于 Standby,以获取数据库的同步副本,这种情况称为日志传送复制。

要设置基于日志传送的复制,应采取以下措施:

  • 在 Master 上,执行以下操作:

    • 确保 WAL 文件具有足够的信息来进行复制 在 postgresql.conf 文件中,将 wal_level 配置参数设置为 replica 或 logical。

    • 通过将 archive_mode 参数设置为 on,启用 WAL 文件的归档功能。

    • 使用 archive_command 配置参数指定的事务日志归档的安全位置。

      PostgreSQL 会使用 archive_command 中的命令归档每个 WAL 文件。例如,此命令可以压缩文件并将其复制到网络存储上。如果命令为空,但是启用了 WAL 文件的归档,则这些文件将累积存储在 pg_wal 目录中。

  • 在 Standby 上,执行以下操作:

    • 首先恢复 Standby 获取的 Master 数据库的备份副本。最简单的方法是使用 pg_basebackup 工具。在 Standby 上执行如下命令:

      $ pg_pasebackup -D /var/lib/pgsql/11/data -h master -U postgres

    • 其次在数据目录中创建一个 recovery.conf 文件。此文件用于配置服务器应如何执行恢复以及它是否应作为 Standby 工作。要将服务器设置为 Standby,文件中至少应包含以下两行:

      standby_mode = on

      restore_command = 'cp /wal_archive_location/%f %p'

      注:restore_command 参数的值将取决于 WAL 归档文件的位置。这是一个 Linux 操作系统的命令,它将 WAL 文件从 WAL 归档位置复制到数据目录中事务日志的位置。

Master 和 Standby 配置完成后,就会启动两个数据库服务,Master 会将所有 WAL 文件复制到存档位置,而 Standby 将从那里获取它们,并进行重放以恢复数据。但是 Standby 上无法进行任何事务操作,可以将其配置为只读访问模式,提高查询性能。

如果 Master 数据库崩溃并且有必要切换到 Standby,则应将 Standby 升级为新的 Master 数据库,同时意味着它应停止恢复模式并允许读写事务。为此,只需删除 Standby 数据库的 recovery.conf 文件并重新启动数据库,那么 Standby 就成为了新的 Master。而这时我们应该将原先的旧 Master 重新投入使用后,使其成为 Standby 以保持集群冗余。在这种情况下,应重复上述步骤,将旧的 Master 切换为新 Master 的 Standby 数据库,切勿再将旧的 Master 作为 Master 启动,否则可能导致副本之间的差异以及最终数据丢失。

通过日志传送实现复制的好处如下:

  • 设置相对容易

  • 无需连接 Standby 和 Master

  • Master 不依赖于 Standby

  • Standby 的数量可以大于一个。实际上,Standby 还可以用作其他 Standby 的 Master,这称为级联复制。

另一方面,基于日志传送的复制还存在一些问题:

  • 必须为 WAL 归档文件提供一个网络存储位置,Master 和 Standby 都可以访问该位置,这意味着需要第三实体的介入。

  • Standby 只有在存档后才能重放 WAL 文件。仅当文件完成时才会发生这种情况,即文件大小达到 16 MB。这意味着在 Master 上执行的最新事务可能不会反映在 Standby 数据库上,尤其是在 Master 上的事务很小且不经常发生的情况下。在这种情况下,Standby 数据库可能没有最近的事务,并且其状态落后于 Master 数据库。在 Master 数据库发生故障的情况下,某些数据可能会丢失。

流复制

除了日志传送复制,还有另一种可以在日志传送之上或根本不需要日志传送的物理复制方案,即流复制。流复制需要 Standby 和 Master 之间的连接,Master 会将所有事务日志条目直接发送到 Standby。这样,Standby 将具有所有最近的更改,而无需等待 WAL 文件被归档。

要配置流复制,除了前面提到的步骤外,还应在 Master 上执行以下操作:

1. 在数据库中创建用于复制的数据库角色,如下 SQL 命令:

postgres=# CREATE USER streamer REPLICATION PASSWORD 'secret';

2. 在 pg_hba.conf 文件中配置允许该用户连接到称为 replication 名称的虚拟数据库,如下所示:

host  replication  streamer  xx.xxx.146.102/32  md5

在 Standby 上执行:

1. 在 Standby 上,将 primary_conninfo 参数添加到 recovery.conf 文件中,建立 Standby 到 Master 的连接,如下所示:

primary_conninfo = 'host=xxx.xx.146.101 port=5432 user=streamer password=secret'

如果启用并配置了 WAL 归档文件,则首先执行日志传送方案,直到归档文件中的所有 WAL 文件都应用于 Standby 数据库为止。当没有更多文件时,Standby 将连接到 Master 数据库,并开始直接从 Master 接收新的 WAL 条目。如果该连接中断或 Standby 重新启动,则该过程将再次开始。

其实可以完全不用 WAL 存档来设置流复制。默认情况下,Master 将最后 64 个 WAL 文件存储在 pg_wal 文件夹中。它可以在必要时将它们发送到 Standby 数据库。每个文件的大小为 16 MB。这意味着,只要数据文件中的更改量小于 1 GB,流复制就可以在不使用 WAL 存档的情况下在 Standby 数据库中重放这些更改。

Replication slot 功能

另外,PostgreSQL 提供了一种使 Master 识别到 Standby 处理了哪些 WAL 文件以及哪些未处理的方式。如果 Standby 数据库运行缓慢(或只是断开了连接),则 Master 数据库将不会删除尚未处理的文件,即使它们的数量大于 64,也可以通过在 Master 数据库上创建 replication slot 来完成。然后 Standby 数据库将使用 replication slot,而 Master 数据库将跟踪该 replication slot 处理了哪些 WAL 文件。

要使用 replication slot 功能,应在 Master 服务器上执行以下操作:

1. 通过在 Master 上执行 pg_create_physical_replication_slot() 来创建 replication slot:

postgres=# SELECT * FROM pg_create_physical_replication_slot('slot1');
 slot_name | lsn
-----------+-----
 slot1     | 
(1 row)

2. 确保 postgresql.conf 文件中 max_replication_slots 值足够大

在 Standby 上配置如下:

1. 在 recovery.conf 添加 primary_slot_name 参数

primary_slot_name = 'slot1'

此后,即使未连接到 Standby 数据库,Master 数据库也不会删除 Standby 数据库未接收到的 WAL 文件。当 Standby 数据库再次连接时,它将收到所有丢失的 WAL 条目,然后 Master 数据库再删除旧文件。

流复制相比日志传送复制来说,好处如下:

  • Standby 和 Master 之间的延迟较小,因为在 Master 上完成事务后立即发送 WAL 条目,而无需等待将 WAL 文件存档。

  • 完全不需要 WAL 存档就可以开启复制,因此无需为 Master 和 Standby 设置网络存储等位置。

基于流复制的同步复制

默认情况下,流复制是异步的。这意味着,如果用户在 Master 上提交事务,它将立即获得提交确认,而复制稍后时间再进行。如果 Master 在提交后立即崩溃并且尚未发送 WAL 记录,则数据会丢失,尽管用户已经看到提交成功。

在某些场景下,如果不允许任何数据丢失,那么就需要高可用性,则可以将流复制设置为同步模式。

要在 Master 上启用同步复制,需要在 postgresql.conf 文件中,将 sync_standby_names 配置参数设置为标识 Standby 的名称,例如,

synchronous_standby_names ='standby1'

然后在 Standby 数据库中,在 recovery.conf 文件中配置如下:

primary_conninfo = 'host=xxx.xx.146.101 port=5432 user=streamer password=secret application_name=standby1'

完成此操作后,Master 将等待 Standby,并在确认提交请求之前确认 Standby 已接收并处理了每条 WAL 记录。当然,这会延迟提交速度。如果 Standby 断开连接,则 Master 上的所有事务都将被阻塞,但是它不会失败。相反,它只会等到 Standby 数据库重新连接,Standby 只读查询将正常工作。

同步复制的好处在于,可以确保如果事务完成并且返回了 commit 命令,则数据将被复制到 Standby。缺点是性能开销比较大以及 Master 对 Standby 的依赖性。

以上,笔者介绍了太多的理论知识,不过,笔者并未讲解太多复杂而枯燥的知识,希望读者都能理解,不影响即将过大年的心情。

接下来,笔者带领大家进入实战环节,使用 Docker 容器部署 PostgreSQL 复制环境,实现主从架构。

实验的前置条件

首先看一下笔者为实验准备的文件,streaming_replication 目录中包含:

$ tree streaming_replication
streaming_replication
├── Dockerfile-master
├── Dockerfile-standby
├── data.sql
├── docker-compose.yml
├── master.sql
├── recovery.conf
└── schema.sql
0 directories, 7 files

其中:

1. docker-compose.yml Docker Compose 是 docker 提供的一个命令行工具,用来定义和运行由多个容器组成的应用。使用 Docker Compose,我们可以通过 YAML 文件声明式地定义应用程序的各个服务,并由单个命令完成应用的创建、启动和停止。 docker-compose.yml 文件使用了两个 Dockerfile,即启动 Master 和 Standby 数据库:

version: '2.1'
services:
  master:
    hostname: master
    build:
      context: .
      dockerfile: Dockerfile-master
    ports:
      - "15432:5432"
  standby:
    hostname: standby
    build:
      context: .
      dockerfile: Dockerfile-standby
    ports:
      - "25432:5432"
    links:
      - master:master

2. Dockerfile-master 启动 Master 数据库。

3. Dockerfile-standby 启动 Standby 数据库。

4. recovery.conf 配置 Standby 数据库连接 Master 数据库,用于恢复数据。

standby_mode = 'on'
primary_conninfo = 'host=master port=5432 user=streamer application_name=standby1'
primary_slot_name = 'slot1'
trigger_file = '/var/lib/postgresql/10/main/start'

其他文件为创建 schema、数据库、表、replication slot 等内容。

启动 Master 和 Standby 数据库

进入到 streaming_replication 目录中执行:

$ cd streaming_replication
$ docker-compose up
$ docker ps
...

执行 docker ps 可以看到启动了两个 Docker 容器,即为 Master 和 Standby 数据库,并且同步复制已启用,Master 中的数据库为空。两个 Docker 容器的日志输出将打印在 console 上。

Master 节点准备

现在我们打开另一个 console 窗口,切换目录到 streaming_replication,然后登录 Master 容器中,创建 carportal 数据库:

$ docker-compose exec master bash
root@master:/# psql -h localhost -U postgres
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
postgres=# \\i schema.sql
...
car_portal=> \\i data.sql
...
car_portal=>

Master 上已经创建好 car_portal 数据库和表,以及往创建的表中插入了数据,并且已经将数据复制到 Standby 数据库。

Standby 数据库验证

为了验证结果,我们再打开一个 console,登录 standby 数据库:

$ docker-compose exec standby bash
root@standby:/# psql -h localhost -U car_portal_app car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=> set search_path = car_portal_app;
SET
car_portal=> SELECT count(*) FROM car;
 count
-------
   229
(1 row)
car_portal=> UPDATE car set car_id = 0;
ERROR:  cannot execute UPDATE in a read-only transaction

可以看到,Standby 数据库的数据已经复制,但都是只读状态。

结束实验后,如果要停止和删除最前面启动的容器时,可以先按 Ctrl+C,然后再执行 docker-compose down,即可删除启动的 Docker 容器。

故障转移程序

物理复制的主要目的是提供高可用性,这意味着在 Master 发生故障的情况下,Standby 可以充当其角色并成为新的 Master,称为故障转移。如日志传送复制部分所述,要升级 Standby 数据库并将其设置为新的 Master 数据库,我们需要删除 recovery.conf 文件并重新启动数据库。这里需要重点强调的是,不要再使用旧的 Master 数据库,否则,新的 Master 和旧的 Master 具有不同的数据,导致数据不一致,出现数据损坏。

故障转移后,旧的 Standby 数据库已充当新的 Master 数据库,但旧的 Master 数据库已损坏,它也不会自动成为 Standby 数据库,这时就需要数据库管理员手工运维。

在许多情况下,有一种机制可以将服务器的 IP 地址自动迁移到当前的 Master 数据库,从而使应用程序不需要关心故障转移,也不需要更改配置。

PostgreSQL 本身不提供任何自动故障转移或公共的 IP 地址(VIP)功能。但是社区提供了一些解决方案,比如 Patroni。Patroni 可以管理 PostgreSQL 服务器集群,并自动执行故障转移并设置新的 Standby,感兴趣的读者可以关注官网文档。

逻辑复制

引入逻辑复制

物理数据复制有一个小的缺点,即它需要同步数据库的相同配置。简单来说,数据必须放在文件系统上的相同位置。另一件事是,数据文件中的每个更改都将被复制,即使这不会更改数据本身。例如,在 Master 节点上删除表中 dead 元组占用的存储空间时执行 VACUUM 命令或执行 CLUSTER 命令时,就会发生这种情况。

即使在表上创建了索引,发送给 Standby 数据库的也不是 CREATE INDEX 命令,而是带有索引数据的数据文件的内容。这会在网络上产生过多的负载,并可能成为高负载下系统的瓶颈。

针对物理复制存在的一些问题,接下来笔者将讲解逻辑复制,PostgreSQL 10 版本开始正式支持逻辑复制,在这之前版本,估计大部分读者都是通过触发器进行同步的。

逻辑复制不发送 SQL 命令的结果,而是命令本身,它是物理复制的一种替代方法。在这种情况下,通过网络发送的数据量可能要少得多,并且服务器也不需要完全相同。而且,服务器上的数据结构也不必相同。

例如,如果要在 Master 上执行 INSERT INTO table_a(a,b)VALUES(1,2) 之类的SQL命令,并且此命令被复制到 Standby 服务器,当 Standby 中表的列除了 a 和 b 列外,还有其他列,因为 SQL 是正确的,因此可以执行,表中多余的列使用其默认值。

Publisher 和 Subscriber 总是一对

PostgreSQL 10 版本开始支持逻辑复制,它的工作方式使同一台服务器可以从一台服务器接收一些数据并将数据发送到另一台服务器。这就是为什么在谈论逻辑复制时,没有提 Master 和 Standby 的原因,而且引入了新的概念,即 publisher(发送数据的服务器)和 subscriber(接收数据的服务器)。同一服务器可以是不同表或一组表的 publisher 和 subscriber,并且 subscriber 可以从多个 publisher 接收数据,可以参考一下消息队列的概念(消息的发布者和订阅者)。

逻辑复制在单个表或表集的级别上工作,也可以在数据库中的所有表上进行设置,并且将自动为所有新表启用此功能。但是,逻辑复制不会影响其他 schema 对象,例如序列,索引或视图。

Publisher 端配置

要配置逻辑复制,需要在 publisher 端执行以下操作:

1. 创建拥有 REPLICATION 功能的数据库角色或修改现有的用户开启 REPLICATION

postgres=# ALTER USER car_portal_app REPLICATION;

2. 配置 pg_hba.conf 访问权限

host replication car_portal_app 172.16.0.2/32 md5

3. 在 postgresql.conf 中将 wal_level 配置参数设置为 logical。这对于 PostgreSQL 在 WAL 文件中写入足够的逻辑复制信息是很有必要的。

4. 确保 max_replication_slot 配置参数的值等于或大于应该连接到此 publisher 的 subscriber 的数量。

5. 将 max_wal_senders 配置参数设置为大于或等于 max_replication_slot 参数的值。

6. 创建 publication 对象 一个 publication 对象是一组命名的表。创建之后,服务器将跟踪这些表中的数据更改,并将其发送给任何使用该 publication 的 subscriber,它使用 CREATE PUBLICATION SQL 命令创建。

下面是一个为 car_portal 数据库中的所有表创建 publication 的示例:

car_portal=> CREATE PUBLICATION car_portal FOR ALL TABLES;

Subscriber 端配置

在 subscriber 方,必须创建 subscription。一个 subscription 是一个特殊的对象,它代表与 publisher 服务器上现有 publication 的连接。为此,使用 CREATE SUBSCRIPTION 命令,如下命令所示:

car_portal=# CREATE SUBSCRIPTION car_portal CONNECTION 'dbname=car_portal host=publisher user=car_portal_app' PUBLICATION car_portal;

要复制的表在 publication 中定义,该表应在 subscriber 侧预先创建。在前面的示例中,carportal 订阅连接到 carportal publication,publication 将发布 car_portal 数据库中的所有表。因此,相同的表也应该在 subscriber 侧存在。

逻辑复制开始

一旦创建 subscription 后,复制将自动开始。首先,默认情况下,PostgreSQL 将复制整个表,然后在 publisher 发生更改后,它将异步复制更改。如果任何服务器重新启动,连接将自动重新建立。如果 subscriber 离线一段时间,则 publisher 将记住所有待处理的更改,一旦再次连接,它们将被发送给 subscriber。该功能是使用 replication slots 实现的,类似于流复制。

可以将逻辑复制视为将对已发布表执行的所有数据更改 SQL 命令都发送给 subscriber。然后,subscriber 将其命令应用于数据库中相同的表。这发生在 SQL 级别,而不是物理复制的低级别。这允许 subscriber 具有不同的数据结构。只要可以执行 SQL 命令,它就会起作用。

逻辑复制的优点和一些限制

逻辑复制不会影响序列,它也不应用任何 DDL 命令,例如 ALTER TABLE。subscriber 在应用接收到的命令时,会考虑主键,在目标表上的 UNIQUE 和 CHECK 约束,但会忽略 FOREIGN KEY 约束。

可以将逻辑复制设置为与流复制相同的方式进行同步。为此,在 publisher 上,将 postgresql.conf 的 sync_standby_names 配置参数中设置 subscriber 名称,并在 subscriber 上执行 CREATE SUBSCRIPTION 命令时在连接字符串中指定此名称。

与物理复制相比,逻辑复制具有以下优点:

  • 设置很简单

  • 非常灵活:

    • 它不需要在两个服务器上使用相同的数据库 schema,并且通常不需要在服务器上使用相同的设置

    • 同一台服务器可以同时充当 subscriber 和 publisher

    • 同一张表可以用于多个 subscription,因此它将从多个服务器收集数据

    • 可以将 publication 配置为仅复制某些类型的操作(例如 INSERT 和 DELETE,而不是 UPDATE)

  • 可以访问 subscriber 上的目标表以进行写入

  • 逻辑复制可以与 PostgreSQL 的不同主要版本一起使用

  • 它不需要任何第三方软件或硬件,并且可以在 PostgreSQL 中直接使用

  • 物理级别的数据更改(例如 VACUUM 或 CLUSTER)不会被复制

另一方面,灵活性带来了复杂性。实施逻辑复制时,应牢记以下几点:

  • 逻辑复制不考虑外键,因此它会使目标数据库进入不一致状态

  • 在 publisher 端更改 schema 时,如果 subscriber 上的 schema 不兼容,则复制可能突然中断

  • 逻辑复制仅将更改从 publisher 复制到 subscriber。如果用户直接在 subscriber 上更改数据,则复制将不会使表恢复同步

  • 只复制表,而其他 schema 对象则不能复制。当在数据库中使用基于序列的自动递增字段时,这可能是一个问题。

逻辑复制实战环节

笔者使用 Docker 容器方式来部署 PostgreSQL,实现逻辑复制架构。

实验的前置条件

首先还是来看一下前置准备的文件,位于 logical_replication 目录下:

$ tree logical_replication
logical_replication
├── Dockerfile-publisher
├── Dockerfile-subscriber
├── data.sql
├── docker-compose.yml
├── publisher.sql
├── schema.sql
└── subscriber.sql
0 directories, 7 files

其中,docker-compose.yml 文件定义了两个 Dockerfile:

version: '2.1'
services:
  publisher:
    hostname: publisher
    build:
      context: .
      dockerfile: Dockerfile-publisher
    ports:
      - "15432:5432"
  subscriber:
    hostname: subscriber
    build:
      context: .
      dockerfile: Dockerfile-subscriber
    ports:
      - "25432:5432"
  • publisher 使用的 Dockerfile 文件为 Dockerfile-publisher。

  • subscriber 使用的 Dockerfile 文件为 Dockerfile-subscriber。

Dockerfile 文件内容比较简单,笔者不做过多的补充说明。如果对 Dockerfile 不熟悉,Google 一下。

启动实验的容器

在 logical_replication 目录下运行 docker-compose 命令:

$ cd logical_replication
$ docker-compose up
$ docker ps
...

通过 docker ps 命令可以查看到已经创建 PostgreSQL 的两个实例,即为 PostgreSQL 服务的 publisher 和 subscriber。

Publisher 操作

在 publisher 上创建 car_portal 数据库,然后创建一个 PUBLICATION:

$ docker-compose exec publisher bash
root@subscriber:/# psql -h localhost -U postgres car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=# ALTER USER car_portal_app REPLICATION;
car_portal=# CREATE PUBLICATION car_portal FOR ALL TABLES;

Subscriber 操作

subscriber 也有一个数据库 car_portal,但它所有的表都是空的。现在,我们重新打开一个新的终端,通过 bash 登录 subscriber 容器中,即执行如下命令:

$ docker-compose exec subscriber bash
root@subscriber:/# psql -h localhost -U postgres car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=# CREATE SUBSCRIPTION car_portal CONNECTION 'dbname=car_portal host=publisher user=car_portal_app' PUBLICATION car_portal;

验证逻辑复制

现在,我们查询 subscriber 上的数据:

$ docker-compose exec subscriber bash
root@subscriber:/# psql -h localhost -U postgres car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=# set search_path to car_portal_app;
car_portal=# \\d
......
car_portal=# select count(*) from car_portal_app.car;
 count
-------
 229
(1 row)

carportalapp 为 car_portal 数据库下面创建的 schema。

可以看到,逻辑复制正常工作了。

Publisher 更新数据

现在,登录 publisher 器中并打开一个会话终端,启动 psql,然后将一些数据插入 publisher 的数据库中。然后再检查 subscriber 的结果。

publisher 执行更改参数,如下:

$ docker exec -it logicalreplication_publisher_1 bash
root@publisher:/# psql -h localhost -U postgres car_portal
car_portal=# set search_path to car_portal_app;
car_portal=# update car set mileage = 6775600 where car_id = 1;
car_portal=# select * from car where car_id = 1;
 car_id | number_of_owners | registration_number | manufacture_year | number_of_
doors | car_model_id | mileage
--------+------------------+---------------------+------------------+-----------
------+--------------+---------
      1 |                3 | MUWH4675            |             2008 |
    5 |           65 | 6775600
(1 row)

Subscriber 再次验证

登录 subscriber 数据库,查看结果:

$ docker-compose exec subscriber bash
root@subscriber:/# psql -h localhost -U postgres car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=# set search_path to car_portal_app;
car_portal=# select * from car where car_id = 1;
 car_id | number_of_owners | registration_number | manufacture_year | number_of_doors | car_mod
el_id | mileage
--------+------------------+---------------------+------------------+-----------------+--------
------+---------
      1 |                3 | MUWH4675            |             2008 |               5 |
   65 | 6775600
(1 row)
car_portal=#

可以看到 car 表中 car_id=1 对应的字段 mileage 的值已经被修改了。

多个 publisher 的实战

logical_replication_multi_master 目录中还有另一个示例,该示例显示同一表如何从两个不同的 publisher 那里获取更改。

如果要尝试这个示例,请打开一个终端,切换工作目录到 logical_replication_multi_master,并使用 docker-compose up 创建三个容器,包括 subscriber、 publisher_a 和 publisher-b。在两个 publisher 中已经为 car_portal.car_model 表创建了 publication 对象,但是还没有创建 publisher。

$ tree logical_replication_multi_master
logical_replication_multi_master
├── Dockerfile-publisher
├── Dockerfile-subscriber
├── data.sql
├── docker-compose.yml
├── publisher.sql
├── schema.sql
└── subscriber.sql
0 directories, 7 files
$ cd logical_replication_multi_master
$ docker-compose up
...

要查看多个 publisher 复制的工作方式,请打开另一个终端,切换到 logical_replication_multi_master 目录,在 subscriber 容器中启动 bash 会话,打开 psql(以用户 postgres 身份连接),然后导入 subscriber.sql 文件,如下所示:

$ cd logical_replication_multi_master
$ docker-compose exec subscriber bash
root@subscriber:/# psql -h localhost -U postgres car_portal
psql (11.6 (Debian 11.6-1.pgdg90+1))
...
car_portal=# \\i subscriber.sql
SET
ALTER TABLE
psql:subscriber.sql:3: NOTICE:  created replication slot "car_model_a" on publisher
CREATE SUBSCRIPTION
psql:subscriber.sql:4: NOTICE:  created replication slot "car_model_b" on publisher
CREATE SUBSCRIPTION
car_portal=#

现在,我们可以连接到两个 publisher,并尝试在 car_portal.car_model 表中插入数据,然后查询 subscriber 中的 car_portal.car_model 表,以确保复制了两个 publisher 中的记录。

多 publisher 的模拟操作

容器启动后,2 个 publisher 和 1 个 subscriber 的表 car_portal.car_model 都是空的,下面进行模拟操作:

1. publisher-a 插入 2 条数据

$ docker exec -it logicalreplicationmultimaster_publisher-a_1 bash
root@publisher-a:/# psql -h localhost -U postgres car_portal
car_portal=# set search_path to car_portal_app;
car_portal=# insert into car_model values(1,'CN','Max');
INSERT 0 1
car_portal=# insert into car_model values(2,'US','Middle');
INSERT 0 1

2. publisher-b 插入 1 条数据

$ docker exec -it logicalreplicationmultimaster_publisher-b_1 bash
root@publisher-b:/# psql -h localhost -U postgres car_portal
car_portal=# set search_path to car_portal_app;
car_portal=# insert into car_model values(3,'JP','Min');
INSERT 0 1

3. subscriber 查看数据

$ docker-compose exec subscriber bash
root@subscriber:/# psql -h localhost -U postgres car_porta
car_portal=# set search_path to car_portal_app;
car_portal=# select * from car_model;
 car_model_id | make | model
--------------+------+--------
            1 | CN   | Max
            2 | US   | Middle
            3 | JP   | Min
(3 rows)

注意:如果在其中一个 publisher 上执行了 TRUNCATE TABLE 命令,它将被复制到 subscriber 并删除整个表数据,同时还将删除从另一个 publisher 复制的数据。这就是逻辑复制为什么复制的是命令本身,而不是对数据进行的物理更改。

总结

PostgreSQL 提供了几种实现复制的方法,这些复制将维护另一台或多台服务器上的数据库中的数据副本。可以用作 Standby 解决方案,以防万一 Master 崩溃。复制还可以用于通过将负载分布在多个数据库服务器上来实现读写分离,提高系统的性能。

在某些情况下,PostgreSQL 提供的复制功能还不够。有一些适用于 PostgreSQL 的第三方解决方案,它们提供了额外的功能,例如用作连接池的 PgBouncer 或可用作负载平衡器的 Pgpool-II。还有一些基于 PostgreSQL 代码的更复杂的解决方案,它们实现了 PostgreSQL 分布式数据库解决方案,处理海量数据查询和巨大的负载,比如 Postgres-XL、Citus 和 Greenplum 等。

参考

  • 部分内容来自《Learning-PostgreSQL-11》

以上是关于PostgreSQL 复制方案(管够)的主要内容,如果未能解决你的问题,请参考以下文章

质量不够数量来凑(代码管够)

基于Docker快速搭建 PostgreSQL 高可用方案

基于Docker快速搭建 PostgreSQL 高可用方案

Postgresql数据库主从流复制

PostgreSQL物理坏块和文件损坏案例分享

PostgreSQL(丢失数据的复制或复制)-远程主PostgreSQL的只读权限