PG_VERSION文件被修改导致小版本升级后启动失败

一、问题背景

小版本升级,如果版本间不涉及重大改动的话。通常可以通过替换可执行软件包并重启数据库进行快速升级。

pg10.3升级至10.11后启动失败,报错如下:

pg_ctl start
waiting for server to start....2021-02-28 18:51:15.066 CST [20514] 
FATAL:  database files are incompatible with server
2021-02-28 18:51:15.066 CST [20514] 
DETAIL:  The data directory was initialized by PostgreSQL version 11, which is not compatible with this version 10.11.
 stopped waiting
pg_ctl: could not start server
Examine the log output.

从报错来看,提示数据文件和实例版本不匹配,数据目录是由11版本初始化,和版本10.11不兼容。

二、原理分析

这里就比较奇怪,升级后的版本是10.11,为什么报错指出数据版本是11?

首先怀疑是不是当前PGDATA环境变量配置错误,数据目录指向了版本11的实例?
核查后确认数据目录正确,并且升级过程中只修改可执行软件包对应的环境变量即
PATH、LD_LIBRARY_PATH并未涉及PGDATA。

通过走读源代码,基本清楚了这个报错的出处。在postmaster启动时,
走到checkDataDir流程中调用ValidatePgVersion函数比较当前的postgres二进制大版本号和数据目录版本(数据目录版本记录在$PGDATA/PG_VERSION文件中)是否一致。
如果不一致,就报错退出。

来看下代码逻辑:

/*-------------------------------------------------------------------------
 *				Version checking support
 *-------------------------------------------------------------------------
 */

/*
 * Determine whether the PG_VERSION file in directory `path' indicates
 * a data version compatible with the version of this program.
 *
 * If compatible, return. Otherwise, ereport(FATAL).
 */
void
ValidatePgVersion(const char *path)
{
	char		full_path[MAXPGPATH];
	FILE	   *file;
	int			ret;
	long		file_major;
	long		my_major;
	char	   *endptr;
	char		file_version_string[64];
    /* my_version_string获取的当前postgres二进制软件版本
赋值为PG_VERSION,PG_VERSION宏定义位于src/include/pg_config.h中,
定义为:#define PG_VERSION "10.11"
*/
	const char *my_version_string = PG_VERSION;

	/* 通过strtol函数获取my_version_string的主版本号my_major
这里为10(“10.11”通过strtol函数被转换为long int类型的10,函数入参的10表示以10进制表示)
*/
	my_major = strtol(my_version_string, &endptr, 10);
	
/*拼接出PG_VERSION文件的绝对路径,path为函数入参即$PGDATA的绝对路径
*/
	snprintf(full_path, sizeof(full_path), "%s/PG_VERSION", path);

/*读取PG_VERSION 文件内容,AllocateFile 函数中调用fopen读取文件内容*/

	file = AllocateFile(full_path, "r");
	if (!file)
	{
		if (errno == ENOENT)
			ereport(FATAL,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("\"%s\" is not a valid data directory",
							path),
					 errdetail("File \"%s\" is missing.", full_path)));
		else
			ereport(FATAL,
					(errcode_for_file_access(),
					 errmsg("could not open file \"%s\": %m", full_path)));
	}
    
	/* 初始化数据文件版本参数 */
	file_version_string[0] = '\0';
	
	/* 从文件流读入至file_version_string */
	ret = fscanf(file, "%63s", file_version_string);
	/* 获取文件版本的主版本号file_major ,这里为11 */
	file_major = strtol(file_version_string, &endptr, 10);

	if (ret != 1 || endptr == file_version_string)
		ereport(FATAL,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("\"%s\" is not a valid data directory",
						path),
				 errdetail("File \"%s\" does not contain valid data.",
						   full_path),
				 errhint("You might need to initdb.")));

	FreeFile(file);
	
	/* 这里就是报错出处了,由于10 != 11为ture 因此报错*/
	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)));
}

这里有个需要注意的小点,如果您是直接下载的源码包。并导入代码查看工具添加工程,是无法找到PG_VERSION对应的宏定义的,因为src/include/pg_config.h文件不存在。

