PG 源代码分析札记

admin 28天前 150

我用会这个帖子来记录我分析 PG 源代码的一些体会和收获。

1. 为什么我们修改了一些参数后必须重启 PG 数据库集群,这些参数才能生效?

我们知道,当我们修改了某个参数后,一种方式使用 SELECT pg_reload_conf()就可以使这个参数的新值生效,例如 archive_command,但是有些参数的修改,必须重启数据库集群才能够生效,譬如 max_wal_senders。这是什么原因呢?

在 postmaster.c 中,主进程的入口函数里会调用

	/*
	 * Set up shared memory and semaphores.
	 *
	 * Note: if using SysV shmem and/or semas, each postmaster startup will
	 * normally choose the same IPC keys.  This helps ensure that we will
	 * clean up dead IPC objects if the postmaster crashes and is restarted.
	 */
	CreateSharedMemoryAndSemaphores();

在这个函数中,它会调用CalculateShmemSize()函数一次性计算全部需要的共享内存大小,然后一次性创建好整块共享内存,而且在整个数据库集群运行期间,这块共享内存的大小是无法改变的。所以一旦你修改的参数影响了整个共享内存的大小,就需要重新启动数据库集群,让程序再次调用CreateSharedMemoryAndSemaphores()函数,才能让这个参数的新值生效。

 

