ITPub博客

首页 > 数据库 > 数据库开发技术 > 从一起等待事件说起

从一起等待事件说起

原创 数据库开发技术 作者:u_a9af4a561c3b 时间:2022-02-08 19:28:22 0 删除 编辑

前言

今天,白鳝老师问了一个问题,关于LogicalRewriteCheckpointSync等待事件的

乍一看有点陌生,遂翻了一下官网,但是遗憾的是,官网的介绍不知所云,对于新手以及我这样的半熟油条,实在忍不住吐槽,着实费力!

Item Description
LogicalRewriteCheckpointSync Waiting for logical rewrite mappings to reach durable storage during a checkpoint.
LogicalRewriteMappingSync Waiting for mapping data to reach durable storage during a logical rewrite.
LogicalRewriteMappingWrite Waiting for a write of mapping data during a logical rewrite.
LogicalRewriteSync Waiting for logical rewrite mappings to reach durable storage.
LogicalRewriteTruncate Waiting for truncate of mapping data during a logical rewrite.
LogicalRewriteWrite Waiting for a write of logical rewrite mappings.

中文手册翻译,依旧没看懂

Item Description
LogicalRewriteCheckpointSync 在一个检查点期间等待逻辑重写映射到达稳定存储。
LogicalRewriteMappingSync 在一次逻辑重写期间等待映射数据到达稳定存储。
LogicalRewriteMappingWrite 在一次逻辑重写期间等待对映射数据的写操作。
LogicalRewriteSync 正等待逻辑重写映射到达稳定存储。
LogicalRewriteWrite 正等待对逻辑重写映射的写操作。

搞不定的时候,只能看源码了

分析

在说明里面,可以看到和checkpoint有关,checkpoint我们知道,最核心的函数是CheckPointGuts()

/*
 * Flush all data in shared memory to disk, and fsync
 *
 * This is the common code shared between regular checkpoints and
 * recovery restartpoints.
 */

static void
CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
{
    CheckPointRelationMap();
    CheckPointReplicationSlots();
    CheckPointSnapBuild();
    CheckPointLogicalRewriteHeap();
    CheckPointReplicationOrigin();

    /* Write out all dirty data in SLRUs and the main buffer pool */
    TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
    CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
    CheckPointCLOG();
    CheckPointCommitTs();
    CheckPointSUBTRANS();
    CheckPointMultiXact();
    CheckPointPredicate();
    CheckPointBuffers(flags);

    /* Perform all queued up fsyncs */
    TRACE_POSTGRESQL_BUFFER_CHECKPOINT_SYNC_START();
    CheckpointStats.ckpt_sync_t = GetCurrentTimestamp();
    ProcessSyncRequests();
    CheckpointStats.ckpt_sync_end_t = GetCurrentTimestamp();
    TRACE_POSTGRESQL_BUFFER_CHECKPOINT_DONE();

    /* We deliberately delay 2PC checkpointing as long as possible */
    CheckPointTwoPhase(checkPointRedo);
}

CheckPointGuts()里面调用了CheckPointLogicalRewriteHeap()这个函数

/* ---
 * Perform a checkpoint for logical rewrite mappings
 *
 * This serves two tasks:
 * 1) Remove all mappings not needed anymore based on the logical restart LSN
 * 2) Flush all remaining mappings to disk, so that replay after a checkpoint
 *      only has to deal with the parts of a mapping that have been written out
 *      after the checkpoint started.
 * ---
 */

