11.2 如何实施流复制

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

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

11.2.1 主从间的通信

假设备库处于同步复制模式,但配置参数hot-standby已禁用,且wal_level'archive'。主库的主要参数如下所示:

synchronous_standby_names = 'standby1'
hot_standby = off
wal_level = archive

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

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

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

  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响应后释放后端进程的锁存器,然后,后端进程完成commitabort动作。 锁存器释放的时间取决于参数synchronous_commit。如果它是'on'(默认),当接收到步骤(5)的ACK时,锁存器被释放。而当它是'remote_write'时,接收到步骤(4)的ACK时,即被释放。

如果配置参数wal_level'hot_standby''logical',则PostgreSQL会根据COMMITABORT操作的记录,写入热备功能相关的WAL记录。(在这个例子中,PostgreSQL不写那些记录,因为它是'archive'。)

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

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

walreceiver 不仅在写入和刷新WAL数据时返回ACK响应,而且还定期发送备库的心跳响应。因此,主库始终掌握所有连接备库的状态。执行如下查询,可以显示所连接备库的相关LSN信息。

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的值设置为空字符串。
    synchronous_standby_names = ''
    
  2. 使用reload选项执行pg_ctl命令。
    postgres> pg_ctl -D $PGDATA reload
    

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

11.2 流复制如何实施

流式复制有两个部分:日志传输与数据库同步。日志传输是很明显的部分,因为流复制正是基于此的 —— 每当主库发生写入时,它会向所有连接着的备库发送WAL数据。数据库同步对于同步复制而言则是必需的 —— 主库与多个备库中的每一个相互沟通,以便各自的数据库集簇保持同步。

为了准确理解流复制的工作原理,我们应当研究主库是如何管理多个备库的。为了简单起见,下面的小节将会描述一种特殊场景(即一主一从的情况),而通用的场景(一主多从)会在更后面一个小节中描述。

11.2.1 主库与同步备库之间的通信

假设备库处于同步复制模式,但参数hot_standby被配置为禁用,而wal_level被配置为archive,而主库上的主要参数如下所示:

synchronous_standby_names = 'standby1'
hot_standby = off
wal_level = archive

除了在 9.5 WAL记录的写入 中提到过的三种操作外,我们在这里主要关注事务的提交。

假设主库上一个后端进程在自动提交模式中发起了一条INSERT语句。首先,后端进程开启了一个事务,执行INSERT语句,然后立即提交。让我们深入研究一下这个提交动作是如何完成的,如下面的序列图11.2。

  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 进程WAL数据已经更新。
  6. startup 进程重放已经被写入WAL段文件中的WAL数据。
  7. walsender 在收到来自walreceiver 的ACK响应后,释放后端进程的锁存器,然后后端进程的提交或中止动作就会完成。释放锁存器的时机取决于参数synchronous_commit,其默认是on,也就是当收到步骤(5)中的确认(远端刷入)时,而当其值为remote_write时,则是在步骤(4)(远端写入)时。

如果配置参数wal_levelhot_standbylogical,PostgreSQL会按照热备功能来写WAL记录,并写入提交或终止的记录(在本例中PostgreSQL不会写这些记录,因为它被配置为archive

每一个ACK响应都会告知主库一些关于备库的信息,包含下列四个项目:

  • 最近被写入(write) 的WAL数据的LSN位置。
  • 最近被刷盘(flush) 的WAL数据的LSN位置。
  • 最近被重放(replay) 的WAL数据的LSN位置。
  • 响应发送的时间戳。
 /* XLogWalRcvSendReply(void) */
 /* src/backend/replication/walreceiver.c */
 /* 构造一条新消息 */
 reply_message.write = LogstreamResult.Write;
 reply_message.flush = LogstreamResult.Flush;
 reply_message.apply = GetXLogReplayRecPtr();
 reply_message.sendTime = now;
 /* 为消息添加消息类型,并执行发送 */
 buf[0] = 'r';
 memcpy(&buf[1], &reply_message, sizeof(StandbyReplyMessage));
 walrcv_send(buf, sizeof(StandbyReplyMessage) + 1);

walreceiver不仅仅在写入和刷盘WAL数据时返回ACK响应,也会周期性地发送ACK,作为备库的心跳。因此主库能掌控所有连接到自己的备库的状态。

在主库上执行下面的查询,可以显示所有关联的备库与LSN相关的信息。

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响应,主库也会继续并永远等待响应。导致运行中的事务无法提交,而后续的查询处理也无法开始。换而言之,主库上的所有操作实际上都停止了(流复制并不支持这种功能:通过超时将同步提交模式降级为异步提交模式)

有两种方法能避免这种情况,一种是使用多个备库,以提高系统的可用性;另一种方法是通过手动执行下列步骤,将同步提交模式改为异步提交(Asynchronous) 模式:

  1. 将参数synchronous_standby_names的值配置为空字符串
    synchronous_standby_names = ''
    
  2. 使用pg_ctl执行reload
    postgres> pg_ctl -D $PGDATA reload
    

上述过程不会影响连接着的客户端,主库会继续进行事务处理,所有客户端与后端进程之间的会话也会被保留。

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