PostgreSQL中的WAL:2.预记录日志

上次,我们遇到了共享内存的重要对象之一,即缓冲区缓存的设备。 从RAM中丢失信息的可能性是需要从故障中恢复的主要原因。 今天我们将讨论这些工具。

杂志杂志


las,奇迹不会发生:为了避免RAM中信息丢失,必须将所有必要的内容及时写入磁盘(或其他非易失性设备)。

因此,这就是所做的。 除数据更改外,还将保留这些更改的日志 。 当我们更改缓冲区缓存中页面上的某些内容时,我们会在日志中创建有关此更改的记录。 该记录包含足够的最少信息,以便在必要时可以重复进行更改。

为此,日记条目必须修改后的页面到达磁盘之前必须进入磁盘。 因此,名称为:预写日志。

如果发生故障,则磁盘上的数据处于不一致状态:某些页面写得较早,有些页面写得较晚。 但是,仍然存在可以由故障之前已经完成但其结果未到达磁盘的那些操作可以读取和重新执行的日志。

为什么不强制将数据页本身写入磁盘,为什么要执行双重作业呢? 事实证明如此有效。
首先,日志是要写入的顺序数据流。 甚至HDD在顺序记录方面也做得很好。 但是数据本身的记录是随机的,因为页面或多或少地随机分布在磁盘上。
其次,日记帐分录可以比页面小得多。
第三,在记录时,您不必担心确保磁盘上的数据在任何时间点都保持一致(此要求极大地延长了使用寿命)。
第四,正如我们稍后将要看到的,该日志(因为它存在)不仅可以用于恢复,还可以用于备份和复制。

您需要记录所有操作,在此期间,如果发生故障,磁盘上可能会出现不一致的风险。 特别是,将记录以下操作:

  • 更改缓冲区高速缓存中的页面(通常是表和索引页面)-因为更改后的页面不会立即进入磁盘;
  • 提交和取消事务-状态更改发生在XACT缓冲区中,并且不会立即到达磁盘;
  • 文件操作(创建和删除文件和目录,例如,在创建表时创建文件)-因为这些操作必须与数据更改同时进行。

未记录:

  • 使用非日志化(未记录)表的操作-它们的名称不言而喻;
  • 临时表的操作-没有意义,因为此类表的生存期不超过创建它们的会话的生存期。

在PostgreSQL 10之前,没有记录哈希索引 (它们仅用于将哈希函数映射到不同的数据类型),但现在已修复。

逻辑设备




从逻辑上讲,日记可以看作是各种长度的记录序列。 每个记录都包含有关特定操作的数据 ,并在其后带有标准标头 。 标题除其他外指示:

  • 记录所属的事务号。
  • 资源管理器-负责记录的系统组件;
  • 校验和(CRC)-允许您确定数据损坏;
  • 记录长度并链接到上一条记录。

数据本身具有不同的格式和含义。 例如,它们可以表示页面的某些片段,需要以一定的偏移量覆盖页面内容。 指定的资源管理器“了解”如何解释其记录中的数据。 对于表,每种索引类型,事务状态等,都有单独的表管理器。如果命令需要,可以获取它们的完整列表。

pg_waldump -r list 

物理设备


在磁盘上,日志作为文件存储在$ PGDATA / pg_wal目录中。 每个文件默认为16 MB。 可以增加大小以避免在一个目录中出现大量文件。 在PostgreSQL 11之前,只能在编译源代码时完成此操作,但是现在您可以在初始化集群时指定大小( --wal-segsize )。

日志条目属于当前使用的文件; 当它结束时,开始使用下一个。

在服务器的共享内存中为日志分配了特殊的缓冲区。 日志高速缓存的大小由wal_buffers参数设置(默认值表示自动配置:已分配缓冲区高速缓存的1/32)。

日志高速缓存的排列方式类似于缓冲区高速缓存,但是它主要在环形缓冲区模式下工作:将条目添加到“头”,然后从“尾部”写入磁盘。

