暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 虚拟文件描述符

PolarDB 2025-04-25
224

PostgreSQL 虚拟文件描述符

背景

PostgreSQL 后端进程经常需要打开大量的文件,包括表文件、索引文件、临时文件(用于排序或构造 hash 表)等。由于操作系统允许一个进程能够打开的文件数量是有上限的,为了防止后端进程打开文件时因超出 OS 的限制而失败,PostgreSQL 提供了 虚拟文件描述符 (VFD) 机制。VFD 抽象层能够对使用 VFD API 的代码屏蔽对 OS 文件描述符的管理细节,使更高层的代码能够打开比 OS 限制的数量更多的文件。

设计

VFD 机制最核心的设计,就是使用一个 LRU (Least-Recently-Used) 缓存池来管理当前进程所有的 操作系统文件描述符,同时对更上层的代码暴露出 虚拟文件描述符 (Virtual File Descriptor) 供使用。理想情况下,除了 VFD 的内部实现,其它部分内核代码不应该直接调用 C 库函数去操作文件。当 PostgreSQL 后端进程需要打开一个文件,且此时进程已经打开的文件数量将要超过 OS 允许的最大数量时,VFD 从其管理的 OS 文件描述符中选出最近最久未被使用的文件描述符并关闭它,此时打开这个文件就不会被 OS 拒绝。这个过程对使用 VFD API 的更高层代码来说是无感知的,仿佛可以不受数量限制地打开文件一样。

虚拟文件描述符

PostgreSQL 中,所有的 VFD 在物理上被组织成一个数组。VfdCache
 是指向这个数组起始位置的指针,SizeVfdCache
 保存这个数组的长度。这个数组的长度会随着对 VFD 需求量的增加而动态扩容。

nfile
 变量记录了 VFD 数组中到底管理了多少个操作系统的文件描述符,这样 VFD 机制才能在打开的文件数量即将超出 OS 限制时,关闭最近最久未被使用的文件描述符。

/*
 * Virtual File Descriptor array pointer and size.  This grows as
 * needed.  'File' values are indexes into this array.
 * Note that VfdCache[0] is not a usable VFD, just a list header.
 */

static Vfd *VfdCache;
static Size SizeVfdCache = 0;

/*
 * Number of file descriptors known to be in use by VFD entries.
 */

static int  nfile = 0;

虚拟文件描述符的结构定义如下:

typedef struct vfd
{

    int         fd;             /* current FD, or VFD_CLOSED if none */
    unsigned short fdstate;     /* bitflags for VFD's state */
    ResourceOwner resowner;     /* owner, for automatic cleanup */
    File        nextFree;       /* link to next free VFD, if in freelist */
    File        lruMoreRecently;    /* doubly linked recency-of-use list */
    File        lruLessRecently;
    off_t       fileSize;       /* current size of file (0 if not temporary) */
    char       *fileName;       /* name of file, or NULL for unused VFD */
    /* NB: fileName is malloc'd, and must be free'd when closing the VFD */
    int         fileFlags;      /* open(2) flags for (re)opening the file */
    mode_t      fileMode;       /* mode to pass to open(2) */
} Vfd;

其中包括了 VFD 的状态信息:

  • fd
     用于保存真正的 OS 文件描述符,如果未使用,那么将会被设置为 VFD_CLOSED

  • fdstate
     保存了 VFD 的状态标志位

  • resowner
     表示这个 VFD 的持有者,方便后续的自动清理

此外,还包括 VFD 数组的管理信息。整个 VFD 数组被组织为两部分:LRU 池和空闲 VFD 列表。

LRU 池在逻辑上是一个双向链表,管理了所有正在持有 OS 文件描述符的 VFD。当一个 VFD 被使用后,它就会从 LRU 链表中移动到队头;此时 LRU 双向链表的尾部就是最近最久未被使用过的那个 VFD。

空闲 VFD 列表在逻辑上是一个单向链表。所有未被使用的 VFD 都会被串联在这个单链表中。被使用完毕释放的 VFD 也会被串回这个链表中。

上述两个部分虽然在逻辑上是双向或单向链表,但在形态上还是存放在 VFD 数组中,通过保存以下三个数组下标来代替链表本应该拥有的指针:

  • nextFree
     是下一个空闲 VFD 在数组中的下标,用于串联空闲 VFD 列表

  • lruMoreRecently
     是后一个被使用过的 VFD 在数组中的下标

  • lruLessRecently
     是前一个被使用过的 VFD 在数组中的下标

后两者相当于双向链表中的 prev
 和 next
 指针,用于串联 LRU 池。VFD 数组中的第一个元素 VfdCache[0]
 永远不使用,该元素中的空间将会被分别作为两个链表的头指针。

最后剩下的几个信息,是真正向操作系统打开文件时传入的参数:

  • fileSize
     表示文件大小

  • fileName
     表示文件名

  • fileFlags
     表示打开文件时的标志位

  • fileMode
     表示打开文件时的模式(权限)

低层函数

LruInsert Insert

LruInsert()
 函数会向 OS 申请真正打开 VFD 所对应的 OS 文件描述符,然后把这个 VFD 添加到 LRU 池的队头。LRU 池中的 VFD 是真正持有 OS 文件描述符的 VFD。把 VFD 移动到 LRU 池的队头是通过 Insert()
 函数实现的,它会修改 VFD 的 LRU 双向链表指针。

LruDelete Delete

LruDelete()
 函数会真正释放 VFD 所持有的 OS 文件描述符,然后将 VFD 从 LRU 池中移除,因为这个 VFD 已经不再持有 OS 文件描述符了。移除动作是由 Delete()
 函数实现的,它负责修改 VFD 的 LRU 指针,重新串联这个 VFD 之前和之后的 VFD。

