从操作系统角度看表空间计算方式

来源:这里教程网 时间:2026-03-14 21:12:21 作者:

来源:PostgreSQL学徒

前言

今天在某个群里看到这样一个问题

有大佬知道下图中表空间变化的原因吗,我新创建了一个表空间,统计大小为0,我将一个表迁移到新表空间,再将表从新表空间迁移到其他表空间,再次统计新表空间的大小为4096,按道理也应该是0,个人认为是Linux的inode的元数据,刚好是一个块4096,大佬们怎么看?

这引起了我的兴趣,简单分析一下。

复现

复现方式很简单,将某张表挪到指定表空间再挪回去即可,大小是 4096 字节。

postgres=# create tablespace myspc location '/home/postgres/mytablespace';
CREATE TABLESPACE
postgres=# create table test(id int,info text);
CREATE TABLE
postgres=# insert into test select n,md5(random()::text) from generate_series(1,10) as n;
INSERT 0 10
postgres=# select pg_size_pretty(pg_tablespace_size('myspc'));
 pg_size_pretty 
----------------
 0 bytes
(1 row)
postgres=# alter table test set tablespace myspc ;
ALTER TABLE
postgres=# select pg_size_pretty(pg_tablespace_size('myspc'));
 pg_size_pretty 
----------------
 20 kB
(1 row)
postgres=# alter table test set tablespace pg_default ;
ALTER TABLE
postgres=# select pg_size_pretty(pg_tablespace_size('myspc'));
 pg_size_pretty 
----------------
 4096 bytes
(1 row)

现在表空间下面什么都没有了,用 du -sh 可以看到也有个 4KB 大小,看样子极有可能就是这个!

[postgres@xiongcc ~]$ ls -l mytablespace/PG_16_202307071/
total 0
[postgres@xiongcc ~]$ du -sh mytablespace/PG_16_202307071/
4.0K    mytablespace/PG_16_202307071/
[postgres@xiongcc ~]$ du -sh mytablespace/
8.0K    mytablespace/

让我们瞅瞅代码,确认一下。

源码分析

调用逻辑:pg_tablespace_size_oid → calculate_tablespace_size,calculate_tablespace_size 的代码很简单,寥寥十几行

/*
 * Calculate total size of tablespace. Returns -1 if the tablespace directory
 * cannot be found.
 */