void
CheckPointLogicalRewriteHeap(void)
{
    XLogRecPtr    cutoff;
    XLogRecPtr    redo;
    DIR           *mappings_dir;
    struct dirent *mapping_de;
    char        path[MAXPGPATH + 20];

    /*
     * We start of with a minimum of the last redo pointer. No new decoding
     * slot will start before that, so that's a safe upper bound for removal.
     */

    redo = GetRedoRecPtr();

    /* now check for the restart ptrs from existing slots */
    cutoff = ReplicationSlotsComputeLogicalRestartLSN();

因为逻辑复制相关的脏数据,会写入到pg_logical目录下面的mappings和snapshots子目录下

[postgres@xiongcc ~]$ ll pgdata/pg_logical/
total 96
drwx------ 2 postgres postgres  4096 Feb  8 18:28 mappings
-rw------- 1 postgres postgres     8 Feb  8 18:28 replorigin_checkpoint
drwx------ 2 postgres postgres 90112 Feb  8 18:28 snapshots

所以,这个等待事件也就呼之欲出了,等待checkpoint将逻辑复制相关的脏数据刷到存储上

过了一会,白老师又问了我一个问题,出了DDL之外,DML会修改吗?

这不又触及到知识盲区了,在pg_logical目录,我们知道是用于配置逻辑复制时,存储逻辑解码的数据状态

[postgres@xiongcc ~]$ ll pgdata/pg_logical/
total 96
drwx------ 2 postgres postgres  4096 Feb  8 18:28 mappings
-rw------- 1 postgres postgres     8 Feb  8 18:28 replorigin_checkpoint
drwx------ 2 postgres postgres 90112 Feb  8 18:28 snapshots

最开始里面什么都没有

[postgres@xiongcc ~]$ ll pgdata/pg_logical/snapshots/
total 0
[postgres@xiongcc ~]$ ll pgdata/pg_logical/mappings/
total 0

创建一个逻辑复制,发布端

postgres=# create table test(id int,info text);
CREATE TABLE
postgres=# create publication pub1 for table test;
CREATE PUBLICATION

订阅端

postgres=# create table test(id int,info text);
CREATE TABLE
postgres=# create subscription test_sub connection 'host=localhost port=5432' publication pub1;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION

发布端查看,OK,创建没什么问题

postgres=# select slot_name,plugin,slot_type,active,active_pid from pg_replication_slots ;
 slot_name |  plugin  | slot_type | active | active_pid 
-----------+----------+-----------+--------+------------
 test_sub  | pgoutput | logical   | t      |      11603
(1 row)

这里面有一个注意点,在刚刚创建完之后,有些人可能会看到有两个复制槽

postgres=# truncate table test;
TRUNCATE TABLE
postgres=# insert into test select n,'test' from generate_series(1,10000000) as n;
INSERT 0 10000000

订阅端重新订阅一下

postgres=# drop subscription test_sub ;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION
postgres=# truncate table test;
TRUNCATE TABLE
postgres=# create subscription test_sub connection 'host=localhost port=5432' publication pub1;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION

再次查看复制槽,便会看到有两个复制槽,虽然订阅端仅打印了一条 NOTICE: created replication slot "test_sub" on publisher,另外一个复制槽名为pg_33777_sync_33758_7038607583722269381

-[ RECORD 1 ]-------+----------------------------------------
slot_name           | test_sub
plugin              | pgoutput
slot_type           | logical
datoid              | 13890
database            | postgres
temporary           | f
active              | t
active_pid          | 12382
xmin                | 
catalog_xmin        | 1212624
restart_lsn         | 7/DD783D70
confirmed_flush_lsn | 7/DD783DA8
wal_status          | reserved
safe_wal_size       | 
two_phase           | f
-[ RECORD 2 ]-------+----------------------------------------
slot_name           | pg_33777_sync_33758_7038607583722269381
plugin              | pgoutput
slot_type           | logical
datoid              | 13890
database            | postgres
temporary           | f
active              | f
active_pid          | 
xmin                | 
catalog_xmin        | 1212624
restart_lsn         | 7/DD783D70
confirmed_flush_lsn | 7/DD783DA8
wal_status          | reserved
safe_wal_size       | 
two_phase           | f

这个其实是14版本做的增强,如果原始表的记录数特别多, 初始数据同步时间比较长, 在这个过程中可能产生了很多新事务, 会带来一系列问题:

1.We have to perform the entire copy operation again if there is any error (network breakdown, error in the database operation, etc.) while we synchronize the WAL position between tablesync worker and apply worker; this will be onerous especially for large copies,2.Using a single transaction in the synchronization-phase (where we can receive WAL from multiple transactions) will have the risk of exceeding the CID limit,3.The slot will hold the WAL till the entire sync is complete because we never commit till the end.

每个 Tablesync Worker 都有一个关联的复制槽。以前是临时的插槽,仅在每个 Tablesync Worker 的生命周期内存在。如果 Tablesync Worker 意外崩溃,则槽位丢失,当替代 Tablesync Worker 启动时,它会从新创建的临时槽位开始,然后再次重复所有内容,就像第一次看到一样。Tablesync Worker已改成使用永久的复制槽而不是临时复制槽。复制源信息会保存,用于跟踪已经复制的数据,因此通过使用永久插槽,这意味着在崩溃/重新启动后,可以从最近记录的检查点再次继续复制。

复制槽命名格式如下:pg<subscription OID>sync<table relid><system id>

回到目录结构pg_logical,那么mappings和snapshots这两个子目录有什么用呢?

snapshots

snapshots顾名思义,快照,逻辑复制相关的脏数据会写到pg_logical/snapshots,发布端插入一些数据

postgres=# insert into test select n,'test' from generate_series(1,1000000) as n;
INSERT 0 1000000
postgres=# insert into test select n,'test' from generate_series(1,1000000) as n;
INSERT 0 1000000
[postgres@xiongcc ~]$ ll pgdata/pg_logical/snapshots/
total 16
-rw------- 1 postgres postgres 132 Feb  8 16:35 7-E54791E0.snap
-rw------- 1 postgres postgres 128 Feb  8 16:37 7-E5479218.snap
-rw------- 1 postgres postgres 128 Feb  8 16:37 7-E547E478.snap
-rw------- 1 postgres postgres 128 Feb  8 16:37 7-ECEE91F8.snap

然后做个checkpoint,在checkpoint期间会回收相关数据

[postgres@xiongcc ~]$ ll pgdata/pg_logical/snapshots/
total 8
-rw------- 1 postgres postgres 128 Feb  8 16:38 7-F868C058.snap
-rw------- 1 postgres postgres 128 Feb  8 16:38 7-F86FB138.snap

具体每个字节是什么意思还要找时间看看代码

[postgres@xiongcc ~]$ hexdump -C pgdata/pg_logical/snapshots/7-F868C058.snap | more
00000000  01 e0 a1 51 5d 1b 3c e2  04 00 00 00 80 00 00 00  |...Q].<.........|
00000010  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  db 80 12 00 d6 80 12 00  e0 3d 78 dd 07 00 00 00  |.........=x.....|
00000030  e0 3d 78 dd 07 00 00 00  00 00 00 00 00 00 00 00  |.=x.............|
00000040  00 00 00 00 00 00 00 00  90 bf 68 f8 07 00 00 00  |..........h.....|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000060  00 00 00 00 00 00 00 00  80 00 00 00 00 00 00 00  |................|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000080

