PostgreSQL中的WAL文件与LSN深入探索和分析


前言

在16MB段大小的前提下,WAL文件的排列大概是这个样子的:

WAL文件的命名规则是什么样的?WAL中的日志序列号的编号规则又是什么样的?我们可以按照wal段大小以及PG中文件的页(块)大小分类,进行综合分析,然后得出结论。

验证与分析

我们要从wal-segsize段大小这个值开始引入这个话题,initdb下有一个蛮重要的参数:

--wal-segsize
 

Set the WAL segment size, in  megabytes. This is the size of each individual file in the WAL log. The  default size is 16 megabytes. The value must be a power of 2 between 1  and 1024 (megabytes). This option can only be set during initialization, and cannot be changed later.

这个参数值的大小,可以是从2的0次方一直到2的10次方,即从1MB到1024MB。一旦初始化完成之后,就不能再更改了。

而默认值是16MB,即2的4次方。

结合configure 编译时配置的:

--with-blocksize=BLOCKSIZE
                      set table block size in kB [8]

默认是8[kB],有多种组合,它意味着是2^^13页内地址。

默认page size为8kB下的分析

16MB wal seg下的WAL段文件命名及LSN编码规则

WAL段文件名是由24位16进制数字组成(3组*8位HEX),以使用默认的段(wal seg)大小16MB为例 ,

-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FA
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FB
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FC
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FD
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FE
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 0000000100000002000000FF
-rw------- 1 postgres postgres 16777216 Apr 16 04:21 000000010000000300000000

00000001      00000002     000000FE

分成三段,分别是时间线:'00000001';  32bit的逻辑日志号:'00000002';  32bit的日志段号:'000000FE'

我们也可以看到,上边的日志段号FE->FF然后又回到了00,但是这个时候日志号会从00000002递增到了00000003

再看看当前的LSN号与WAL文件的对应关系:

mydb=# select pg_current_wal_lsn() as lsn,  pg_walfile_name(pg_current_wal_lsn()) as filename, pg_walfile_name_offset(pg_current_wal_lsn()) as lsn_offset;
    lsn     |         filename         |             lsn_offset
------------+--------------------------+-------------------------------------
 3/3A993360 | 00000001000000030000003A | (00000001000000030000003A,10040160)
(1 row)

mydb=# insert into t select n, 'test' || n from generate_series(1, 550000) as n;                                                        INSERT 0 550000
mydb=# select pg_current_wal_lsn() as lsn,  pg_walfile_name(pg_current_wal_lsn()) as filename, pg_walfile_name_offset(pg_current_wal_lsn()) as lsn_offset;
    lsn     |         filename         |            lsn_offset
------------+--------------------------+-----------------------------------
 3/3D023F10 | 00000001000000030000003D | (00000001000000030000003D,147216)
(1 row)

看看上边的LSN:   3/3D023F10 , 它是一个64位的BIGINT,划分为4部分:

32位 8位 11位 13位
逻辑日志文件号: 00000003 wag seg号:0000003D 块号 偏移量
计算下3/3D023F10的偏移量:
mydb=# select x'023F10'::int;
  int4
--------
 147216
(1 row)

注意,这是因为这里采用的是8K的块, 所以用13位来表示块号后的偏移量。

但是这个023F10也只是个总体的偏移量,要想得到3D这个段文件里头具体哪一块,块内偏移,将这个二进制切成(11bit, 13bit)两部分,分别求值即可。

mydb=# select x'023F10';
         ?column?
--------------------------
 000000100011111100010000
(1 row)

mydb=# select B'00000010001'::int as blkno, B'1111100010000'::int as address;
 blkno | address
-------+---------
    17 |    7952
(1 row)

为什么上边段号最大是000000FF,然后重新回到00000000,这是因为一个段是16MB, 2^^24,最多总共提供2^^32的空间,只能分成2^^8 = 256个段。

64MB wal seg下的WAL段文件命名及LSN编码规则

如果采用的是 --wal-segsize=64初始化,你得到的WAL文件应该是下边这个样子的:

initdb --wal-segsize=64

并没有等到段号积到000000FF,再回到00,而是到了3F,就直接回到00,并且日志编号加1。这是因为2^^32(寻址)/2^^26 (段大小) = 2^6 = 64 = 0X3F + 1.

再来看此规则下的LSN求算:

mydb=# select pg_current_wal_lsn() as lsn,  pg_walfile_name(pg_current_wal_lsn()) as filename, pg_walfile_name_offset(pg_current_wal_lsn()) as lsn_offset;
    lsn     |         filename         |             lsn_offset