static int64
calculate_tablespace_size(Oid tblspcOid)
{
 char  tblspcPath[MAXPGPATH];
 char  pathname[MAXPGPATH * 2];
 int64  totalsize = 0;
 DIR     *dirdesc;
 struct dirent *direntry;
 AclResult aclresult;
 /*
  * User must have privileges of pg_read_all_stats or have CREATE privilege
  * for target tablespace, either explicitly granted or implicitly because
  * it is default for current database.
  */
 if (tblspcOid != MyDatabaseTableSpace &&
  !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
 {
  aclresult = pg_tablespace_aclcheck(tblspcOid, GetUserId(), ACL_CREATE);
  if (aclresult != ACLCHECK_OK)
   aclcheck_error(aclresult, OBJECT_TABLESPACE,
         get_tablespace_name(tblspcOid));
 }
 if (tblspcOid == DEFAULTTABLESPACE_OID)
  snprintf(tblspcPath, MAXPGPATH, "base");
 else if (tblspcOid == GLOBALTABLESPACE_OID)
  snprintf(tblspcPath, MAXPGPATH, "global");
 else
  snprintf(tblspcPath, MAXPGPATH, "pg_tblspc/%u/%s", tblspcOid,
     TABLESPACE_VERSION_DIRECTORY);
 dirdesc = AllocateDir(tblspcPath);
 if (!dirdesc)
  return -1;
 while ((direntry = ReadDir(dirdesc, tblspcPath)) != NULL)
 {
  struct stat fst;
  CHECK_FOR_INTERRUPTS();
  if (strcmp(direntry->d_name, ".") == 0 ||
   strcmp(direntry->d_name, "..") == 0)
   continue;
  snprintf(pathname, sizeof(pathname), "%s/%s", tblspcPath, direntry->d_name);
  if (stat(pathname, &fst) < 0)
  {
   if (errno == ENOENT)
    continue;
   else
    ereport(ERROR,
      (errcode_for_file_access(),
       errmsg("could not stat file \"%s\": %m", pathname)));
  }
  if (S_ISDIR(fst.st_mode))
   totalsize += db_dir_size(pathname);
  totalsize += fst.st_size;
 }
 FreeDir(dirdesc);
 return totalsize;
}

大概流程如下:

检查用户是否有读取表空间的权限。如果用户没有足够的权限,函数会报错。
根据传入的 tblspcOid,构造表空间的路径。这部分考虑了默认表空间和全局表空间的特殊情况
使用 AllocateDir 打开表空间目录,并使用 ReadDir 遍历目录。对于每个目录项(direntry),还会做
    中断检查: CHECK_FOR_INTERRUPTS() 用于处理可能的中断请求。
    忽略特殊目录: 跳过 "." 和 ".." 目录。
    构建完整路径: 使用 snprintf 构建文件或目录的完整路径。
    文件状态检查:使用 stat 获取文件或目录的状态。
    如果 stat 失败且错误不是 ENOENT(文件不存在),则报错。
    累计大小:如果是目录,递归计算目录大小。最后,累加文件大小到 totalsize。

那让我们看下这 4KB 是什么大小,其实到这里各位已经知道答案了。

各位可以看到,该文件的 inode 是 2504420,st_size 是 4096,那么这个 inode 是什么呢?让我们也用 stat 命令看看,没错正是 "5" 这个目录,这个 5 就是数据库的 oid

[postgres@xiongcc PG_16_202307071]$ stat 5
  File: ‘5’
  Size: 4096            Blocks: 8          IO Block: 4096   directory
Device: fd01h/64769d    Inode: 2504420     Links: 2
Access: (0700/drwx------)  Uid: ( 1000/postgres)   Gid: ( 1000/postgres)
Access: 2023-11-25 22:54:15.269934346 +0800
Modify: 2023-11-25 22:52:00.034105361 +0800
Change: 2023-11-25 22:52:00.034105361 +0800
 Birth: -

因此这个 4096 字节就是这么来的。Linux 一切皆文件呐!

那么假如你将这个表挪回去,各位应该就能理解为什么计算出来是 20KB 了吧。

postgres=# select pg_size_pretty(pg_tablespace_size('myspc'));
 pg_size_pretty 
----------------
 20 kB
(1 row)
[root@xiongcc 5]# du -sh *
8.0K    58100
0       58101
8.0K    58102
[root@xiongcc 5]# du -sh ../5
20K     ../5

发散

其实这里还涉及到一些操作系统相关的知识,我们不妨思考一下:

为什么目录占用的空间是 4096?也就是本例的现象?
为什么空文件占用的空间却是 0?
如果空文件真占用 0 byte 空间,那么该文件的文件名、创建者以及权限等文件夹相关的信息都存到哪儿去了?
[postgres@xiongcc 5]$ touch empty.file   ---创建一个空文件
[postgres@xiongcc 5]$ du -sh empty.file   ---大小是0
0       empty.file

可以看到我们使用 du -sh 看到的大小是 0字节,但是注意,其实空文件大小并非是"0",而是 du -sh 的计算方式"欺骗"了我们,实际上仍会占用一个 inode 的大小。

[root@xiongcc 5]# df -i /
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      2621440 242516 2378924   10% /
[root@xiongcc 5]# touch empty.file
[root@xiongcc 5]# df -i /   ---inode加1
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      2621440 242517 2378923   10% /

具体 inode 大小可以通过 dumpe2fs 查看,可以看到是 256 字节 ????????

[root@xiongcc 5]# dumpe2fs -h /dev/vda1 | grep Inode
dumpe2fs 1.42.9 (28-Dec-2013)
Inode count:              2621440
Inodes per group:         8192
Inode blocks per group:   512
Inode size:               256

而对于目录,目录也是一个特殊的文件 (Linux一切皆文件~) 也会占用一个 inode + 一个 block size,至于 block size 也可以使用 dumpe2fs 查看,可以看到,正是 4096 字节。

[root@xiongcc 5]# dumpe2fs -h /dev/vda1 | grep 'Block size'
dumpe2fs 1.42.9 (28-Dec-2013)
Block size:               4096
[root@xiongcc PG_16_202307071]# stat 5
  File: ‘5’
  Size: 4096            Blocks: 8          IO Block: 4096   directory
Device: fd01h/64769d    Inode: 2504420     Links: 2
Access: (0700/drwx------)  Uid: ( 1000/postgres)   Gid: ( 1000/postgres)
Access: 2023-11-25 22:57:15.645308113 +0800
Modify: 2023-11-25 23:02:19.443251339 +0800
Change: 2023-11-25 23:02:19.443251339 +0800
 Birth: -

那么空目录为啥一开始就消耗block了呢,那是因为其必须默认带两个目录项 "." 和 ".." (至少一个 block,就和 PostgreSQL 的 block 一样,哪怕写 1 个字节也会分配一个数据块),可以看到 PostgreSQL 的代码"巧妙"地处理了这个 "." 和 ".."。

小结

其实这个案例更多和操作系统有关,小结一下:

    一个空文件夹,首先要消耗掉一个inode,具体大小取决于具体机器,然后加上一个 block
    一个空文件,并不消耗 block
    目录下的文件/子目录越多,目录就需要申请越多的 block

em,一个有趣的案例 ~

相关推荐