最新回复 (7)
  • admin 23天前
    引用 2

    2. 检查点的触发时机

    检查点进程的入口函数是CheckpointerMain(),源代码文件是 src/backend/postmaster/checkpointer.c。

    这个函数的主体是一个无限循环for (;;) {...},在这个循环体里面有一个布尔变量do_checkpoint,如果它为 true,则需要做检查点操作:

                     /*
    		 * Do a checkpoint if requested.
    		 */
    		if (do_checkpoint) 
    		{
                              ......
    			if (!do_restartpoint)
    			{
    				CreateCheckPoint(flags);
    				ckpt_performed = true;
    			}
    			else
    				ckpt_performed = CreateRestartPoint(flags);
                             ......
                     }

    现在就有一个问题了,do_checkpoint什么时候为 true 呢?我们继续分析源码,发现有两个条件,一个是

    if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
    		{
    			do_checkpoint = true;
    			chkpt_or_rstpt_requested = true;
    		}

    一个是超时:

    		now = (pg_time_t) time(NULL);
    		elapsed_secs = now - last_checkpoint_time;
    		if (elapsed_secs >= CheckPointTimeout) 
    		{
    			if (!do_checkpoint)
    				chkpt_or_rstpt_timed = true;
    			do_checkpoint = true;
    			flags |= CHECKPOINT_CAUSE_TIME;
    		}

    通过上面的代码逻辑,我们知道了,当时间超过 checkpoint_timeout 参数设置的时间间隔后,会自动触发一个检查点,或者共享内存中的 ckpg_flags 不等于 0 时,就会触发检查点操作。

    检查点操作在执行时,要区分数据库集群实例是否处于恢复模式(或者是备库,因为备库始终处于恢复模式),又分为CreateCheckPoint(flags)调用和CreateRestartPoint()两个函数的调用。前者在数据库集群实例处于非恢复模式进行,后者在数据库集群实例处于恢复模式时进行。

     

    从上述代码中我们可以看到,检查点的触发机制分为超时触发和别的条件触发,别的条件满足后,就会设置共享内存中的ckpt_flags这个值,从而再下一次循环中触发检查点。譬如当WAL文件的体积超过max_wal_size规定的值以后,就会设置这个ckpt_flags,触发检查点。

    /* These indicate the cause of a checkpoint request */
    #define CHECKPOINT_CAUSE_XLOG	0x0080	/* XLOG consumption */
    #define CHECKPOINT_CAUSE_TIME	0x0100	/* Elapsed time */
    

    在一个良好的数据库系统中,应该是超时触发检查点越多越好,其它触发次数越少越好。

    具体可以参考这篇文章:

    https://www.enterprisedb.com/blog/tuning-maxwalsize-postgresql

     

  • admin 21天前
    引用 3

    3. PostgreSQL主进程寻找参数文件的逻辑

    在postmaster.c中,在主进程启动阶段,会执行如下代码:

    	/*
    	 * Locate the proper configuration files and data directory, and read
    	 * postgresql.conf for the first time.
    	 */
    	if (!SelectConfigFiles(userDoption, progname)) 
    		ExitPostmaster(2);
    

    我们阅读SelectConfigFiles的代码,不难发现PostgreSQL主进程寻找参数文件的逻辑。PG有三个参数文件,其定义如下:

    #define CONFIG_FILENAME "postgresql.conf"
    #define HBA_FILENAME	"pg_hba.conf"
    #define IDENT_FILENAME	"pg_ident.conf"

    这三个配置文件必须存在,否则主进程拒绝启动。具体逻辑可以参考SelectConfigFiles()函数的代码。

    PG寻找这三个参数的逻辑是:

    1. 用户在启动postgresql程序时指定了-c参数,则以这个参数指定的参数文件为准。这个优先级最高。
    2. 如果用户在pg_ctl中指定的-D参数,则把-D后面的字符串当做一个目录,譬如它的值是/xxxx/yyyy,则PG会尝试发现/xxxx/yyyy/postgresql.conf和/xxxx/yyyy/pg_hba.conf。这个优先级次之。
    3. 如果用户设置了$PGDATA环境变量,则以该环境变量指示的路径为准。这个优先级最低。
  • admin 21天前
    引用 4

    4. PG_VERSION文件的作用

    当我们使用initdb创建一个数据库集群(database cluster)的时候,在它的目录下有一个小小的文本文件叫做PG_VERSION

    postgres@ip-172-31-29-179:~/postgresql-17.5$ ls -l /home/postgres/data1
    total 128
    -rw------- 1 postgres postgres     3 Jul 18 17:10 PG_VERSION
    drwx------ 7 postgres postgres  4096 Jul 18 17:16 base
    drwx------ 2 postgres postgres  4096 Jul 18 17:16 global
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_commit_ts
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_dynshmem
    -rw------- 1 postgres postgres  5830 Jul 18 17:11 pg_hba.conf
    -rw------- 1 postgres postgres  2640 Jul 18 17:10 pg_ident.conf
    drwx------ 4 postgres postgres  4096 Jul 18 17:20 pg_logical
    drwx------ 4 postgres postgres  4096 Jul 18 17:10 pg_multixact
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_notify
    drwx------ 2 postgres postgres  4096 Jul 18 17:15 pg_replslot
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_serial
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_snapshots
    drwx------ 2 postgres postgres  4096 Jul 18 17:12 pg_stat
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_stat_tmp
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_subtrans
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_tblspc
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_twophase
    drwx------ 4 postgres postgres  4096 Jul 18 17:22 pg_wal
    drwx------ 2 postgres postgres  4096 Jul 18 17:10 pg_xact
    -rw------- 1 postgres postgres   109 Jul 18 17:24 postgresql.auto.conf
    -rw------- 1 postgres postgres 32433 Jul 18 17:12 postgresql.conf
    -rw------- 1 postgres postgres    63 Jul 18 17:12 postmaster.opts
    -rw------- 1 postgres postgres    81 Jul 18 17:12 postmaster.pid
    postgres@ip-172-31-29-179:~/postgresql-17.5$ cat /home/postgres/data1/PG_VERSION
    18

    这个文件是用来校验数据库集群的版本和软件的大版本是否一致,如果不一致,则无法启动数据库集群。在PG编译源码时,会写死一个常数:

    src/include/pg_config.h:#define PG_VERSION "17.5"

    pg_config.h这个头文件是在执行configure命令时自动产生,它会侦测你的软件版本,写死一个常数PG_VERSION。

    在PostmasterMain() -> checkDataDir() -> ValidatePgVersion()中,你可以查阅ValidatePgVersion()函数的源代码,它会读取这个小小的PG_VERSION文件,如果发现里面的数字和软件的大版本不一致,就拒绝启动。

    	if (my_major != file_major)
    		ereport(FATAL,
    				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
    				 errmsg("database files are incompatible with server"),
    				 errdetail("The data directory was initialized by PostgreSQL version %s, "
    						   "which is not compatible with this version %s.",
    						   file_version_string, my_version_string)));
  • admin 20天前
    引用 5

    5. 如果数据库集群目录下同时存在standby.signal和recovery.signal会出现什么结果?

    我们知道,在做PITR过程中需要在数据库集群目录下创建recovery.signal信号文件,在启动备库时需要在数据库集群目录下创建standby.signal信号文件。所谓信号文件,就是指该文件的内容并不重要,这个文件的存在就代表一种信号。所以我们经常使用类似touch recovery.signal的命令创建一个空文件。

    现在有一个问题:如果你在数据库集群目录中同时创建了standby.signal和recovery.signal两个信号文件,PG会如何处理呢?

    通过阅读源代码,我们得知,postmaster主进程启动后,会无条件启动startup进程。Startup进程的入口函数是StartupProcessMain(),如果你不知道这个函数在哪个文件中,你可以在PG的源代码目录下使用类似如下的命令进行搜索:

    postgresql-17.5 $ find . -name \*.c | xargs grep StartupProcessMain
    ./src/backend/postmaster/launch_backend.c:	[B_STARTUP] = {"startup", StartupProcessMain, true},
    ./src/backend/postmaster/startup.c:StartupProcessMain(char *startup_data, size_t startup_data_len)

    由上面的结果我们可以很清楚地看到这个函数在src/backend/postmaster/startup.c这个文件中。从这个入口函数的代码入手,我们耐着性子就可以理顺它的调用逻辑:

    StartupProcessMain() -> StartupXLOG() -> InitWalRecovery() -> readRecoverySignalFile()

    我们分析readRecoverySignalFile()这个函数,可以看到如下的逻辑:

    	if (stat(STANDBY_SIGNAL_FILE, &stat_buf) == 0) /// 如果找到了该文件, #define STANDBY_SIGNAL_FILE		"standby.signal"
    	{
    		int			fd;
    
    		fd = BasicOpenFilePerm(STANDBY_SIGNAL_FILE, O_RDWR | PG_BINARY,
    							   S_IRUSR | S_IWUSR);
    		if (fd >= 0)
    		{
    			(void) pg_fsync(fd);
    			close(fd);
    		}
    		standby_signal_file_found = true; /// 设置这个变量为true.
    	}
    	else if (stat(RECOVERY_SIGNAL_FILE, &stat_buf) == 0) /// #define RECOVERY_SIGNAL_FILE	"recovery.signal"
    	{
    		int			fd;
    
    		fd = BasicOpenFilePerm(RECOVERY_SIGNAL_FILE, O_RDWR | PG_BINARY,
    							   S_IRUSR | S_IWUSR);
    		if (fd >= 0)
    		{
    			(void) pg_fsync(fd);
    			close(fd);
    		}
    		recovery_signal_file_found = true;
    	}
    	/// 从上面的逻辑可以看出,standby.signal的优先级要比recovery.signal高。

    上面的代码逻辑很清楚地显示:如果startup进程检测到了standby.signal,它就无视recovery.signal了。检测文件所使用的系统函数是stat(),它的用法可以参考如下链接:

    https://man7.org/linux/man-pages/man2/stat.2.html

    希望以上的源代码分析给你一些小的知识。

  • admin 18天前
    引用 6

    6. 关于PG源代码中的ereport类似的函数的一些理解

    当我们阅读PG源代码时,会很容易注意到在源代码各处都有类似的函数调用:

    ereport(PANIC,
        (errmsg("could not locate a valid checkpoint record at %X/%X",
            LSN_FORMAT_ARGS(CheckPointLoc))));

    这种函数类似C语言中的printf()函数,会在日志中写入信息。我们看看这个函数的定义:

    #define ereport(elevel, ...)	\
    	ereport_domain(elevel, TEXTDOMAIN, __VA_ARGS__)

    再继续挖掘下去:

    #define ereport_domain(elevel, domain, ...)	\
    	do { \
    		const int elevel_ = (elevel); \
    		pg_prevent_errno_in_scope(); \
    		if (errstart(elevel_, domain)) \
    			__VA_ARGS__, errfinish(__FILE__, __LINE__, __func__); \
    		if (elevel_ >= ERROR) \
    			pg_unreachable(); \
    	} while(0)

    我们在上述代码中看到这样的语句:

    		if (elevel_ >= ERROR) \
    			pg_unreachable(); \

    pg_unreachable()函数的定义如下:

    #define pg_unreachable() abort()

    关于abort()函数的用法,可以参考这个网页:

    https://man7.org/linux/man-pages/man3/abort.3.html

    由这个网页可知:abort()实际上就是终止本进程。

    ERROR的定义如下:

    #define PGWARNING	19			/* Must equal WARNING; see NOTE below. */
    #define WARNING_CLIENT_ONLY	20	/* Warnings to be sent to client as usual, but
    								 * never to the server log. */
    #define ERROR		21			/* user error - abort transaction; return to
    								 * known state */
    #define PGERROR		21			/* Must equal ERROR; see NOTE below. */
    #define FATAL		22			/* fatal error - abort process */
    #define PANIC		23			/* take down the other backends with me */
    

    根据以上的分析,我们可以得出一个结论:

    当我们使用ereport()时,如果第一个参数是ERROR或者更高等级的错误(包括PGERROR, FATAL和PANIC)时,则本进程在ereport()函数执行结束时会终止。

  • xiaobu 8天前
    引用 7

    一个PG数据块中最多可以放多少条记录?


    因为PG是开源的,所以我们很容易搞清楚PG的数据文件中的数据块的结构。概而言之,它分为24字节的块头,每个指针4字节,每条记录的开销23字节。MaxHeapTulpesPerPage是一个常量,它就表示理论上一个数据块能放多少条记录,其定义如下:

    /*
     * MaxHeapTuplesPerPage is an upper bound on the number of tuples that can
     * fit on one heap page.  (Note that indexes could have more, because they
     * use a smaller tuple header.)  We arrive at the divisor because each tuple
     * must be maxaligned, and it must have an associated line pointer.
     *
     * Note: with HOT, there could theoretically be more line pointers (not actual
     * tuples) than this on a heap page.  However we constrain the number of line
     * pointers to this anyway, to avoid excessive line-pointer bloat and not
     * require increases in the size of work arrays.
     */
    #define MaxHeapTuplesPerPage	\
    	((int) ((BLCKSZ - SizeOfPageHeaderData) / \
    			(MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))))
    

    从上面的定义可以看出,BLCKSZ = 8192, SizeOfPageHeaderData = 24, MAXALIGN(SizeofHeapTuleHeader) = 24, sizeof(ItemIdData) = 4,套用上面的公式,则:

    MaxHeapTuplesPerPage = (8192 - 24) /(24 + 4) = 291

    即:

    在PG数据库中,一个数据块中最多存放291条记录。

  • xiaobu 8天前
    引用 8

    一些常用数据结构的体积(单位是字节)


    本人亲测,非常准确。

    /*
     * line pointer(s) do not count as part of header
     */
    #define SizeOfPageHeaderData (offsetof(PageHeaderData, pd_linp)) 

    SizeOfPageHeaderData是24字节。

    sizeof(ItemIdData) 是4字节。

     

返回
发新帖