记录(“尾部”)和插入(“头”)位置分别显示函数pg_current_wal_lsn和pg_current_wal_insert lsn:

 => SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn(); 
  pg_current_wal_lsn | pg_current_wal_insert_lsn --------------------+--------------------------- 0/331E4E64 | 0/331E4EA0 (1 row) 

为了引用特定记录,使用了数据类型pg_lsn(LSN =日志序列号)-这是一个64位数字,表示相对于日志开头的记录前的字节偏移。 LSN作为两个32位数字以十六进制表示形式输出。

您可以找出我们将在哪个文件中找到所需的位置以及与文件开头的偏移量:

 => SELECT file_name, upper(to_hex(file_offset)) file_offset FROM pg_walfile_name_offset('0/331E4E64'); 
  file_name | file_offset --------------------------+------------- 000000010000000000000033 | 1E4E64 \ /\ /  0/331E4E64  

文件名由两部分组成。 高8位十六进制数字显示时间分支的编号(在从备份还原时使用),其余部分对应于最高LSN数字(其余的较低LSN数字指示偏移量)。

可以在$ PGDATA / pg_wal /目录中的文件系统上查看日志文件,但是从PostgreSQL 10开始,还可以使用特殊功能来查看日志文件:

 => SELECT * FROM pg_ls_waldir() WHERE name = '000000010000000000000033'; 
  name | size | modification --------------------------+----------+------------------------ 000000010000000000000033 | 16777216 | 2019-07-08 20:24:13+03 (1 row) 

转发写


让我们看看日志如何发生以及如何提供主动记录。 创建一个表:

 => CREATE TABLE wal(id integer); => INSERT INTO wal VALUES (1); 

我们将看一下表格页面的标题。 为此,我们需要一个已经熟悉的扩展:

 => CREATE EXTENSION pageinspect; 

让我们开始事务并记住日志中的插入位置:

 => BEGIN; => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F377C (1 row) 

现在让我们做一些操作,例如,更新该行:

 => UPDATE wal set id = id + 1; 

此更改已记录在日志中,插入位置已更改:

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F37C4 (1 row) 

为了确保修改后的数据页不早于日志条目被推入磁盘,与该页面相关的最后一个日志条目的LSN存储在页眉中:

 => SELECT lsn FROM page_header(get_raw_page('wal',0)); 
  lsn ------------ 0/331F37C4 (1 row) 

请记住,该日志对于整个集群都是通用的,并且新条目始终都在其中。 因此,页面上的LSN可能小于pg_current_wal_insert_lsn函数刚刚返回的值。 但是在我们的系统中什么也没发生,所以数字是相同的。

现在完成交易。

 => COMMIT; 

提交记录也进入日志,并且位置再次更改:

 => SELECT pg_current_wal_insert_lsn(); 
  pg_current_wal_insert_lsn --------------------------- 0/331F37E8 (1 row) 

提交在称为XACT的结构中更改了事务的状态(我们已经讨论过 )。 状态存储在文件中,但是它们也使用自己的缓存,该缓存在共享内存中占据128页。 因此,对于XACT页面,必须跟踪最后一个日记条目的LSN。 但是此信息不是存储在页面本身中,而是存储在RAM中。

在某个时候,创建的日记帐分录将被写入磁盘。 在哪种情况下-我们将再谈一次,但是在我们的情况下,这已经发生:

 => SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn(); 
  pg_current_wal_lsn | pg_current_wal_insert_lsn --------------------+--------------------------- 0/331F37E8 | 0/331F37E8 (1 row) 

此后,可以将数据和XACT页面推出缓存。 但是,如果需要将它们更早地强制退出,则将对其进行检测,并且将强制首先记录日记帐分录。

