PostgreSQL vacuum 在不使用 full 的情况下,为什么有时也能回收空间

数据库技术
308
0
0
2024-04-02
标签   PostgreSQL

最近是不知道怎么回事,年底了自己的公司,群里都在关于磁盘的空间部分,MySQL怼完架构师,PostgreSQL 也让我想起曾经有一个资深的架构提出一个问题,PostgreSQL 不非要使用 vacuum full 就能回收空间的谣言,也让我给怼了一顿。所以今天说说这个问题,众所周知vauum full的

2024-01-10 01:24:00.771 EST [1575] psql 00000 client backend test VACUUM STATEMENT:  vacuum full test;
2024-01-10 01:24:00.771 EST [1491]  00000 stats collector   DEBUG:  received inquiry for database 58209
2024-01-10 01:24:00.771 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/global.stat"
2024-01-10 01:24:00.771 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/db_58209.stat"
2024-01-10 01:24:00.772 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/db_0.stat"
2024-01-10 01:24:00.795 EST [1487]  00000 background writer   DEBUG:  snapshot of 1+0 running transaction ids (lsn 7/5C0165A0 oldest xid 878886 latest complete 878885 next xid 878887)
2024-01-10 01:24:00.797 EST [1575] psql 00000 client backend test VACUUM DEBUG:  vacuuming "public.test"
2024-01-10 01:24:00.798 EST [1575] psql 00000 client backend test VACUUM DEBUG:  "test": found 0 removable, 3 nonremovable row versions in 1 pages
2024-01-10 01:24:00.798 EST [1575] psql 00000 client backend test VACUUM DETAIL:  0 dead row versions cannot be removed yet.
 CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
2024-01-10 01:24:00.798 EST [1575] psql 00000 client backend test VACUUM DEBUG:  drop auto-cascades to type pg_temp_58574
2024-01-10 01:24:00.798 EST [1575] psql 00000 client backend test VACUUM DEBUG:  drop auto-cascades to type pg_temp_58574[]
2024-01-10 01:24:00.805 EST [1575] psql 00000 client backend test VACUUM LOG:  duration: 34.487 ms
2024-01-10 01:24:01.030 EST [1576]  00000 autovacuum worker   DEBUG:  autovacuum: processing database "test"
2024-01-10 01:24:01.030 EST [1491]  00000 stats collector   DEBUG:  received inquiry for database 58209
2024-01-10 01:24:01.030 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/global.stat"
2024-01-10 01:24:01.030 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/db_58209.stat"
2024-01-10 01:24:01.030 EST [1491]  00000 stats collector   DEBUG:  writing stats file "pg_stat_tmp/db_0.stat"
2024-01-10 01:24:01.057 EST [1483]  00000 postmaster   DEBUG:  server process (PID 1576) exited with exit code 0

这里我们在PG14 版本中,运行一下这个命令,然后将PG的日志也模拟成MySQL 的genernal log 的方式,上面就是我们记录后整体的操作,这里蓝色的部分是我标记,其中主要的功能如下

在PG接受到你要进行vacuum full 操作的时候,他会针对你要操作的表的统计信息先进行数据的写入,并且要对这个表进行快照,来发现这个表是否正在被事务占用,并且要记录当前在使用他的事务的ID信息,如果此时没有事务对这个表进行操作,则他就开始针对表的一些物理特性进行分析比如到底有多少行,行版本中的live and dead 的情况。

同时会生成临时表来对数据进行周转,在周转完毕后临时表会被清理掉,然后在将刚才所做的镜像的信息恢复到新的表上,整体的处理完毕。

当然与其他数据库如optimize table 的mysql一样,如果此时表被其他的事务占用,比如在插入数据,那么此时vacuum full 会无法执行,或等待锁释放获得锁在进行,或直接在配置的等待锁超时的设置下,直接跳出执行失败。