checkpoint回收的代码可以参照CheckPointSnapBuild

/*
 * Remove all serialized snapshots that are not required anymore because no
 * slot can need them. This doesn't actually have to run during a checkpoint,
 * but it's a convenient point to schedule this.
 *
 * NB: We run this during checkpoints even if logical decoding is disabled so
 * we cleanup old slots at some point after it got disabled.
 */

void
CheckPointSnapBuild(void)
{
    XLogRecPtr    cutoff;
    XLogRecPtr    redo;
    DIR           *snap_dir;
    struct dirent *snap_de;
    char        path[MAXPGPATH + 21];

mappings

那么mappings是什么呢,在插入数据解码的过程中,该子目录下面一直是空的,那么什么时候会有数据呢?

[postgres@xiongcc ~]$ ll pgdata/pg_logical/mappings/
total 0

翻翻代码看看

/*
 * Apply any existing logical remapping files if there are any targeted at our
 * transaction for relid.
 */

static void
UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot)
{
    DIR           *mapping_dir;
    struct dirent *mapping_de;
    List       *files = NIL;
    ListCell   *file;
    Oid            dboid = IsSharedRelation(relid) ? InvalidOid : MyDatabaseId;

    mapping_dir = AllocateDir("pg_logical/mappings");
    ...
/*
 * Apply a single mapping file to tuplecid_data.
 *
 * The mapping file has to have been verified to be a) committed b) for our
 * transaction c) applied in LSN order.
 */

static void
ApplyLogicalMappingFile(HTAB *tuplecid_data, Oid relid, const char *fname)
{
    char        path[MAXPGPATH];
    int            fd;
    int            readBytes;
    LogicalRewriteMappingData map;

    sprintf(path, "pg_logical/mappings/%s", fname);
    fd = OpenTransientFile(path, O_RDONLY | PG_BINARY);
    if (fd < 0)
        ereport(ERROR,
                (errcode_for_file_access(),
                 errmsg("could not open file \"%s\": %m", path)));

    while (true)
    {
        ReorderBufferTupleCidKey key;
        ReorderBufferTupleCidEnt *ent;
        ReorderBufferTupleCidEnt *new_ent;
    /*
     * failed to find a mapping, check whether the table was rewritten and
     * apply mapping if so, but only do that once - there can be no new
     * mappings while we are in here since we have to hold a lock on the
     * relation.
     */

    if (ent == NULL && !updated_mapping)
    {
        UpdateLogicalMappings(tuplecid_data, htup->t_tableOid, snapshot);
        /* now check but don't update for a mapping again */
        updated_mapping = true;
        goto restart;
    }

可以看到,和逻辑映射关系以及table rewritten有关,那么给表做一个DDL试试

postgres=# alter table test add column t_time timestamp;
ALTER TABLE
postgres=# vacuum test;
VACUUM
postgres=# vacuum full test;
VACUUM

但目录下还是空的

[postgres@xiongcc mappings]$ ll 
total 0
[postgres@xiongcc mappings]$ ll 
total 0
[postgres@xiongcc mappings]$ ll 
total 0

再看一下代码,在rewriteheap.c里面也涉及到了mappings,此处提到了XLOG_HEAP2_REWRITE这个类型的wal record

/*
 * Replay XLOG_HEAP2_REWRITE records
 */

void
heap_xlog_logical_rewrite(XLogReaderState *r)
{
    char        path[MAXPGPATH];
    int            fd;
    xl_heap_rewrite_mapping *xlrec;
    uint32        len;
    char       *data;

    xlrec = (xl_heap_rewrite_mapping *) XLogRecGetData(r);

    snprintf(path, MAXPGPATH,
             "pg_logical/mappings/" LOGICAL_REWRITE_FORMAT,
             xlrec->mapped_db, xlrec->mapped_rel,
             LSN_FORMAT_ARGS(xlrec->start_lsn),
             xlrec->mapped_xid, XLogRecGetXid(r));

wal record的类型在src\include\access\heapam_xlog.h,

•heap2(RM_HEAP2_ID)是数据库对heap的修改产生的wal record(去除heap)•heap(RM_HEAP_ID)是数据库对heap进行insert、delete、update等常规操作后所产生的日志类型

其实根据名字也能看出大概的用途

/*
 * WAL record definitions for heapam.c's WAL operations
 *
 * XLOG allows to store some information in high 4 bits of log
 * record xl_info field.  We use 3 for opcode and one for init bit.
 */

#define XLOG_HEAP_INSERT          0x00
#define XLOG_HEAP_DELETE          0x10
#define XLOG_HEAP_UPDATE          0x20
#define XLOG_HEAP_TRUNCATE        0x30
#define XLOG_HEAP_HOT_UPDATE      0x40
#define XLOG_HEAP_CONFIRM         0x50
#define XLOG_HEAP_LOCK            0x60
#define XLOG_HEAP_INPLACE         0x70

#define XLOG_HEAP_OPMASK          0x70
/*
 * When we insert 1st item on new page in INSERT, UPDATE, HOT_UPDATE,
 * or MULTI_INSERT, we can (and we do) restore entire page in redo
 */

#define XLOG_HEAP_INIT_PAGE        0x80
/*
 * We ran out of opcodes, so heapam.c now has a second RmgrId.  These opcodes
 * are associated with RM_HEAP2_ID, but are not logically different from
 * the ones above associated with RM_HEAP_ID.  XLOG_HEAP_OPMASK applies to
 * these, too.
 */

#define XLOG_HEAP2_REWRITE        0x00
#define XLOG_HEAP2_PRUNE          0x10
#define XLOG_HEAP2_VACUUM         0x20
#define XLOG_HEAP2_FREEZE_PAGE    0x30
#define XLOG_HEAP2_VISIBLE        0x40
#define XLOG_HEAP2_MULTI_INSERT   0x50
#define XLOG_HEAP2_LOCK_UPDATED   0x60
#define XLOG_HEAP2_NEW_CID        0x70

回到XLOG_HEAP2_REWRITE,当开启logical日志模式时,在系统表和一些特殊用户表在执行vacuum full的过程中,会产生这个wal记录类型。这个wal日志类型主要是为了恢复pg_logical/mapping下的文件,mapping目录下的文件用于在系统表发生vacuum full后来判断系统表元组的可见性。

/*
 * Replay XLOG_HEAP2_REWRITE records
 */

void
heap_xlog_logical_rewrite(XLogReaderState *r)
{
    char        path[MAXPGPATH];
    int            fd;
    xl_heap_rewrite_mapping *xlrec;
    uint32        len;
    char       *data;

    xlrec = (xl_heap_rewrite_mapping *) XLogRecGetData(r);

    snprintf(path, MAXPGPATH,
             "pg_logical/mappings/" LOGICAL_REWRITE_FORMAT,
             xlrec->mapped_db, xlrec->mapped_rel,
             LSN_FORMAT_ARGS(xlrec->start_lsn),
             xlrec->mapped_xid, XLogRecGetXid(r));

    fd = OpenTransientFile(path,
                           O_CREAT | O_WRONLY | PG_BINARY);
    if (fd < 0)
        ereport(ERROR,
                (errcode_for_file_access(),
                 errmsg("could not create file \"%s\": %m", path)));

所以这里也可以看出,假如碰到类似的错,有迹可循,参照 https://postgrespro.com/list/thread-id/2399779

 ERROR:  53000: exceeded maxAllocatedDescs (492) while trying to open
 file "pg_logical/mappings/map-4000-4eb-1_60DE1E08-5376b5-537c6b"
 LOCATION:  OpenTransientFile, fd.c:2161
 LOG:  00000: starting logical decoding for slot "s"
 DETAIL:  streaming transactions committing after 1/6097DD48, reading
 WAL from 1/60275848
 LOCATION:  CreateDecodingContext, logical.c:414
 LOG:  00000: logical decoding found consistent point at 1/60275848
 DETAIL:  Logical decoding will begin using saved snapshot.

并且此处也提到了,运行了vacuum full pg_class

I suppose there are reasons why it's done this way, and admittedly the test that happens to trigger this is a bit extreme (essentially running pgbench concurrently with 'vacuum full pg_class' in a loop)

相关结构体

/* logical rewrite xlog record header */
typedef struct xl_heap_rewrite_mapping
{
    TransactionId mapped_xid;    /* xid that might need to see the row */
    Oid            mapped_db;        /* DbOid or InvalidOid for shared rels */
    Oid            mapped_rel;        /* Oid of the mapped relation */
    off_t        offset;            /* How far have we written so far */
    uint32        num_mappings;    /* Number of in-memory mappings */
    XLogRecPtr    start_lsn;        /* Insert LSN at begin of rewrite */
} xl_heap_rewrite_mapping;

pg_logical/mappings目录下的文件格式如下

/*
 * On-Disk data format for an individual logical rewrite mapping.
 */

typedef struct LogicalRewriteMappingData
{
    RelFileNode old_node;
    RelFileNode new_node;
    ItemPointerData old_tid;
    ItemPointerData new_tid;
} LogicalRewriteMappingData;

那么做一个vacuum full试试

postgres=# vacuum full pg_class;
VACUUM

果然,出现了mapping文件

[postgres@xiongcc mappings]$ ll
total 4
-rw------- 1 postgres postgres 36 Feb  8 18:21 map-3642-4eb-8_25DA5598-1280ed-1280ed

同理,checkpoint的时候去删除mapping文件

/* ---
 * Perform a checkpoint for logical rewrite mappings
 *
 * This serves two tasks:
 * 1) Remove all mappings not needed anymore based on the logical restart LSN
 * 2) Flush all remaining mappings to disk, so that replay after a checkpoint
 *      only has to deal with the parts of a mapping that have been written out
 *      after the checkpoint started.
 * ---
 */

void
CheckPointLogicalRewriteHeap(void)
{
    XLogRecPtr    cutoff;
    XLogRecPtr    redo;
    DIR           *mappings_dir;
    struct dirent *mapping_de;
    char        path[MAXPGPATH + 20];

    /*
     * We start of with a minimum of the last redo pointer. No new decoding
     * slot will start before that, so that's a safe upper bound for removal.
     */

    redo = GetRedoRecPtr();
postgres=# checkpoint ;
CHECKPOINT

mappings文件被删除

[postgres@xiongcc mappings]$ ll
total 0

小结

逻辑解码依赖于WAL日志,进行decoding,变更都记录在WAL日志里面,翻译出来必须要知道当时的表结构是啥样的,即需要元数据信息才能将二进制信息转化为逻辑操作信息。因为也需要类似的快照保留机制,这个就是pg_replication_slots里面的catalog_xmin字段的作用,vacuum在清理系统表的时候会加以判断,保留还需要的那些系统表元组,这个mappings也是类似的道理。

不过话说回来,PostgreSQL里面的等待事件光看官网,有些地方着实令人费解,需要实际场景的打磨。罪名+1,白鳝老师在公众号上有过一篇整理,《针对PG的等待事件不妨玩点大的》各位感兴趣的童鞋可以看一下,不过明天会放出最新的等待事件,让我们拭目以待。






原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。

上一篇: 深度剖析CIC
下一篇: 锁拾遗
请登录后发表评论 登录
全部评论

注册时间:2020-07-14

  • 博文量
    306
  • 访问量
    139319