# 流复制

> 主备启动、WAL 传输、同步复制、多备库管理与故障检测。

---

LLMS 索引： [llms.txt](/llms.txt)

---

PostgreSQL在9.1版本中实现了流复制。它属于所谓的一主多从类型的复制，而这两个术语 —— **主（master）**和**从（slave）**，在PostgreSQL中通常分别被称为**主（primary）**和**备（standby）**。

> 译注：存储数据库副本的每个节点称为**副本（replica）**。每一次对数据库的写入操作都需要传播到所有副本上，否则副本就会包含不一样的数据。最常见的解决方案被称为**基于领导者的复制（leader-based replication）**，也称**主动/被动（active/passive）** 或 **主/从（master/slave）**复制。其中，副本之一被指定为**领导者（leader）**，也称为 **主库（master）**、**首要（primary）**。当客户端要向数据库写入时，它必须将请求发送给**领导者**，领导者会将新数据写入其本地存储。其他副本被称为**追随者（followers）**，亦称为**只读副本（read replicas）**、**从库（slaves）**、**次要（secondaries）**或**热备（hot standby）**。

这种原生复制功能是基于日志传输实现的，这是一种通用的复制技术：主库不断发送**WAL数据**，而每个备库接受WAL数据，并立即重放日志。

本章将介绍以下主题，重点介绍流复制的工作原理：

* 流复制是如何启动的
* 数据是如何在主备之间传递的
* 主库如何管理多个备库
* 主库如何检测到备库的失效


> 尽管在9.0版本中最初实现的复制功能只能进行异步复制，它很快就在9.1版中被新的实现（如今采用的）所替代，可以支持同步复制。


## 11.1 流复制的启动

在流复制中，有三种进程协同工作。首先，主库上的**walsender（WAL发送器）**进程将WAL数据发送到备库；同时，备库上的**walreceiver（WAL接收器）**在接收这些数据，而备库上的**startup**进程可以重放这些数据。 其中**walsender**和**walreceiver** 之间使用单条TCP连接进行通信。