ReleaseLruFile ReleaseLruFiles

这两个函数负责不断关闭 LRU 池中最近最久未被使用的 VFD 所持有的 OS 文件描述符,直到将 LRU 池中的 VFD 数量控制到 OS 允许的安全范围以下。具体的实现方式就是调用上面的 LruDelete()
 来关闭 OS 文件描述符并从 LRU 池中移除。

AllocateVfd FreeVfd

这两个函数负责在 VFD 数组中占用一个空闲的 VFD(但并不打开底层的 OS 文件),以及归还 VFD。

在分配 VFD 时,如果 VFD 数组的空闲链表已经为空,那么就需要使用 realloc()
 重新分配一个更大的 VFD 数组(通常是原 VFD 数组长度的两倍),并把新分配数组的后一半 VFD 初始化到空闲链表中以备未来使用。然后从空闲链表中摘下一个 VFD 并返回其下标。

归还 VFD 的过程很直接:将 VFD 恢复为初始状态,然后将其重新放回空闲链表中。

BasicOpenFile:Open 系统调用的替代者

这个函数封装了传统的 open()
 系统调用。理论上,PostgreSQL 内核的其它部分不应再直接使用 open()
 系统调用。这个函数将会返回一个裸的 OS 文件描述符,而不是 VFD——所以调用这个函数的代码需要保证这个文件描述符不会被泄露。

该函数内部真正调用了 open()
 来获取一个 OS 文件描述符。如果失败,那么将会试图从 LRU 池中删除一个已有的文件描述符,然后再度重试,直到成功为止。

FileAccess:访问文件

每个高层文件访问接口都会调用 FileAccess()
。调用这个函数意味着 VFD 持有的 OS 文件描述符将要被使用。那么:

  1. 如果这个文件在 OS 层面还没有被打开,那么调用 LruInsert()
     打开文件并将 VFD 插入 LRU 池

  2. 如果这个文件已被打开,那么先将 VFD 从 LRU 池中移除,然后将 VFD 插入到 LRU 池的队头,表示这个 VFD 最近刚刚被访问

高层文件操作接口

打开文件

PathNameOpenFilePerm()
 将会根据参数打开文件,并返回一个 VFD。经历了以下步骤:

  1. 调用 AllocateVfd()
     从 VFD 数组中拿到一个空闲 VFD 并初始化

  2. 调用 ReleaseLruFiles()
     把 LRU 池中的 VFD 减少到操作系统允许的水平

  3. 调用 BasicOpenFilePerm()
     打开一个 OS 文件描述符,并关联到新分配的 VFD 上

  4. 调用 Insert()
     把新分配的 VFD 添加到 LRU 池中

  5. 返回新分配的 VFD

关闭文件

FileClose()
 将会关闭一个 VFD 所对应的一切:

  1. 如果 VFD 对应的 OS 文件描述符已被打开,那么调用 close()
     关掉它,然后调用 Delete()
     从 LRU 池里移除这个 VFD

  2. 如果 VFD 对应的文件被设置了 关闭时删除 的标志(临时文件),那么调用 unlink()
     删掉它!

  3. 调用 FreeVfd()
     清空这个 VFD 并重新归还到 VFD 数组的空闲链表中

其它文件操作

除去文件的打开与关闭以外,其它文件操作需要基于 文件已被打开 的假设进行。这些文件操作都被 PostgreSQL 内核做了一层封装。在进行真正的文件操作之前,需要先使用打开文件后持有的 VFD 调用一次 FileAccess()
 函数。这个函数能够保证:

  1. VFD 内部持有的 OS 文件描述符已经打开(如果没打开,那就立刻打开,或许会导致其它 OS 文件描述符被关闭)

  2. VFD 在 LRU 池中的位置移动到队头,因为这个 VFD 对应的 OS 文件描述符最近被使用了

以文件库函数 read()
 的包装 FileRead()
 为例:

int
FileRead(File file, char *buffer, int amount, off_t offset,
         uint32 wait_event_info)

{
    int         returnCode;
    Vfd        *vfdP;

    Assert(FileIsValid(file));

    DO_DB(elog(LOG, "FileRead: %d (%s) " INT64_FORMAT " %d %p",
               file, VfdCache[file].fileName,
               (int64) offset,
               amount, buffer));

    returnCode = FileAccess(file);
    if (returnCode < 0)
        return returnCode;

    vfdP = &VfdCache[file];

retry:
    pgstat_report_wait_start(wait_event_info);
    returnCode = pread(vfdP->fd, buffer, amount, offset);
    pgstat_report_wait_end();

    if (returnCode < 0)
    {
        /* OK to retry if interrupted */
        if (errno == EINTR)
            goto retry;
    }

    return returnCode;
}

总结

PostgreSQL 内核中的 VFD 机制用于防止 PostgreSQL 后端进程受 OS 对进程打开文件数量的限制。VFD 内部维护了一个 LRU 池来管理所有被打开的 OS 文件描述符。使用 VFD 的高层接口来操作文件,就可以享受到 VFD 为我们屏蔽掉的文件描述符管理所带来的的便利。

VFD 的实现思想与操作系统的进程调度有些类似。OS 上的进程有成百上千个,而 CPU 只有一个(或几个)。从使用者的角度看,这些进程似乎都在同时执行,只有 OS 知道每一个时刻只有一个进程在一个 CPU 核心上运行;在 PostgreSQL 中,类似地,从 VFD 使用者的角度看,似乎能够同时持有远超操作系统数量限制的文件描述符,但只有 VFD 知道,每一个时刻打开的 OS 文件描述符数量必定小于操作系统对进程打开文件数量的限制。

一招瞒天过海。


文章转载自PolarDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论