PostgreSQL学徒

小案例一则,PG 中的延迟创建

前言

这两天在研究一个客户问题的时候,意外发现了 PG/GP 中的"懒创建" —— 临时文件所在目录并非初始化时便会创建,而是按需创建,或许是为了提升启动速度、降低文件系统噪音,也可能是避免初始化阶段创建一堆无用的目录,让我们一起瞅瞅这个小案例。

现象

对于 PG,默认情况下,产生的临时文件会位于 base 的 pgsql_tmp 子目录下,以 pid 为前缀,会话结束后 (安全退出) 自动释放,crash 之后是否保留临时文件则取决于 remove_temp_files_after_crash 参数。Greenplum 就是大号的 PG,GP 中有类似 work_mem 的 statement_mem 参数,不过是会话级的内存限制,即控制整个会话可以使用的内存,而不是算子级,查询所需内存超过 statement_mem 那么就溢出到磁盘,和 PG 是相同的原理。

但是以这个 GP 环境为例,可以看到 Segment 的 base 目录下并没有 pgsql_tmp 这个目录,莫非 GP 与 PG 有所不同,放在了其他路径之下?

[mxadmin@segment1 base]$ ll
total 128
drwx------ 2 mxadmin mxadmin  8192 Oct 13 16:30 1
drwx------ 2 mxadmin mxadmin  8192 Oct 28 01:11 14010
drwx------ 2 mxadmin mxadmin 12288 Oct 29 00:32 14011
drwx------ 2 mxadmin mxadmin 40960 Oct 29 15:26 18653
drwx------ 2 mxadmin mxadmin  8192 Oct 27 11:04 49152
[mxadmin@segment1 base]$ pwd
/home/mxdata_20251013162933/primary/mxseg0/base

不过咨询了下研发同事,经过 DEBUG,发现确实也在 pgsql_tmp 目录下。

那么问题来了,为啥我这个环境没有这个目录,会不会是动态按需创建?为此,我特意去重新初始化了一个 PG

[postgres@mypg ~]$ ll pgdata/base/
total 12
drwx------ 2 postgres postgres 4096 Oct 29 17:48 1
drwx------ 2 postgres postgres 4096 Oct 29 17:48 4
drwx------ 2 postgres postgres 4096 Oct 29 17:48 5

可以看到,PG 也没有此目录。大概翻了下代码,找到了背后原因:

/*
 * Return the path of the temp directory in a given tablespace.
 */

void
TempTablespacePath(char *path, Oid tablespace)
{
/*
  * Identify the tempfile directory for this tablespace.
  *
  * If someone tries to specify pg_global, use pg_default instead.
  */

if (tablespace == InvalidOid ||
  tablespace == DEFAULTTABLESPACE_OID ||
  tablespace == GLOBALTABLESPACE_OID)
snprintf(path, MAXPGPATH, "base/%s", PG_TEMP_FILES_DIR);
else
 {
/* All other tablespaces are accessed via symlinks */
snprintf(path, MAXPGPATH, "pg_tblspc/%u/%s/%s",
     tablespace, TABLESPACE_VERSION_DIRECTORY,
     PG_TEMP_FILES_DIR);
 }
}

/* Filename components */
#define PG_TEMP_FILES_DIR "pgsql_tmp"
#define PG_TEMP_FILE_PREFIX "pgsql_tmp"

临时文件就两个路径,一种情况是 base/pgsql_tmp,另外一种是指定了临时表空间时,pg_tblspc/<ts_oid>/<PG_VERSION_DIR>/pgsql_tmp。然后便是打开临时文件的逻辑

  1. 第一次创建文件时,若 base/pgsql_tmp 目录还不存在,那么PathNameOpenFile() 会失败;
  2. 然后执行 (void) MakePGDirectory(tempdirpath);,内部就是 mkdir(path, 0700);
  3. 目录创建后,再次尝试创建文件
/*
 * Open a temporary file in a specific tablespace.
 * Subroutine for OpenTemporaryFile, which see for details.
 */

static File
OpenTemporaryFileInTablespace(Oid tblspcOid, bool rejectError)
{
char  tempdirpath[MAXPGPATH];
char  tempfilepath[MAXPGPATH];
 File  file;

  ...

/*
  * Open the file.  Note: we don't use O_EXCL, in case there is an orphaned
  * temp file that can be reused.
  */

 file = PathNameOpenFile(tempfilepath,
       O_RDWR | O_CREAT | O_TRUNC | PG_BINARY);
if (file <= 0)
 {
/*
   * We might need to create the tablespace's tempfile directory, if no
   * one has yet done so.
   *
   * Don't check for an error from MakePGDirectory; it could fail if
   * someone else just did the same thing.  If it doesn't work then
   * we'll bomb out on the second create attempt, instead.
   */

  (void) MakePGDirectory(tempdirpath);

  file = PathNameOpenFile(tempfilepath,
        O_RDWR | O_CREAT | O_TRUNC | PG_BINARY);
if (file <= 0 && rejectError)
   elog(ERROR, "could not create temporary file \"%s\": %m",
     tempfilepath);
 }

return file;
}

复现

既然知晓了逻辑,随便跑个 SRF,超过 work_mem 即可复现:

[postgres@mypg base]$ ls -l
total 12
drwx------ 2 postgres postgres 4096 Oct 29 18:16 1
drwx------ 2 postgres postgres 4096 Oct 29 17:48 4
drwx------ 2 postgres postgres 4096 Oct 29 18:17 5

postgres=# explain analyze SELECT generate_series(1,10000000) AS id ORDER BY id DESC;               
                                                    QUERY PLAN                                      

----------------------------------------------------------------------------------------------------
--------------
 Sort  (cost=1486115.85..1511115.85 rows=10000000 width=4) (actual time=2829.537..3602.655 rows=1000
0000 loops=1)
   Sort Key: (generate_series(1, 10000000)) DESC
   Sort Method: external merge  Disk: 117528kB
   ->  ProjectSet  (cost=0.00..50000.02rows=10000000 width=4) (actual time=0.006..687.297rows=1000
0000 loops=1)
         ->  Result  (cost=0.00..0.01rows=1 width=0) (actual time=0.003..0.004rows=1 loops=1)
 Planning Time: 0.036 ms
 Execution Time: 4079.191 ms
(7rows)

[postgres@mypg base]$ ls -l
total 16
drwx------ 2 postgres postgres 4096 Oct 29 18:16 1
drwx------ 2 postgres postgres 4096 Oct 29 17:48 4
drwx------ 2 postgres postgres 4096 Oct 29 18:17 5
drwx------ 2 postgres postgres 4096 Oct 29 18:21 pgsql_tmp

小结

  1. 创建时机:只有当第一次需要临时文件 (例如 work_mem 不够、大排序/大哈希发生落盘、或某些临时对象需要落盘) 时,后端在打开临时文件的过程中才确保目录存在并创建文件,因此新库/新实例刚初始化时看不到此目录,PG 和 GP 同样逻辑。

  2. 创建/命名/清理由 fd.c 的 OpenTemporaryFile* 系列与 postmaster 启动清理流程共同完成。