该文件是运行configure时通过confdef.h、 Makefile.global.in 、config.status pg_config.h.in等文件产生的,因此需要运行configure后,再将源码包导入工具。

报错处代码走读也可以参考下gdb跟踪过程:

(gdb) b ValidatePgVersion
Breakpoint 1 at 0x97baf4: file miscinit.c, line 1367.
(gdb) r
Starting program: /home/postgres/postgresql-10.11/pg10debug/bin/postmaster 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, ValidatePgVersion (path=0xeaf8e0 "/data/pg10-11debug/data") at miscinit.c:1367
1367            const char *my_version_string = PG_VERSION;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.tl2.3.x86_64
(gdb) bt 
#0  ValidatePgVersion (path=0xeaf8e0 "/data/pg10-11debug/data") at miscinit.c:1367
#1  0x000000000078884a in checkDataDir () at postmaster.c:1547
#2  0x0000000000787aa2 in PostmasterMain (argc=1, argv=0xe8ec20) at postmaster.c:878
#3  0x00000000006d0a16 in main (argc=1, argv=0xe8ec20) at main.c:228
(gdb) n
1369            my_major = strtol(my_version_string, &endptr, 10);
(gdb) p my_version_string
$1 = 0xb2b07e "10.11"
(gdb) n
1371            snprintf(full_path, sizeof(full_path), "%s/PG_VERSION", path);
(gdb) p my_major         
$2 = 10
(gdb) n
1373            file = AllocateFile(full_path, "r");
(gdb) p path
$3 = 0xeaf8e0 "/data/pg10-11debug/data"
(gdb) p full_path
$4 = "/data/pg10-11debug/data/PG_VERSION\000\377\377\377\177\000\000P\331\377\377\377\177\000\000\343\277\265\000\000\000\000\000\211V\026\367\377\177\000\000P\337\377\377\377\177\000\000@\337\377\377\377\177\000\000\225a\263\000\000\000\000\000\362c\263\000\000\000\000\000\220a\263\000\000\000\000\000\211B\023\367\377\177\000\000\366\333\377\377\377\177\000\000\366\333\377\377\377\177\000\000\366\333\377\377\377\177\000\000\326\247\022\367\377\177\000\000\337\337\377\377\377\177\000\000\260\321\352\000\000\000\000\000\001\000\000\000\000\000\000\000H\336\377\377\377\177\000\000\000\000\000\000\000\000\000\000\300\332\377\377\377\177\000\000\200\332\377\377\377\177\000\000"...
(gdb) n
1374            if (!file)
(gdb) p file
$5 = (FILE *) 0xeb0bd0
(gdb) n
1388            file_version_string[0] = '\0';
(gdb) 
1389            ret = fscanf(file, "%63s", file_version_string);
(gdb) n
1390            file_major = strtol(file_version_string, &endptr, 10);
(gdb) p file_version_string
$10 = "11\000\000\000\000\000\000p\373\355\367\377\177\000\000\310\364\355\367\377\177\000\000&\323E\000\000\000\000\000x\335\017\367\377\177\000\000\260;A\000\000\000\000\000\000\000\000\000\001\000\000\000\350\001\000\000\001\000\000"
(gdb) p ret
$11 = 1
(gdb) n
1392            if (ret != 1 || endptr == file_version_string)
(gdb) p ret
$12 = 1
(gdb) p file_major
$13 = 11
(gdb) p file_version_string
$14 = "11\000\000\000\000\000\000p\373\355\367\377\177\000\000\310\364\355\367\377\177\000\000&\323E\000\000\000\000\000x\335\017\367\377\177\000\000\260;A\000\000\000\000\000\000\000\000\000\001\000\000\000\350\001\000\000\001\000\000"
(gdb) n
1401            FreeFile(file);
(gdb) 
1403            if (my_major != file_major)
(gdb) 
1404                    ereport(FATAL,
(gdb) 
2021-02-28 19:22:23.265 CST [11639] FATAL:  database files are incompatible with server
2021-02-28 19:22:23.265 CST [11639] DETAIL:  The data directory was initialized by PostgreSQL version 11, which is not compatible with this version 10.11.
[Inferior 1 (process 11639) exited with code 01]
(gdb) quit

查看$PGDATA/PG_VERSION确实为11,实例安装时是10.3,为什么PG_VERSION文件内容为11?

如果当前数据目录真的是11版本,除PG_VERSION之外,pg_control文件版本也应该是11,执行pg_controldata $PGDATA查看pg_control版本为1002(10版本的pg_control版本均为1002,取决于宏定义#define PG_CONTROL_VERSION 1002)。说明数据目录版本确实为10

有没有可能是代码某处问题,导致PG_VERSION重置为11?
分析了下源代码,排除了这个可能性。PG_VERSION文件产生于initdb过程,在initdb流程中,会向$PGDATA中写入这个文件,并写入版本号PG_MAJORVERSION(10版本统一为10,取决于宏定义#define PG_MAJORVERSION “10”)。涉及修改此文件的操作只有通过pg_upgrade进行升级,代码会根据版本修改该文件,该实例未曾使用该工具。

推测应该是DBA同学误操作,将PG_VERSION文件覆盖;或者是编辑重置为11,这是一个权限为0600的text文本类型文件。

问题原因分析清楚了,解决方案也很简单,将PG_VERSION文件内容修改为10,数据库可以正常启动。

这里还是存在漏洞的,从这个问题来看,postgres不希望有任何人去改动PG_VERSION文件内容,那么文件默认权限应该为当前用户只读,也就是0400。需要修改此文件时,例如pg_upgrade中可以先修改文件权限为0600,再去修改内容,最终继续修改为0400。

设置默认权限为0400代码改动如下:

/*
 * write out the PG_VERSION file in the data dir, or its subdirectory
 * if extrapath is not NULL
 */
static void
write_version_file(char *extrapath)
{
        FILE       *version_file;
        char       *path;

        if (extrapath == NULL)
                path = psprintf("%s/PG_VERSION", pg_data);
        else
                path = psprintf("%s/%s/PG_VERSION", pg_data, extrapath);

        if ((version_file = fopen(path, PG_BINARY_W)) == NULL)
        {
                fprintf(stderr, _("%s: could not open file \"%s\" for writing: %s\n"),
                                progname, path, strerror(errno));
                exit_nicely();
        }
        if (fprintf(version_file, "%s\n", PG_MAJORVERSION) < 0 ||
                fclose(version_file))
        {
                fprintf(stderr, _("%s: could not write file \"%s\": %s\n"),
                                progname, path, strerror(errno));
                exit_nicely();
        }
        /* Begin */
        /* 在这里增加了文件权限处理,修改文件权限为S_IRUSR,即0400 */
        if (chmod(path,S_IRUSR) < 0 )
        {
                fprintf(stderr, _("%s: could not chmod file \"%s\": %s\n"),
                                progname, path, strerror(errno));
                free(path);

                exit_nicely();
        }

        /* End */
        free(path);
}

修改后重新编译安装initdb程序,执行initdb

[postgres@postgres_primary:pg10.11:5403 ~]$initdb -D /data/pg10-11debug/data1
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C".
The default database encoding has accordingly been set to "SQL_ASCII".
The default text search configuration will be set to "english".

Data page checksums are disabled.

fixing permissions on existing directory /data/pg10-11debug/data1 ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting dynamic shared memory implementation ... posix
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

WARNING: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    pg_ctl -D /data/pg10-11debug/data1 -l logfile start

[postgres@postgres_primary:pg10.11:5403 ~]

文件权限为0400

[postgres@postgres_primary:pg10.11:5403 ~]$ll /data/pg10-11debug/data1/PG_VERSION 
-r-------- 1 postgres postgres 3 Feb 28 20:38 /data/pg10-11debug/data1/PG_VERSION
[postgres@postgres_primary:pg10.11:5403 ~]$cat /data/pg10-11debug/data1/PG_VERSION
10
[postgres@postgres_primary:pg10.11:5403 ~]$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值