------------+--------------------------+------------------------------------
 2/144F4488 | 000000010000000200000005 | (000000010000000200000005,5194888)
(1 row)

我们看看LSN值:2/144F4488,也把它划分为4部分:

32位 6位 13位 13位
逻辑日志文件号: 00000002 000101: 即为5 B'0001001111010' B'0010010001000'

原来是8位值来表示段号的,那是16MB为一个段,现在是64MB为一个段,我们把144F4488展开成32bit:

0001 01,  00 0100 1111 0100 0100 0111 0111

后边的13位块号,以及块内偏移也是13位。

再看具体偏移:

mydb=# select (x'144F4488');
             ?column?
----------------------------------
 00010100010011110100010010001000
(1 row)

mydb=# select B'00010011110100010010001000'::int;
  int4
---------
 5194888
(1 row)

去掉最左边高位的6个bit,剩余部分,刚好就是offset:  5194888。这只是一个笼统的逻辑偏移,如果再细究,我们可以再算一下:

mydb=# select B'0001001111010'::int as blkno, B'0010010001000'::int as address;
 blkno | address
-------+---------
   634 |    1160
(1 row)

意思是说它现在位于时间线1下的日志逻辑号为2下的,第5段的blkno为634,偏移量为1160。

对此,嗯,需不需要用pg_waldump来验证一下?对目标段文件:000000010000000200000005?

[06:32:22-postgres@sean-rh3:/opt/pg/14/d1/pg_wal]$ pg_waldump 000000010000000200000005 | grep 144F4488
rmgr: Heap        len (rec/tot):     65/    65, tx:        753, lsn: 2/144F4488, prev 2/144F4450, desc: INSERT off 119 flags 0x01, blkref #0: rel 1663/16384/16386 blk 717300
rmgr: Heap        len (rec/tot):     65/    65, tx:        753, lsn: 2/144F44D0, prev 2/144F4488, descINSERT off 120 flags 0x00, blkref #0: rel 1663/16384/16386 blk 717300

我们也确实看不到blkno的信息,但是相当于是在64MB的空间里头,进行切分,第634页里头的一个偏移,这么理解就相对合理一些。

我们再看下段文件更小一些的情况。

4MB wal seg大小下的WAL段文件命名及LSN编码规则

initdb --wal-segsize=4

为了看到结果,我把wal log参数设为:

wal_keep_size = 5120 
## 让它大于4G
mydb=# insert into t select n, 'test' || n from generate_series(1, 30000000) as n;
INSERT 0 30000000
mydb=# insert into t select n, 'test' || n from generate_series(1, 30000000) as n;
INSERT 0 30000000

看看这里头的段号:是从3FF回到0的。按照寻址空间的原理:2^^32 / 2^^22 = 2^^ 10 = X'03FF' + 1

这样一下子就清楚了。

mydb=# \timing on
Timing is on.
mydb=# insert into t select n, 'test' || n from generate_series(1, 30000000) as n;
INSERT 0 30000000
Time36951.270 ms (00:36.951)

我们再看下LSN的计算,看看这个LSN:2/893B3ED8

mydb=# select pg_current_wal_lsn() as lsn,  pg_walfile_name(pg_current_wal_lsn()) as filename, pg_walfile_name_offset(pg_current_wal_lsn()) as lsn_offset;
    lsn     |         filename         |             lsn_offset
------------+--------------------------+------------------------------------
 2/893B3ED8 | 000000010000000200000224 | (000000010000000200000224,3882712)
(1 row)

Time: 0.359 ms

2/893B3ED8,  表示段号的应该是10位,而不是默认的8位。

mydb=# select X'893B3ED8';
             ?column?
----------------------------------
 1000100100 111011001 1111011011000
(1 row)
mydb=# select B'1000100100'::int;
 int4
------
  548
(1 row)

Time: 0.226 ms
mydb=# select to_hex(B'1000100100'::int);
 to_hex
--------
 224
(1 row)

mydb=# select B'111011001'::int as blkno, B'1111011011000'::int as address;
 blkno | address
-------+---------
   473 |    7896
(1 row)

得到下给的值。

2位 10位 9位 13位
逻辑日志文件号: 00000002 B'1000100100': 即为:X('224') B'111011001' 即blk: 473 B'1111011011000'即地址:7896

回过头来看看,上边的结果都是在block size (page size) = 8K下得到的结果。

如果page size = 16K,那么上边的计算方法,有一个地方是需要调整的,就是末尾的那个13位的页内偏移要改为14位(2^^14 = 16K)。其它的规则基本上是不变的。