不过说到这里还没有说到主题,就是为什么vacuum 有的时候也能达到vacuum full的功能,运行完毕,磁盘空间释放给操作系统。实际在PostgreSQL 操作中会对于vacuum 操作中调用freeSpaceMapVacuum中的函数来通过页面的偏移码来进行数据页面的释放,而vacuum本身会对页面的偏移码进行改变,因为每个页面都有最大偏移量的标记,这个部分在每个页面的最尾部存储本页的偏移量,而当vacuum 对于页面的偏移量进行更改后,会对于当前的数据文件进行判断是否调用释放空间的功能来释放空间,这里在调用中会会对于FSM文件来进行维护,对于页面空闲空间的数据的重新写入,并检查空间空间的位图。

所以如果通过vacuum 来操作表后,发现表空间被释放了,那说明你有效数据后面在合并数据块后,都是没有数据存在,没有数据存在就可以释放页尾后面的数据空间,所以拜托某些“架构师” 不要在说 vacuum 也能释放空间,是的他能但是你说的那个能你说的他能就差你买一个500万的彩票。

下面是vacuum.c 和 freespace.c 两个关于执行vacuum也能释放空间的部分代码。

下面这段代码的大致注释:

1 在客户运行vacuum 命令时根据参数来判断输入的参数并根据参数判断是 vacuum full or 其他,并且开启一个事务,用vacuum open relation 的函数,获取相关表结构,并且针对命令来对相关的表进行加锁的工作,不同的模式使用不同的锁来应对,在此还需要判断当前操作的用户是否对表有权限操作,并且判断表的类型是否是用户表等不是临时表,如果这些都不符合则自动报错退,但如果是分区表则会降级为 vacuum analyze 的操作,基于分区表的一些特性,是不能对根表进行除analyze 以外的操作。更多详细操作还请参看源代码,相关代码为pg14 代码与网上展示的代码可能有出入。

2 FSM 部分代码是一个实现空间映射搜索的函数,通过一个循环从FSM根地址进行搜索空闲的空间,通过将FSM 读取到内存缓冲区的方式,用fsm_readbuffer的函数来对表进行扫描,在上传后,对于上传你的部分进行一个锁定,此时不能进行DDL 相关的操作,并且通过fsm_search_avail来鉴别空闲的位置,最终确定 fsm_get_max_avail 函数来确认缓冲区中最大的可用的空闲的空间,周而复始的,遍历完毕。

vacuum.c

