更多"C/C++、PostgreSQL、编译原理、计算机原理、TCP/IP、数据结构&算法、Linux编程”等技术文章更新于公众号: 君子黎


1. pg_control文件

     pg_control是一个8KB大小的二进制文件,该文件中记录了PostgreSQL服务器内部信息状态的各方面信息,比如最新检查点(checkpoint)、系统状态、当前运行的postgres服务版本、CRC校验,以及initdb初始化PostgreSQL数据库蔟时设置的某些基本参数。它是在PostgreSQL的7.1版本中新引入的。实际上,该文件中的有效字段值内容仅有几百字节,即sizeof(ControlFileData)。 剩下的数据全用零填充。这个在1.3.1小节会详细介绍。

在这里插入图片描述

     从PostgreSQL的9.6版本开始,以下几个函数也可以用于从该文件(pg_control)中提取相应的数据信息。
     I. pg_control_checkpoint()
     有关当前检查点状态的信息。 如下图所示:

在这里插入图片描述

     II. pg_control_system()
     pg_control控制文件状态信息。比如当前运行的postgres版本号、目录版本号、系统标识和该控制文件的最后一次修改时间。详情如下图所示:

在这里插入图片描述

     III. pg_control_init()
     该函数将查询出有关集群初始化状态信息。详情如下图所示:

在这里插入图片描述

     VI. pg_control_recovery()
     关于恢复状态信息。如下图所示:

在这里插入图片描述

     pg_control控制文件的格式可以进行变更,即从一个发行包变更为另一个发行版。
     

1.1 pg_control文件位于何处?

     该文件位于PostgreSQL数据蔟的global目录下,即PGDATA/global下,如下图所示:

在这里插入图片描述

1.2 pg_control文件数据内容

     不同的PostgreSQL数据库版本,pg_control文件中的内容会有些许差异,因为每次版本的迭代都意味着修复了某些已知缺陷,或是优化了某些性能瓶颈。因此,本次文件中内容相比于其他版本的控制文件,可能多增加了一些字段,也可能移除了一些字段。比如:PostgreSQL 12版本中,新增了:int max_wal_sender,同时又删除了:uint32 nextXidEpoch, TransactionnextXid; PostgreSQL 11版本中,删除了字段: XLogRecPtr prevCheckPoint; PostgreSQL 10版本中,添加了字段: char mock_authentication_nonce[MOCK_AUTH_NONCE_LEN]等等。

     对于PostgreSQL 13.2版本,pg_control文件中的内容即为ControlFileData结构体变量值。该结构体类型声明如下(共35个成员变量):

typedef struct ControlFileData
{
    uint64        system_identifier;            //系统唯一标识
    uint32        pg_control_version;         //pg的控制版本号. 在13.2版本中,#define PG_CONTROL_VERSION    1300
    uint32        catalog_version_no;         //目录版本号. 在13.2版本中, #define CATALOG_VERSION_NO    202007201

    DBState        state;                       //系统状态数据. 
    pg_time_t    time;    

    XLogRecPtr    checkPoint;        
    CheckPoint    checkPointCopy;
    XLogRecPtr    unloggedLSN;
    XLogRecPtr    minRecoveryPoint;
    TimeLineID    minRecoveryPointTLI;
    XLogRecPtr    backupStartPoint;
    XLogRecPtr    backupEndPoint;
    bool        backupEndRequired;

    int            wal_level;
    bool        wal_log_hints;
    int            MaxConnections;
    int            max_worker_processes;
    int            max_wal_senders;
    int            max_prepared_xacts;
    int            max_locks_per_xact;
    bool        track_commit_timestamp;

    uint32        maxAlign;        
    double        floatFormat;
#define FLOATFORMAT_VALUE    1234567.0

    uint32        blcksz;            
    uint32        relseg_size;    
    uint32        xlog_blcksz;    
    uint32        xlog_seg_size;    
    uint32        nameDataLen;    
    uint32        indexMaxKeys;    
    uint32        toast_max_chunk_size;
    uint32        loblksize;

    bool        float8ByVal;
    uint32        data_checksum_version;
    char        mock_authentication_nonce[MOCK_AUTH_NONCE_LEN];
    pg_crc32c    crc;
} ControlFileData;

     由于ControlFileData结构体类型中的成员数量较多,因此这里仅对其中部分成员做详细说明。

     · state 系统状态

     成员state表示当前系统的状态,其类型为DBState。在PostgreSQL 13.2版本中,PostgreSQL数据库管理系统的状态信息有以下几种:

typedef enum DBState
{
    DB_STARTUP = 0,                    //启动
    DB_SHUTDOWNED,                    //关机
    DB_SHUTDOWNED_IN_RECOVERY,        //关机恢复
    DB_SHUTDOWNING,                    //关机中
    DB_IN_CRASH_RECOVERY,            //奔溃恢复
    DB_IN_ARCHIVE_RECOVERY,            //存档恢复
    DB_IN_PRODUCTION                //生产(正确启动成功的PostgreSQL服务器状态码)
} DBState;

     当执行附加选项-start的pg_ctl程序命令时候,若PostgreSQL服务正常启动,那么系统状态值为“DB_IN_PRODUCTION”。对于以上几个系统状态枚举值,有对应的可视化字符串表示,分别对应如下:

static const char *
dbState(DBState state)
{
    switch (state)
    {
        case DB_STARTUP:
            return _("starting up");
        case DB_SHUTDOWNED:
            return _("shut down");
        case DB_SHUTDOWNED_IN_RECOVERY:
            return _("shut down in recovery");
        case DB_SHUTDOWNING:
            return _("shutting down");
        case DB_IN_CRASH_RECOVERY:
            return _("in crash recovery");
        case DB_IN_ARCHIVE_RECOVERY:
            return _("in archive recovery");
        case DB_IN_PRODUCTION:
            return _("in production");
    }
    return _("unrecognized status code");
}

     当然,控制文件(pg_control)中的数据均为二进制,并没有写入系统枚举值对应的可视化字符串。这里终端展示的数据信息是使用了PostgreSQL提供的客户端命令工具读取转换展示的,该工具下面会详细介绍。

在这里插入图片描述
     · catalog_version_no 目录版本号

     目录版本号用于标记PostgreSQL系统目录中不兼容的更改。每当有人更改系统目录关系的格式,如添加、删除或修改标准目录条目时,更新的后端将无法与旧数据库一起工作(反之亦然),目录版本号也应该更改。initdb将存储在pg_control中的版本号与启动时编译到后端的版本号进行检查,这样后端就可以拒绝在不兼容的数据库中运行。

     如果开发人员提交了init的更改,则同时也应该更新目录版本号。对于目录版本号的命名,使用的格式是:"YYYYMMDDN"。 其中YYYY-代表年份、 MM-代表月、 DD-代表某天,N则代表当天更改的编号,该编号从1开始(备注: 希望永远不要再同一天提交10次独立的目录更改请求)。该版本号用宏CATALOG_VERSION_NO表示,该宏位于/src/include/catalog/目录中的catversion.h文件中。

     · time pg_control文件最后一次更新的时间戳.
     PostgreSQL数据库的每次启动、关闭、重启等等都会对文件中的该字段进行实时更新。

     该结构体类型中的其他一些成员是用于确保此数据库的配置与后端进程可执行程序的相兼容。还有一些参数用于归档或是热备用的参数设置。可与1.2.1节中pg_controldata命令所展示的数据相结合进行阅读。

1.2.1 借助工具查看pg_control中的数据

     由于pg_control控制文件中的内容是二进制,所以你无法使用类似strings等命令之间来查看文件中的数据。不过好在PostgreSQL数据库提供了用来查看该文件的程序命令pg_controldata,该命令的使用方式如下:

     su postgres -c '/usr/local/postgresql_13_2/bin/pg_controldata -D /home/ssd/PGSQL132'

     使用该命令可以直接看到pg_control文件中的数据,如下所示(以下展示的数据来自PostgreSQL 13.2版本):

pg_control version number:            1300
Catalog version number:               202007201
Database system identifier:           6937939388570580870
Database cluster state:               in production
pg_control last modified:             Wed 17 Mar 2021 05:34:45 PM CST
Latest checkpoint location:           0/15C1FF8
Latest checkpoint's REDO location:    0/15C1FC0
Latest checkpoint's REDO WAL file:    000000010000000000000001
Latest checkpoint's TimeLineID:       1
Latest checkpoint's PrevTimeLineID:   1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:486
Latest checkpoint's NextOID:          16385
Latest checkpoint's NextMultiXactId:  1
Latest checkpoint's NextMultiOffset:  0
Latest checkpoint's oldestXID:        478
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  486
Latest checkpoint's oldestMultiXid:   1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint:            Wed 17 Mar 2021 05:34:45 PM CST
Fake LSN counter for unlogged rels:   0/3E8
Minimum recovery ending location:     0/0
Min recovery ending loc's timeline:   0
Backup start location:                0/0
Backup end location:                  0/0
End-of-backup record required:        no
wal_level setting:                    replica
wal_log_hints setting:                off
max_connections setting:              100
max_worker_processes setting:         8
max_wal_senders setting:              10
max_prepared_xacts setting:           0
max_locks_per_xact setting:           64
track_commit_timestamp setting:       off
Maximum data alignment:               8
Database block size:                  8192
Blocks per segment of large relation: 131072
WAL block size:                       8192
Bytes per WAL segment:                16777216
Maximum length of identifiers:        64
Maximum columns in an index:          32
Maximum size of a TOAST chunk:        1996
Size of a large-object chunk:         2048
Date/time type storage:               64-bit integers
Float8 argument passing:              by value
Data page checksum version:           0
Mock authentication nonce:            e260837100cb356118c5ad1463cac97906e35a660f52ce1953e2b28df2f815d4