默认page size为16kB下wal段为8MB的分析

我们先编译出一份页大小为16kB的PG,仅用于分析。

wget https://ftp.postgresql.org/pub/source/v14.7/postgresql-14.7.tar.gz
tar zxf postgresql-14.7.tar.gz
cd postgresql-14.7
su
yum install -y readline readline-devel flex bison openssl openssl-devel
 mkdir build
 cd build
 ../configure -with-extra-version=" [Sean]" --prefix=/usr/pgsql-14.7build --with-blocksize=16

make -j 4 world-bin
su -c "make install-world-bin"

设置好环境变量:

[07:17:13-postgres@sean-rh3:/opt/pg]$ cat env14build.sh
export PGROOT=/usr/pgsql-14.7build
export PGHOME=/var/lib/pgsql/14
export PGPORT=5555
export PGDATA=$PGHOME/data
export PATH=$PGROOT/bin:$PATH
export LD_LIBRARY_PATH=$PGROOT/lib:$LD_LIBRARY_PATH

我们就试一下wal段大小为8MB的结果

initdb --wal-segsize=8

为了确保wal文件被覆盖,略改下配置文件:

vi postgresql.conf

archive_mode = off
wal_keep_size = 5120

生成数据:

mydb=# \timing on
Timing is on.
mydb=# create table t(id int, col2 text);
CREATE TABLE
Time: 5.584 ms
mydb=# insert into t select n, 'test'||n from generate_series(1, 30000000) as n;
INSERT 0 30000000
Time: 30704.232 ms (00:30.704)
mydb=# insert into t select n, 'test'||n from generate_series(1, 30000000) as n;
INSERT 0 30000000
Time: 37620.341 ms (00:37.620)

2^^32 / 2^^23 = 2^^9 = X'1FF' + 1,应该是到了1FF (9bit),就会回到000的段号,同时日志号加1。

如下图:

顺道看一下data file:

mydb=# select setting || '/' || pg_relation_filepath('t') from pg_settings where name='data_directory';
                ?column?
-----------------------------------------
 /var/lib/pgsql/14/data/base/16384/16386

mydb=# \! ls -la /var/lib/pgsql/14/data/base/16384/16386*
-rw------- 1 postgres postgres 1073741824 Apr 16 08:37 /var/lib/pgsql/14/data/base/16384/16386
-rw------- 1 postgres postgres 1073741824 Apr 16 08:37 /var/lib/pgsql/14/data/base/16384/16386.1
-rw------- 1 postgres postgres  822886400 Apr 16 08:39 /var/lib/pgsql/14/data/base/16384/16386.2
-rw------- 1 postgres postgres     409600 Apr 16 08:37 /var/lib/pgsql/14/data/base/16384/16386_fsm
-rw------- 1 postgres postgres      49152 Apr 16 08:37 /var/lib/pgsql/14/data/base/16384/16386_vm

差不多1000万条记录有450M~500M的样子。

我们来分析一下LSN号:(1/3F9E448)

mydb=# select pg_current_wal_lsn() as lsn,  pg_walfile_name(pg_current_wal_lsn()) as filename, pg_walfile_name_offset(pg_current_wal_lsn()) as lsn_offset;
    lsn    |         filename         |             lsn_offset
-----------+--------------------------+------------------------------------
 1/3F9E448 | 000000010000000100000007 | (000000010000000100000007,7988296)
(1 row)

mydb=# select X'3F9E448' as offset;
            offset
------------------------------
 00111 1111001111 0010001001000
(1 row)

看一下下表:

32位 9位(段号) 9位 (块号) 14位(块内偏移)
逻辑日志文件号: 00000001 00111: 即为X'7' B'1111001111' B'0010001001000'

因为是8KB的页大小,所以页内地址应该是13位。段编号去了9位。页号应该是32 - 13 - 9 = 10位。

用PG算一算结果:

-- 7988296这个综合offset值的得来
select B'11110011110010001001000'::int as offset;
mydb=# select B'11110011110010001001000'::int as offset;
 offset
---------
 7988296
(1 row)

select B'00111'::int as segno, B'1111001111'::int as blk_no, B'0010001001000'::int as blk_address;

mydb=# select B'00111'::int as segno, B'1111001111'::int as blk_no, B'0010001001000'::int as blk_address;
 segno | blk_no | blk_address
-------+--------+-------------
     7 |    975 |        1096
(1 row)

至此,我们完整的验证完毕。

参考

https://www.postgresql.org/docs/15/app-initdb.html

请使用浏览器的分享功能分享到微信等