知道了两个LSN位置,您可以通过简单地从另一个位置减去一个位置来获得它们之间的日记条目的大小(以字节为单位)。 您只需要将职位转换为pg_lsn类型:

 => SELECT '0/331F37E8'::pg_lsn - '0/331F377C'::pg_lsn; 
  ?column? ---------- 108 (1 row) 

在这种情况下,行更新和提交需要日志中的108个字节。

以相同的方式,您可以估计在特定负载下服务器每单位时间生成多少日记条目。 这是安装过程中将需要的重要信息(我们将在下一次讨论)。

现在,我们将使用pg_waldump实用程序来查看创建的日志条目。

该实用程序可以使用LSN范围(如本例中所示),并选择指定事务的记录。 它应该代表postgres OS用户运行,因为她需要访问磁盘上的日志文件。

 postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/331F377C -e 0/331F37E8 000000010000000000000033 
 rmgr: Heap len (rec/tot): 69/ 69, tx: 101085, lsn: 0/331F377C, prev 0/331F3014, desc: HOT_UPDATE off 1 xmax 101085 ; new off 2 xmax 0, blkref #0: rel 1663/16386/33081 blk 0 
 rmgr: Transaction len (rec/tot): 34/ 34, tx: 101085, lsn: 0/331F37C4, prev 0/331F377C, desc: COMMIT 2019-07-08 20:24:13.945435 MSK 

在这里,我们看到两个条目的标题。

第一个是HOT_UPDATE操作,与堆资源管理器有关。 文件名和页码显示在blkref字段中,并与更新的表格页面匹配:

 => SELECT pg_relation_filepath('wal'); 
  pg_relation_filepath ---------------------- base/16386/33081 (1 row) 

第二项是COMMIT,与事务资源管理器有关。

不是最易读的格式,但是如有必要,您可以找出来。

恢复


当我们启动服务器时,postmaster进程首先启动,然后依次启动启动过程,其任务是确保在发生故障时进行恢复。

为了确定是否需要恢复,启动将查看特殊控制文件$ PGDATA / global / pg_control并查看集群状态。 我们可以使用pg_controldata实用程序自己检查状态:

 postgres$ /usr/lib/postgresql/11/bin/pg_controldata -D /var/lib/postgresql/11/main | grep state 
 Database cluster state: in production 

干净停止的服务器将处于“关闭”状态。 如果服务器不工作,并且状态仍为“生产中”,则意味着DBMS已下降,然后将自动执行恢复。

为了恢复,启动过程将顺序读取日志,并在必要时将条目应用于页面。 您可以通过将磁盘上页面的LSN与日记条目的LSN进行比较来验证需求。 如果页面的LSN较大,则不需要记录。 但是实际上-甚至不可能,因为记录是为严格一致的应用而设计的。

也有例外。 某些记录形成为整页图像(FPI,整页图像),很明显,此类图像可以在任何状态下应用于页面-仍会擦除那里的所有内容。 事务状态的另一种更改可以应用于XACT页面的任何版本-因此,在此类页面内无需存储LSN。

像在正常工作期间一样,在恢复过程中更改页会在缓冲区缓存中发生-为此邮局局长启动必要的后台进程。

同样,日记帐分录也适用于文件:例如,如果一条记录表明该文件必须存在,但不存在,则创建该文件。

好了,在恢复过程的最后,所有未分类的表都被其初始层中的 “虚拟”覆盖。

这是该算法的非常简化的表示。 特别是,我们对于从何处开始阅读日记帐条目一无所知(必须将对话推迟到考虑检查点之前)。

最后的澄清。 “经典”恢复过程包括两个阶段。 在第一个阶段(前滚)中,将对日记帐分录进行滚动,并且服务器重复失败期间丢失的所有工作。 在第二个(回滚)上,故障时未提交的事务将回滚。 但是PostgreSQL不需要第二阶段。 如前所述 ,由于实现了多版本事务的特殊性,因此无需进行物理回滚。 仅在XACT中不设置固定位就足够了。

待续

Source: https://habr.com/ru/post/zh-CN459250/


All Articles