1.3 pg_control文件创建原理

     pg_control控制文件是在系统安装时创建的(即initdb),此外,系统安装时候还会初始化XLOG段。对于pg_control文件的创建与初始化主要由函数BootStrapXLOG()负责完成。其内部创建流程如下图所示:
在这里插入图片描述

     系统首先初始化一些相关的变量数据,比如对于系统唯一标识码system_identifier(该系统唯一标识码确保我们将xlog文件与生成它们的安装相匹配), 将根据gettimeofday()函数获取到的时间戳值,再加上当前进程PID,结合位运算,将得到的结果值用于初始化该成员变量。代码如下:

gettimeofday(&tv, NULL);
sysidentifier = ((uint64) tv.tv_sec) << 32;
sysidentifier |= ((uint64) tv.tv_usec) << 12;
sysidentifier |= getpid() & 0xFFF;

      再完成了待写入pg_control文件中的所有成员字段值之后,将创建pg_control文件,并将这些字段值写入该文件中,对于pg_control文件,默认大小是8BK(#define PG_CONTROL_FILE_SIZE 8192),但是该文件该文件中的有效内容字段值为几百字节左右(即sizeof(ControlFileData)),然后多余部分空间将用零来进行填充。这样做减少了在读取pg_control文件时候发生过早的EOF错误的几率。当我们去检查该文件的内容时,我们仍然会失败,但是希望有一个比“无法读取pg_control”更加具体的错误提示。

     对于pg_control文件大小为8KB,其源码中有注释到(位于src/include/catalog/pg_control.h文件中):

#define PG_CONTROL_FILE_SIZE 8192

pg_control文件的物理大小。注意,这比实际使用的大小要大得多(例如,sizeof(ControlFileData))。其思想是保持物理大小恒定,不受格式变化的影响,这样ReadControlFile在查看不兼容的文件时就会传递一个合适的错误版本消息,而不是一个读取错误。

     当所有字段成功写入文件I/O缓存区中之后,将立刻调用fysnc()函数同步将文件句柄I/O缓存区中的数据刷新到磁盘文件上,持久化存储。若刷新磁盘成功,则关闭句柄,继续postmaster的其他初始化流程;反之,则结束本次的数据库操作。

2. pg_control文件功能

     pg_control控制文件可以看做是PostgreSQL数据库的一层保护罩,它可以保证数据库不会被不满足数据蔟相同版本其他程序命令进行读取修改操作。这样就间接有效地保护了数据库系统的安全。比如你不能用PostgreSQL 9.0版本的程序命令pg_ctl去启动由PostgreSQL13.2版本初始化的数据蔟服务器,因为不同版本中,底层细节的某些实现都作了微调,这样冒然地允许混合版本进行数据库操作,那将是很危险的信号。

2.1 pg_control文件对系统的影响

     提到pg_control文件对PostgreSQL数据库的影响,可分为两种情形进行讨论。第一种是,pg_control文件缺失,即PGDATA/global目录根本不存在该文件; 第二种是,PGDATA/global目录下存在pg_control文件,但是该文件中没有数据,或者是数据内容和当前启动服务的postmaster服务版本不匹配。

     首先可以肯定的是,无论出现以上两种情形中的任何一种,PostgreSQL服务都不会正常启动。因为这两种情形的出现都明显表名当前环境上的PostgreSQL服务出现了未知的问题,是不正常的现象。

     当初始化postmaster守护进程的(PostmasterMain()函数)时候,会对pg_control文件进行两次的读取操作(红色字体表示第一次读取pg_contrl文件,紫色字体表示第二次读取pg_control文件)。如下流程图所示:

在这里插入图片描述

2.1.1 pg_control文件不存在

     第一次读取pg_control控制文件时,不对该文件中的内容进行读操作,以及其他的数据逻辑判断,仅仅用于检查该文件(pg_control)是否存在。该功能由函数checkControlFile()完成。

static void
checkControlFile(void)
{
    char        path[MAXPGPATH];
    FILE       *fp;

    snprintf(path, sizeof(path), "%s/global/pg_control", DataDir); //$PGDATA

    fp = AllocateFile(path, PG_BINARY_R);
    if (fp == NULL)
    {
        write_stderr("%s: could not find the database system\n"
                     "Expected to find it in the directory \"%s\",\n"
                     "but could not open file \"%s\": %s\n",
                     progname, DataDir, path, strerror(errno));
        ExitPostmaster(2);
    }

    FreeFile(fp);
}

     若文件存在,则继续下面其他初始化流程,反之,则打印日志信息并结束当前的postmaster服务启动操作。比如这里我特意删除了PGDATA/global目录下的pg_control文件,然后使用pg_ctl启动postmaster服务时候,报错提示并结束。终端打印的日志信息和代码匹配上。

在这里插入图片描述

2.1.2 pg_control文件存在但内容不正确

     若pg_control文件存在,那么至少可以说明该我们正在查看一个真正的数据库目录。通俗地说,就是pg_ctl参数-D后面的选项是真正使用过initdb初始化的数据蔟目录,而非任意指定的一个目录位置。

     之后,我们将当前工作目录切换至PGDATA中。该功能由函数ChangeToDataDir()完成。

void
ChangeToDataDir(void)
{
    AssertState(DataDir);

    if (chdir(DataDir) < 0)
        ereport(FATAL,
                (errcode_for_file_access(),
                 errmsg("could not change directory to \"%s\": %m",
                        DataDir)));
}

     将当前工作目录切换至PGDATA中的原因是因为,大多数的postmaster守护进程和后端进程都是假定我们在PGDATA目录下,因为它们可以使用相对路径来访问数据目录中或是数据目录下的内容。

     接着校验GUC中的一些无效组合参数,然后开始创建PGDATA数据蔟目录锁文件,即postmaster.pid文件,详细创建流程请阅读 【PostgreSQL教程】· postmaster.pid文件都存储了什么? 。当数据目录锁文件postmaster.pid文件创建成功之后,再次打开pg_control锁文件。本次将读取该文件的二进制数据,并对该文件中的内容进行校验处理,此部分的功能主要由函数ReadControlFile()完成。该函数的部分逻辑代码如下:

static void
ReadControlFile(void)
{
    pg_crc32c    crc;
    int            fd;
    static char wal_segsz_str[20];
    int            r;

    /*
     * Read data...
     */
    fd = BasicOpenFile(XLOG_CONTROL_FILE,
                       O_RDWR | PG_BINARY);
    if (fd < 0)
        ereport(PANIC,
                (errcode_for_file_access(),
                 errmsg("could not open file \"%s\": %m",
                        XLOG_CONTROL_FILE)));

    pgstat_report_wait_start(WAIT_EVENT_CONTROL_FILE_READ);
    r = read(fd, ControlFile, sizeof(ControlFileData));
    if (r != sizeof(ControlFileData))
    {
        if (r < 0)
            ereport(PANIC,
                    (errcode_for_file_access(),
                     errmsg("could not read file \"%s\": %m",
                            XLOG_CONTROL_FILE)));
        else
            ereport(PANIC,
                    (errcode(ERRCODE_DATA_CORRUPTED),
                     errmsg("could not read file \"%s\": read %d of %zu",
                            XLOG_CONTROL_FILE, r, sizeof(ControlFileData))));
    }

    pgstat_report_wait_end();

    close(fd);
    . . . . . . //省略若干
}

     pg_control_version成员标识了pg_control本身的格式,而catalog_version_no则负责标识系统目录(PGDATA)的格式。对pg_control文件中读出来的内容,着重判断catlog_version_no字段值,进行兼容性检查,若当前数据库与后端可执行文件不兼容,则在可能造成任何损坏之前立即终止数据库。每个版本的PostgreSQL数据库,都有着唯一的catalog_version_no标识符,对于PostgreSQL 13.2版本,该值为:202007201

     当pg_control文件中的某个内容不正确时候,立即终止数据库的初始化和运行。比如本次测试中,我特意移除pg_control文件,并重新创建一个同名文件,然后尝试启动数据库,最终报错如下:

在这里插入图片描述

3. pg_control文件损坏恢复措施

     若由于某些因素导致pg_control控制文件被损坏,这是可以使用pg_resetwal程序命令来进行恢复。在9.6以及9.6更早之前的版本中,使用pg_resetxlog。

在这里插入图片描述