前言
如果没有合理的业务设计,vacuum full或将是PG用户一个无法绕开的痛点。特别是对于DBA而言,可能会带来不小的困扰。
正如我在“PostgreSQL数据库技术峰会西安站”中分享的一样,当我们处理大表vacuum full时,由于一些客观因素,磁盘监控告警没有达到预期,很可能会出现vacuum full写满磁盘的情况,这个时候大概率要背锅。
因此,在我看来内核自身支持vacuum full空间预检查是个不错的主意。
vacuum full原理
在设计方案之前,我们回顾下为什么PG中需要vacuum full?
借用下interdb对应章节中的相关内容:
标记删除特性,vacuum并不释放空间,当vacuum效率低于dead_tuple产生速率就会出现“表膨胀”,造成空间浪费同时影响性能。这个时候就需要vacuum full来处理,当然也可以使用各种扩展,选择vacuum full是因为它足够“简单”。
vacuum full原理简介:
1)创建新表
2)将live tuples数据copy到新表
3)清理old file,更新FSM和VM
vacuum full主要流程:
方案设计
1)设计一个guc参数控制是否开启预检查特性
2)计算表总大小(包含索引、toast),获取表所在表空间物理路径(对于软链接的处理,返回目标链接所在路径)这里可以照猫画虎新增c函数(参考sql函数,修改入参方式和返回值等,总之微调)或者是SPI方式去调用这些sql函数。我选择了前者。
3)当表大小大于对应分区剩余空间大小时报错终止vacuum full
逻辑看起来很简单,那么这段逻辑放在什么位置合适呢?
放在vacuum加AccessExclusiveLock之后的话,计算表大小时耗时相对久一点,加锁时间会久;放在加锁之前表大小可能计算不准确?
好吧,我暂且先放在加锁前吧。
代码修改
总览:涉及6个文件,几百行。
主要逻辑:
/*
* Begin at 2024-07-24
* Add by NickYoung
* check the free space of tablespace's file system, when we run vacuum full or cluster (table) command
*/
if (vacuum_full_check_space)
{
char *filepath;
char *filepatha;
char *filepathb;
int64 tablesize;
struct config_generic *record;
const char *name = "data_directory";
char *pgdata;
char *tbspath;
char *tbsoidstr;
Oid tbsoid;
char *tabfilepath;
char *tbsfs;
char *tbsfslink;
struct statvfs stat;
unsigned long totalspace;
unsigned long freespace;
/* Get the relative path of the table file from the pgdata directory */
filepath = pg_rel_filepath(tableOid);
filepatha = pstrdup(filepath);
filepathb = pstrdup(filepath);
/* Calculate table size */
tablesize = pg_total_rel_size(tableOid);
/* Get data_directory configuration */
record = find_option(name, false, false, ERROR);
pgdata = ShowGUCOption(record, true);
tbspath = subpathstring(filepatha,1);
tbsoidstr = subpathstring(filepathb,2);
tbsoid = atoi(tbsoidstr);
/* If the table in a custom tablespace */
if (strcmp(tbspath,"pg_tblspc") == 0)
{
tbsfs = pg_tbs_location(tbsoid);
tbsfslink = pg_dir_islink(tbsfs);
}
/* If the table in pg_global tablespace */
else if (strcmp(tbspath,"global") == 0)
{
tbsfs = psprintf("%s/%s",pgdata,"global");
tbsfslink = pg_dir_islink(tbsfs);
}
/* If the table in pg_default tablespace */
else
{
tbsfs = pg_dir_islink(pgdata);
/* If the PGDATA directory is not a link, check PGDATA/base directory */
if (strcmp(pgdata,tbsfs) == 0)
{
tabfilepath = psprintf("%s/%s",pgdata,"base");
tbsfs = pg_dir_islink(tabfilepath);
/* If the PGDATA/base directory is not a link, check PGDATA/base/databaseOid directory */
if (strcmp(tabfilepath,tbsfs) == 0)
{
tabfilepath = psprintf("%s/%s",tabfilepath,tbsoidstr);
tbsfs = pg_dir_islink(tabfilepath);
}
}
tbsfslink = pstrdup(tbsfs);
}
/* check the free space of tablespace on it's file system */
if (statvfs(tbsfslink, &stat) != 0)
{
ereport(ERROR,
(errcode(ERRCODE_SYSTEM_ERROR),
errmsg("call statvfs() failed")));
}
totalspace = stat.f_blocks * stat.f_frsize;
freespace = stat.f_bfree * stat.f_frsize;
/* if freespace less than tablesize then report the error */
if (freespace <= tablesize)
{
ereport(ERROR,
(errcode(ERRCODE_DISK_FULL),
errmsg("check failed! The free space on the device is not enough.\n"
"\ttablesize:\"%ld B\" is bigger than tbsfs:\"%s\" freespace:\"%ld B\"",
tablesize,tbsfslink,freespace)));
}
else
{
ereport(DEBUG1,
errmsg("check passed. The free space on the device is enough.\n"
"\ttbsfs:\"%s\" freespace:\"%ld B\" is bigger than tablesize:\"%ld B\" ",
tbsfslink,freespace,tablesize));
}
}
/*
* End at 2024-07-24 AM
*/
/*
* We grab exclusive access to the target rel and index for the duration
* of the transaction. (This is redundant for the single-transaction
* case, since cluster() already did it.) The index lock is taken inside
* check_index_is_clusterable.
*/
OldHeap = try_relation_open(tableOid, AccessExclusiveLock);
复制
测试验证
场景一:磁盘空间充足
关闭参数时不进行空间预检查。
打开参数时,可以看到有打印check passed. The free space on the device is enough.
场景二:磁盘空间不足
表大小13657137152B,表所在路径为软连接/baselink/base,对应/分区可用空间为12631183360B
因此报错可用空间不足,终止vacuum full。
小结
支持了磁盘预检查逻辑,即便监控告警失效也不用担心vacuum full会写满磁盘。这样能很大程度减轻DBA的压力,执行vacuum full不用时刻关注磁盘空间,减少背锅。
当然也期待PG后续可以对文件存储进一步深入优化,例如大文件段的管理,更智能的空间管理等。
评论