在本节中，我们将探讨流复制的启动顺序，以了解这些进程如何启动并且它们之间是如何建立连接的。[图11.1](#fig-11.1)显示了流复制的启动顺序图：

**图 11.1.** 流复制的启动顺序

![图11\.1 流复制的启动顺序](/img/fig-11-01.png)
1. 启动主库服务器和备库服务器。
2. 备库服务器启动一个**startup**进程。
3. 备库服务器启动一个**walreceiver**进程。
4. **walreceiver**向主库服务器发送连接请求。如果主库尚未启动，**walreceiver**会定期重发该请求。
5. 当主库服务器收到连接请求时，将启动**walsender**进程，并建立**walsender**和**walreceiver**之间的TCP连接。
6. **walreceiver**发送备库数据库集簇上最新的LSN。在IT领域中通常将该阶段称作**握手（handshaking）**。
7. 如果备库最新的LSN小于主库最新的LSN（备库的LSN < 主库的LSN），则**walsender**会将前一个LSN到后一个LSN之间的WAL数据发送到**walreceiver**。这些WAL数据由存储在主库`pg_xlog`子目录（版本号为10+的更名为`pg_wal`）中的WAL段提供。最终，备库重放接收到的WAL数据。在这一阶段，备库在追赶主库，因此被称为 **追赶（catch-up）** 阶段。
8. 最终，流复制开始工作。

每个**walsender**进程都维护了连接上的**walreceiver**或其他应用程序的**复制进度状态**（请注意，不是连接到**walsender**的**walreceiver**或应用程序的本身的状态）。如下是其可能的状态：
* **启动（start-up）** —— 从启动**walsender**到握手结束。如[图11.1](#fig-11.1)(5)-(6)。
* **追赶（catch-up）** —— 处于追赶期间，如[图11.1](#fig-11.1)(7)。
* **流复制（streaming）**—— 正在运行流复制。如[图11.1](#fig-11.1)(8)。
* **备份（backup）**—— 处于向`pg_basebackup`等备份工具发送整个数据库集簇文件的过程中。

系统视图`pg_stat_replication`显示了所有正在运行的**walsenders**的状态，如下例所示：

```bash
testdb=# SELECT application_name,state FROM pg_stat_replication;
 application_name |   state
------------------+-----------
 standby1         | streaming
 standby2         | streaming
 pg_basebackup    | backup
(3 rows)
```

如上结果所示，有两个**walsender**正在运行，其正在向连接的备库发送WAL数据，另一个**walsender**在向`pg_basebackup`应用发送所有数据库集簇中的文件。



> ### 在备库长时间停机后，如果重启会发生什么？
>
> 在9.3版及更早版本中，如果备库所需的WAL段在主库上已经被回收了，备库就无法追上主库了。这一问题并没有可靠的解决方案，只能为参数 [`wal_keep_segments`](https://www.postgresql.org/docs/11/runtime-config-replication.html#GUC-WAL-KEEP-SEGMENTS)配置一个较大的值，以减少这种情况发生的可能性，但这只是权宜之计。
>
> 在9.4及后续版本中，可以使用 **复制槽（replication slot）** 来预防此问题发生。复制槽是一项提高WAL数据发送灵活性的功能，主要是为 **逻辑复制（logical replication）** 而提出的，同时也能解决这类问题——复制槽通过暂停回收过程，从而保留`pg_xlog`（10及后续版本的`pg_wal`）中含有未发送数据的WAL段文件，详情请参阅[官方文档](https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS)。
>



## 11.2 如何实施流复制

流复制有两个方面：日志传输和数据库同步。因为流复制基于日志，日志传送显然是其中的一个方面 —— 主库会在写入日志记录时，将WAL数据发送到连接的备库。同步复制中需要数据库同步 —— 主库与多个备库通信，从而同步整个数据库集簇。

为准确理解流复制的工作原理，我们应该探究下主库如何管理多个备库。为了尽可能简化问题，本节描述了一个特例（即单主单备系统），而下一节将描述一般情况（单主多备系统）。

### 11.2.1 主从间的通信

假设备库处于同步复制模式，但配置参数`hot_standby`已禁用，且`wal_level`为`'archive'`。主库的主要参数如下所示：

```bash
synchronous_standby_names = 'standby1'
hot_standby = off
wal_level = archive
```

另外，在9.5节中提到，有三个情况触发写WAL数据，这里我们只关注事务提交。

假设主库上的一个后端进程在自动提交模式下发出一个简单的`INSERT`语句。后端启动事务，发出`INSERT`语句，然后立即提交事务。让我们进一步探讨此提交操作如何完成的。如[图11.2](#fig-11.2)中的序列图：

**图 11.2.** 流复制的通信序列图

![图11\.2 流复制的通信序列图](/img/fig-11-02.png)

1. 后端进程通过执行函数`XLogInsert()`和`XLogFlush()`，将WAL数据写入并刷新到WAL段文件中。
2. **walsender**进程将写入WAL段文件的WAL数据发送到**walreceiver**进程。
3. 在发送WAL数据之后，后端进程继续等待来自备库的ACK响应。更确切地说，后端进程通过执行内部函数`SyncRepWaitForLSN()`来获取**锁存器**（latch），并等待它被释放。
4. 备库上的**walreceiver**通过`write()`系统调用，将接收到的WAL数据写入备库的WAL段，并向**walsender**返回ACK响应。
5. **walreceiver**通过系统调用（例如`fsync()`）将WAL数据刷新到WAL段中，向**walsender**返回另一个ACK响应，并通知 **启动进程（startup process ）** 相关WAL数据的更新。
6. 启动进程重放已写入WAL段的WAL数据。
7. **walsender**在收到来自**walreceiver**的ACK响应后释放后端进程的锁存器，然后，后端进程完成`commit`或`abort`动作。 锁存器释放的时间取决于参数`synchronous_commit`。如果它是`'on'`（默认），当接收到步骤（5）的ACK时，锁存器被释放。而当它是`'remote_write'`时，接收到步骤（4）的ACK时，即被释放。

> 如果配置参数`wal_level`是`'hot_standby'`或`'logical'`，则PostgreSQL会根据`COMMIT`或`ABORT`操作的记录，写入热备功能相关的WAL记录。（在这个例子中，PostgreSQL不写那些记录，因为它是`'archive'`。）
>


每个ACK响应将备库的内部信息通知给主库。包含以下四个项目：

* 已写入最新WAL数据的LSN位置。
* 已刷新最新WAL数据的LSN位置。
* 启动进程已经重放最新的WAL数据的LSN。
* 发送此响应的时间戳。

**walreceiver**不仅在写入和刷新WAL数据时返回ACK响应，而且还定期发送备库的心跳响应。因此，主库始终掌握所有连接备库的状态。

执行如下查询，可以显示所连接备库的相关LSN信息。

```bash
testdb=# SELECT application_name AS host,
        write_location AS write_LSN, flush_location AS flush_LSN, 
        replay_location AS replay_LSN FROM pg_stat_replication;

   host   | write_lsn | flush_lsn | replay_lsn 
----------+-----------+-----------+------------
 standby1 | 0/5000280 | 0/5000280 | 0/5000280
 standby2 | 0/5000280 | 0/5000280 | 0/5000280
(2 rows)
```

> 心跳的间隔设置为参数`wal_receiver_status_interval`，默认为10秒。



### 11.2.2 发生故障时的行为

在本小节中，将介绍在同步备库发生故障时，主库的行为方式，以及主库会如何处理该情况。

即使同步备库发生故障，且不再能够返回ACK响应，主库也会继续等待响应。因此，正在运行的事务无法提交，而后续查询也无法启动。换而言之，实际上主库的所有操作都已停止（流复制不支持发生超时时自动降级回滚到异步模式的功能）。

有两种方法可以避免这种情况。其中之一是使用多个备库来提高系统可用性，另一个是通过手动执行以下步骤从同步模式切换到异步模式。

1. 将参数`synchronous_standby_names`的值设置为空字符串。

   ```ini
   synchronous_standby_names = ''
   ```

2. 使用`reload`选项执行`pg_ctl`命令。

   ```bash
   postgres> pg_ctl -D $PGDATA reload
   ```

上述过程不会影响连接的客户端。主库继续事务处理，以及会保持客户端与相应的后端进程之间的所有会话。



## 11.3 管理多个备库

本节描述了存在多个备库时，流复制是如何工作的。

### 11.3.1 同步优先级与同步状态

主库为自己管理的每一个备库指定一个**同步优先级（`sync_priority`）** 与 **同步状态（`sync_state`）**。（上一节并没有提到这一点，即使主库只管理一个备库，也会指定这些值。）

**同步优先级（sync_priority）** 表示备库在同步模式下的优先级，它是一个固定值。较小的值表示较高的优先级，而`0`是一个特殊值，表示“异步模式”。备库优先级是一个有序列表，在主库配置参数 `synchronous_standby_names`中依序给出。例如在以下配置中，`standby1`和`standby2`的优先级分别为1和2。

```bash
synchronous_standby_names = 'standby1, standby2'
```

（未列于此参数中的备库处于异步模式，优先级为0）

**同步状态（sync_state）** 是备库的状态，它因所有在列备库的运行状态及其优先级而异，以下是可能的状态：

* **同步（Sync）** 状态的备库，是所有正在工作中的备库中，具有最高优先级的同步备库的状态（异步模式除外）。
* **潜在（Potential）** 状态的备库，是所有工作备库（异步备库除外）中，优先级等于或低于2的闲置同步备库。如果同步备库失效，潜在备库中有着最高优先级的那个将替换为同步备库。
* **异步（Async）** 状态的备库是固定的。主库以与潜在备库相同的方式处理异步备库，只是它们的`sync_state`永远不会是`sync`或`potential`。

执行以下查询来显示备库的优先级和状态：

```sql
testdb=# SELECT application_name AS host, 
         sync_priority, sync_state FROM pg_stat_replication;
   host   | sync_priority | sync_state
----------+---------------+------------
 standby1 |             1 | sync
 standby2 |             2 | potential
(2 rows)
```

> 最近有几个开发者尝试实现“多个同步备库”。详情参见[此处](https://commitfest.postgresql.org/6/293/)。

### 11.3.2 主库如何管理多个备库

主库仅等待来自同步备库的ACK响应。换句话说，主库仅确保同步备库写入并刷新WAL数据。因此在流复制中，只有同步备库的状态是与主库始终一致且同步的。

[图11.3](#fig-11.3)展示了潜在备库的ACK响应早于首要备库ACK响应的情况。这时主库并不会完成当前事务的`COMMIT`操作，而是继续等待首要备库的ACK响应。而当收到首要备库的响应时，后端进程释放锁存器并完成当前事务的处理。

**图 11.3.** 管理多个备库

![图11\.3 管理多个备库](/img/fig-11-03.png)

> 备库1与备库2的同步状态分别为`sync`和`potential`。
>
> 1. 尽管从潜在备库接收到ACK响应，但主库的后端进程会继续等待来自同步备库的ACK响应。
> 2. 主库的后端进程释放锁存器，完成当前的事务处理。

在相反的情况下（即首要从库的ACK响应返回早于潜在从库的响应），主库会立即完成当前事务的`COMMIT`操作，而不会去确认潜在从库是否已经写入和刷盘了WAL数据。



### 11.3.3 发生故障时的行为

我们再来看看当从库发生故障时主库的表现。

当潜在或异步备库发生故障时，主库会终止连接到故障备库的**walsender**进程，并继续进行所有处理。换而言之，主库上的事务处理不会受到这两种备库的影响。

当同步备库发生故障时，主库将终止连接到故障备库的**walsender**进程，并使用具有最高优先级的潜在备库替换首要同步备库，如[图11.4](#fig-11.4)。与上述的故障相反，主库将会暂停从失效点到成功替换同步备库之间的查询处理。（因此备库的故障检测对于提高复制系统可用性至关重要，故障检测将在下一节介绍）

**图 11.4.** 更换同步备库

![图11\.4 更换同步备库](/img/fig-11-04.png)

在任何情况下，如果一个或多个备库在同步模式下运行，主库始终只会保留一个同步备库，而同步备库始终与主库保持一致且同步的状态。



## 11.4 备库的故障检测

流复制使用两种常见的故障检测过程，不需要任何特别的硬件。

1. 备库服务器的失效检测

   当检测到**walsender**和**walreceiver**之间的连接断开时，主库**立即**判定备库或**walreceiver**进程出现故障。当底层网络函数由于未能成功读写**walreceiver**的套接字接口而返回错误时，主库也会立即判定其失效。

2. 硬件与网络的失效检测

   如果**walreceiver**在参数`wal_sender_timeout`（默认为60秒）配置的时间段内没有返回任何结果，则主库会判定备库出现故障。相对于上面的故障而言，尽管从库可能因为一些失效原因（例如备库上的硬件失效，网络失效等），已经无法发送任何响应，但主库仍需要耗费特定的时间 —— 最大为`wal_sender_timeout`，来确认备库的死亡。

取决于失效的类型，一些失效可以在失效发生时被立即检测到，而有时候则可能在出现失效与检测到失效之间存在一段时间延迟。如果在同步从库上出现后一种失效，那么即使有多个潜在备库正常工作，直到检测到同步备库失效了，主库仍然可能会停止一段时间的事务处理。

> 在9.2或更早版本中，参数`wal_sender_timeout`被称为`replication_timeout`。
