问题现象
使用备份集恢复实例后无法登录业务db,报错如下:
postgres=# \c test0703
connection to server on socket "/tmp/.s.PGSQL.5403" failed: FATAL: cannot connect to invalid database "test0703"
HINT: Use DROP DATABASE to drop invalid databases.
Previous connection kept
postgres=#
报错很明确,test0703是一个invalid database无法登录,建议我们删除该库。
我的目的就是使用备份集恢复这个数据库,结果恢复出一个无效数据库?命中BUG?
报错分析
不要慌,先看下为什么报错。
报错位于InitPostgres函数,由于database_is_invalid_form(datform)为true进入了报错逻辑。
void
InitPostgres(const char *in_dbname, Oid dboid,
const char *username, Oid useroid,
bits32 flags,
char *out_dbname)
{
/* 省略 */
if (database_is_invalid_form(datform))
{
ereport(FATAL,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot connect to invalid database \"%s\"", dbname),
errhint("Use DROP DATABASE to drop invalid databases."));
}
/* 省略 */
}
也就是说由于pg_database中查询到该db的datconnlimit为DATCONNLIMIT_INVALID_DB即-2,则登录报错。
#define DATCONNLIMIT_INVALID_DB -2
bool
database_is_invalid_form(Form_pg_database datform)
{
return datform->datconnlimit == DATCONNLIMIT_INVALID_DB;
}
查看pg_database确实如此。所以PG中定义datconnlimit为-2就是invalid database无法登录。
postgres=# select datname,datconnlimit from pg_database where datname='test0703';
datname | datconnlimit
----------+--------------
test0703 | -2
(1 row)
postgres=#
那么为什么datconnlimit会等于-2?
代码走读
从源代码得知只有在drop database的过程中才会设置datconnlimit为-2
把drop database的主要过程整理为流程图方便大家理解,当然这不是完整的流程,只是把我认为关键的部分呈现出来。

我们都知道drop database是一个不可逆的操作[1] ,理论上要么不删除,要么删除成功。
在exec_simple_query中start_xact_command隐式开启事务,看到这你可能会质疑,不是说不可逆吗,怎么还会放到事务中?别着急,继续往下看。
由于drop database是一个Utility statement,因此不会生成执行计划,是直接调用到对应的逻辑里执行的。流程图中绿色部分从dropdb函数开始就是drop database语句的核心逻辑。
step1:
在dropdb函数里,首先做了一些ACLcheck及login校验后对pg_database加RowExclusiveLock
step2:
使用inplace_update去修改当前 drop database的datconnlimit这个过程稍微详细的描述下。
systable_inplace_update_begin里对我们要修改的这条记录使用heap_inplace_lock即调用Lock_buffer给当前buffer_content加LWlock LW_EXCLUSIVE模式锁,这样其他进程都无法访问这条内容。
然后执行修改datform->datconnlimit = !!#ff0000 DATCONNLIMIT_INVALID_DB!!;
改完之后systable_inplace_update_finish释放锁LWlock释放,这样修改就“生效”了。什么?生效了?不是在事务里?事务还没提交,怎么生效的?
大家应该注意到了这里是inplace update,而并不是PG默认的多版本标记更新,默认的标记更新只有等当前事务提交,才对外可见。为什么这么设计呢?
我们先串完整个过程,后续会有答案。
然后进行XLogFlush把WAL及时刷下去。
step3:
CatalogTupleDelete删除pg_database当前database这条记录,这里是标准的heap_delete,目前在事务里,得等到commit后才“生效”。
所以事务其实保护的就是这里,我们某个database可不可见主要取决于pg_database里对应的记录可不可见。那么这部分是可逆的,在这个步骤事务提交就这条记录清理,事务回滚这条记录依然可见。
step4:
ok,下来就到了不可逆的操作了,DropDatabaseBuffers丢弃这个database对应修改过的buffer内容,并且触发一次checkpoint。
remove_dbtablespaces调用unlink删除该database目录下物理文件。
处理完毕table_close释放pg_database加的锁。
step5:
finish_xact_command提交事务,操作完毕,这个时候要drop 的database在pg_database中记录就看不到了。
流程走读完毕,那么看起来就是drop database具体的流程中有部分可逆的步骤,有大部分不可逆的步骤。其实也好理解,当一个操作有一个不可逆的步骤,那么这个操做本身就是不可逆的,这些不可逆的其实也就是无法支持事务的。
再回到drop database datname这个语句本身,执行结果就只有两种,要么删除了,要么不删除,我们肯定不接受删除了一半,或者删除了部分数据这种中间状态,这种情况下database也是无法使用的。
所以在一开始执行drop database,就使用了inplace update将database修改为invalid禁止登录。即便你在drop database中间状态备份了数据,使用这个备份恢复数据,或者访问数据库都无法访问。似乎是保证了drop database的“原子性”。
所以回到这个case,不是什么BUG,是PITR选择的时间点不对,应该再往前,选用更早备份结合增量WAL重新恢复。当前恢复的时间点,这个database正在被删除。
如果我的描述不太清晰,可以参考源代码[2] ,篇幅问题,这里我只粘贴一部分注释吧。
/*
* Except for the deletion of the catalog row, subsequent actions are not
* transactional (consider DropDatabaseBuffers() discarding modified
* buffers). But we might crash or get interrupted below. To prevent
* accesses to a database with invalid contents, mark the database as
* invalid using an in-place update.
*
* We need to flush the WAL before continuing, to guarantee the
* modification is durable before performing irreversible filesystem
* operations.
*/
代码跟踪
好人做到底,把debug关键过程截图下,不用麻烦大家自行验证了,有兴趣的也可以自行验证(切勿操作生产库,搞出异常和本文无关,博主表示接不住)
修改之前datconnlimit是-1,修改后为-2

其他会话查询也是一致。

执行CatalogTupleDelete删除这条信息时,由于还在事务中,其他会话还可以查询到这条记录。


当finish_xact_command提交事务后,其他会话也就查不到这条记录了。


结论
这不是什么BUG,是为了保证drop database动作的“原子性”,所以当PITR恢复的实例datconnlimit为-2时,说明恢复的时间点不正确,当前database正在被删除,需要选用更早备份结合增量WAL重新恢复。
当然datconnlimit可以从-2改为-1,直接修改pg_database即可(allow_alter_system必须为on,17版本default为on),但我不建议这么做,因为这样操作恢复的数据是不可靠的。
Reference:
[1] https://www.postgresql.org/docs/17/sql-dropdatabase.html
[2] https://github.com/postgres/postgres/blob/REL_17_STABLE/src/backend/commands/dbcommands.c#L1634C20-L1634C26