static bool
vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
{
 LOCKMODE lmode;
 Relation rel;
 LockRelId lockrelid;
 Oid   toast_relid;
 Oid   save_userid;
 int   save_sec_context;
 int   save_nestlevel;

 Assert(params != NULL);

 /* Begin a transaction for vacuuming this relation */
 StartTransactionCommand();

 if (!(params->options & VACOPT_FULL))
 {
  
  LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
  MyProc->statusFlags |= PROC_IN_VACUUM;
  if (params->is_wraparound)
   MyProc->statusFlags |= PROC_VACUUM_FOR_WRAPAROUND;
  ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;
  LWLockRelease(ProcArrayLock);
 }


 PushActiveSnapshot(GetTransactionSnapshot());


 CHECK_FOR_INTERRUPTS();

 
 lmode = (params->options & VACOPT_FULL) ?
  AccessExclusiveLock : ShareUpdateExclusiveLock;


 rel = vacuum_open_relation(relid, relation, params->options,
          params->log_min_duration >= 0, lmode);


 if (!rel)
 {
  PopActiveSnapshot();
  CommitTransactionCommand();
  return false;
 }


 if (!vacuum_is_relation_owner(RelationGetRelid(rel),
          rel->rd_rel,
          params->options & VACOPT_VACUUM))
 {
  relation_close(rel, lmode);
  PopActiveSnapshot();
  CommitTransactionCommand();
  return false;
 }


 if (rel->rd_rel->relkind != RELKIND_RELATION &&
  rel->rd_rel->relkind != RELKIND_MATVIEW &&
  rel->rd_rel->relkind != RELKIND_TOASTVALUE &&
  rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
 {
  ereport(WARNING,
    (errmsg("skipping \"%s\" --- cannot vacuum non-tables or special system tables",
      RelationGetRelationName(rel))));
  relation_close(rel, lmode);
  PopActiveSnapshot();
  CommitTransactionCommand();
  return false;
 }


 if (RELATION_IS_OTHER_TEMP(rel))
 {
  relation_close(rel, lmode);
  PopActiveSnapshot();
  CommitTransactionCommand();
  return false;
 }


 if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 {
  relation_close(rel, lmode);
  PopActiveSnapshot();
  CommitTransactionCommand();
  /* It's OK to proceed with ANALYZE on this table */
  return true;
 }


 lockrelid = rel->rd_lockInfo.lockRelId;
 LockRelationIdForSession(&lockrelid, lmode);


 if (params->index_cleanup == VACOPTVALUE_UNSPECIFIED)
 {
  StdRdOptIndexCleanup vacuum_index_cleanup;

  if (rel->rd_options == NULL)
   vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
  else
   vacuum_index_cleanup =
    ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;

  if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
   params->index_cleanup = VACOPTVALUE_AUTO;
  else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
   params->index_cleanup = VACOPTVALUE_ENABLED;
  else
  {
   Assert(vacuum_index_cleanup ==
       STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
   params->index_cleanup = VACOPTVALUE_DISABLED;
  }
 }


 if (params->truncate == VACOPTVALUE_UNSPECIFIED)
 {
  if (rel->rd_options == NULL ||
   ((StdRdOptions *) rel->rd_options)->vacuum_truncate)
   params->truncate = VACOPTVALUE_ENABLED;
  else
   params->truncate = VACOPTVALUE_DISABLED;
 }


 if ((params->options & VACOPT_PROCESS_TOAST) != 0 &&
  (params->options & VACOPT_FULL) == 0)
  toast_relid = rel->rd_rel->reltoastrelid;
 else
  toast_relid = InvalidOid;


 GetUserIdAndSecContext(&save_userid, &save_sec_context);
 SetUserIdAndSecContext(rel->rd_rel->relowner,
         save_sec_context | SECURITY_RESTRICTED_OPERATION);
 save_nestlevel = NewGUCNestLevel();

 
 if (params->options & VACOPT_FULL)
 {
  ClusterParams cluster_params = {0};

  /* close relation before vacuuming, but hold lock until commit */
  relation_close(rel, NoLock);
  rel = NULL;

  if ((params->options & VACOPT_VERBOSE) != 0)
   cluster_params.options |= CLUOPT_VERBOSE;

  /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
  cluster_rel(relid, InvalidOid, &cluster_params);
 }
 else
  table_relation_vacuum(rel, params, vac_strategy);


 AtEOXact_GUC(false, save_nestlevel);


 SetUserIdAndSecContext(save_userid, save_sec_context);


 if (rel)
  relation_close(rel, NoLock);

 
 PopActiveSnapshot();
 CommitTransactionCommand();

 
 if (toast_relid != InvalidOid)
  vacuum_rel(toast_relid, NULL, params);

 
 UnlockRelationIdForSession(&lockrelid, lmode);


 return true;
}

freespace.c

static BlockNumber
fsm_search(Relation rel, uint8 min_cat)
{
 int   restarts = 0;
 FSMAddress addr = FSM_ROOT_ADDRESS;

 for (;;)
 {
  int   slot;
  Buffer  buf;
  uint8  max_avail = 0;

 
  buf = fsm_readbuf(rel, addr, false);


  if (BufferIsValid(buf))
  {
   LockBuffer(buf, BUFFER_LOCK_SHARE);
   slot = fsm_search_avail(buf, min_cat,
         (addr.level == FSM_BOTTOM_LEVEL),
         false);
   if (slot == -1)
    max_avail = fsm_get_max_avail(BufferGetPage(buf));
   UnlockReleaseBuffer(buf);
  }
  else
   slot = -1;

  if (slot != -1)
  {
  
   if (addr.level == FSM_BOTTOM_LEVEL)
    return fsm_get_heap_blk(addr, slot);

   addr = fsm_get_child(addr, slot);
  }
  else if (addr.level == FSM_ROOT_LEVEL)
  {
 
   return InvalidBlockNumber;
  }
  else
  {
   uint16  parentslot;
   FSMAddress parent;


   parent = fsm_get_parent(addr, &parentslot);
   fsm_set_and_search(rel, parent, parentslot, max_avail, 0);

   
   if (restarts++ > 10000)
    return InvalidBlockNumber;

  
   addr = FSM_ROOT_ADDRESS;
  }
 }
}