從作業系統角度看錶空間計算方式

帶你聊技術發表於2023-11-27

來源: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),還會做
  1. 中斷檢查: CHECK_FOR_INTERRUPTS() 用於處理可能的中斷請求。
  2. 忽略特殊目錄: 跳過 "." 和 ".." 目錄。
  3. 構建完整路徑: 使用 snprintf 構建檔案或目錄的完整路徑。
  4. 檔案狀態檢查:使用 stat 獲取檔案或目錄的狀態。
  5. 如果 stat 失敗且錯誤不是 ENOENT(檔案不存在),則報錯。
  6. 累計大小:如果是目錄,遞迴計算目錄大小。最後,累加檔案大小到 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 的程式碼"巧妙"地處理了這個 "." 和 ".."。

小結

其實這個案例更多和作業系統有關,小結一下:

  1. 一個空資料夾,首先要消耗掉一個inode,具體大小取決於具體機器,然後加上一個 block
  2. 一個空檔案,並不消耗 block
  3. 目錄下的檔案/子目錄越多,目錄就需要申請越多的 block

em,一個有趣的案例 ~


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2997325/,如需轉載,請註明出處,否則將追究法律責任。

相關文章