前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgres 源码学习 2—Postgres 的 VFD 机制

Postgres 源码学习 2—Postgres 的 VFD 机制

作者头像
roseduan
发布2024-04-19 11:10:42
1280
发布2024-04-19 11:10:42
举报
文章被收录于专栏:roseduan写字的地方

操作系统中的文件

数据库的本质其实就是用来存储数据的,所以免不了和文件系统、存储进行交互,万丈高楼平地起,存储一般是一个数据库的最底层,Postgres 在存储的文件管理方面也有很多的设计与抽象。

在操作系统层面,提供了一些文件操作相关的系统调用(fopen、fclose、fsync 等),我们作为上层使用者,可以直接通过 C 语言库进行调用即可(Postgres 使用 C 语言编写)。

具体和文件系统的交互我们并不关心,操作系统打开文件之后,会在进程的控制块中维护一些打开文件的相关信息,并返回一个文件描述符,后续我们与文件的交互都通过文件描述符进行。

操作系统能够打开多少文件,是有限制的,一个是系统级限制,指的是在内核中可以打开多少文件,可以通过命令 sysctl fs.file-max 查看。另一个是用户级限制,为了不让某个进程打开太多的文件,进而消耗所有的资源,对单个进程能打开文件也有限制,可以通过 ulimit -n 命令查看。

Postgres 的 VFD 作用

Postgres 数据库在运行的过程当中,可能会打开非常多的文件,比如数据表对应的文件,元数据表文件,以及一些在 SQL 运行时打开的临时文件,例如排序、哈希表所需的文件。

所以有非常大的概率超过单个进程打开文件数量的限制,为了解决这个问题,Postgres 设计了 VFD(虚拟文件描述符)机制,主要是将实际的操作系统文件描述符维护到一个 LRU 缓存中,通过切换打开的方式,规避了进程打开文件数量的限制。

如果一个进程打开的文件数目达到了限制,则暂时关闭最久未使用的文件,保存其状态,待下次重新打开。

VFD 的基本工作方式

Postgres 主要通过一个进程私有的数组来维护 VFD,名为 VfdCache

代码语言:javascript
复制
/*
 * 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;

VfdCache 数组的第一个元素不存储任何数据,仅作为头部使用,下面是 vfdCache 的初始化逻辑,会在 backend 进程启动的时候调用,大致的逻辑就是为 VfdCache 数组分配内存。

代码语言:javascript
复制
/*
 * InitFileAccess --- initialize this module during backend startup
 *
 * This is called during either normal or standalone backend start.
 * It is *not* called in the postmaster.
 *
 * Note that this does not initialize temporary file access, that is
 * separately initialized via InitTemporaryFileAccess().
 */
void
InitFileAccess(void)
{
    Assert(SizeVfdCache == 0);  /* call me only once */

    /* initialize cache header entry */
    VfdCache = (Vfd *) malloc(sizeof(Vfd));
    if (VfdCache == NULL)
        ereport(FATAL,
                (errcode(ERRCODE_OUT_OF_MEMORY),
                 errmsg("out of memory")));

    MemSet((char *) &(VfdCache[0]), 0, sizeof(Vfd));
    VfdCache->fd = VFD_CLOSED;

    SizeVfdCache = 1;
}

如果需要打开一个文件,那么会首先在 VfdCache 数组中查找空闲的虚拟文件描述符,主要是通过 nextFree 指针进行查找,如果当前没有空闲的 vfd 了,那么会启动扩容机制,初始情况下,VfdCache size 是 32,每次扩容为原来的 2 倍。

Vfd 扩容和分配的逻辑都在方法 AllocateVfd 中。

代码语言:javascript
复制
static File
AllocateVfd(void)
{
    Index       i;
    File        file;

    DO_DB(elog(LOG, "AllocateVfd. Size %zu", SizeVfdCache));

    Assert(SizeVfdCache > 0);   /* InitFileAccess not called? */

    if (VfdCache[0].nextFree == 0)
    {
        /*
         * The free list is empty so it is time to increase the size of the
         * array.  We choose to double it each time this happens. However,
         * there's not much point in starting *real* small.
         */
        Size        newCacheSize = SizeVfdCache * 2;
        Vfd        *newVfdCache;

        if (newCacheSize < 32)
            newCacheSize = 32;

        /*
         * Be careful not to clobber VfdCache ptr if realloc fails.
         */
        newVfdCache = (Vfd *) realloc(VfdCache, sizeof(Vfd) * newCacheSize);
        if (newVfdCache == NULL)
            ereport(ERROR,
                    (errcode(ERRCODE_OUT_OF_MEMORY),
                     errmsg("out of memory")));
        VfdCache = newVfdCache;

        /*
         * Initialize the new entries and link them into the free list.
         */
        for (i = SizeVfdCache; i < newCacheSize; i++)
        {
            MemSet((char *) &(VfdCache[i]), 0, sizeof(Vfd));
            VfdCache[i].nextFree = i + 1;
            VfdCache[i].fd = VFD_CLOSED;
        }
        VfdCache[newCacheSize - 1].nextFree = 0;
        VfdCache[0].nextFree = SizeVfdCache;

        /*
         * Record the new size
         */
        SizeVfdCache = newCacheSize;
    }

    file = VfdCache[0].nextFree;

    VfdCache[0].nextFree = VfdCache[file].nextFree;

    return file;
}

拿到虚拟文件描述符之后,会调用 C 库函数 open 实际去打开文件,并且将一些文件状态维护到 Vfd 结构体中,这个结构体主要存储的是虚拟文件描述符的一些信息,也就是存储到 VfdCache 数组中的结构。

代码语言:javascript
复制
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 结构体中,主要通过 nextFree、lruMoreRecently、lruLessRecently 指针将 vfd 维护到不同的队列里面。

每次新打开一个文件,都会将该 vfd 通过 lruMoreRecently 和 lruLessRecently 指针,维护这个双向链表,每次关闭一个 VfdCache 中的文件,都会将其从链表中删除。

每次查找空闲的 VfdCache 的时候,都会通过 nextFree 链表进行查找。

img

以访问文件为例,首先会判断文件是否打开,如果没有打开的话,则打开文件并且将其放到最近使用的链表中。

主要的逻辑在函数 LruInsert 中,在实际打开文件之前,会尝试关闭最久未使用的文件。

然后会通过系统调用打开文件,并且获取到实际的文件描述符(fd),将其保存到 vfdP 结构中。

代码语言:javascript
复制
static int
LruInsert(File file)
{
    Vfd        *vfdP;

    Assert(file != 0);

    DO_DB(elog(LOG, "LruInsert %d (%s)",
               file, VfdCache[file].fileName));

    vfdP = &VfdCache[file];

    if (FileIsNotOpen(file))
    {
        /* Close excess kernel FDs. */
        ReleaseLruFiles();

        /*
         * The open could still fail for lack of file descriptors, eg due to
         * overall system file table being full.  So, be prepared to release
         * another FD if necessary...
         */
        vfdP->fd = BasicOpenFilePerm(vfdP->fileName, vfdP->fileFlags,
                                     vfdP->fileMode);
        if (vfdP->fd < 0)
        {
            DO_DB(elog(LOG, "re-open failed: %m"));
            return -1;
        }
        else
        {
            ++nfile;
        }
    }

    /*
     * put it at the head of the Lru ring
     */

    Insert(file);

    return 0;
}

如果文件已经是打开状态,那么会先从链表中删除,然后将其插入到最近使用的链表中。将 Vfd 加入到链表中,代码如下,可以看到主要是通过维护 lruMoreRecently 和 lruLessRecently 这两个指针,将当前 vfd 加入到链表的头部。

代码语言:javascript
复制
static void
Insert(File file)
{
    Vfd        *vfdP;

    Assert(file != 0);

    DO_DB(elog(LOG, "Insert %d (%s)",
               file, VfdCache[file].fileName));
    DO_DB(_dump_lru());

    vfdP = &VfdCache[file];

    vfdP->lruMoreRecently = 0;
    vfdP->lruLessRecently = VfdCache[0].lruLessRecently;
    VfdCache[0].lruLessRecently = file;
    VfdCache[vfdP->lruLessRecently].lruMoreRecently = file;

    DO_DB(_dump_lru());
}

而 Delete 方法则描述的是将一个 vfd 从链表中删除。

代码语言:javascript
复制
static void
Delete(File file)
{
    Vfd        *vfdP;

    Assert(file != 0);

    DO_DB(elog(LOG, "Delete %d (%s)",
               file, VfdCache[file].fileName));
    DO_DB(_dump_lru());

    vfdP = &VfdCache[file];

    VfdCache[vfdP->lruLessRecently].lruMoreRecently = vfdP->lruMoreRecently;
    VfdCache[vfdP->lruMoreRecently].lruLessRecently = vfdP->lruLessRecently;

    DO_DB(_dump_lru());
}

小结

Postgres 中的 VFD,即虚拟文件描述符,主要是为了能够规避操作系统中最大打开文件数的限制,采用切换打开的方式,维护了一个链表,将最近打开的文件维护到链表头部,最久未使用的文件放置到链表尾部。

访问文件的时候,会从 VfdCache 数组中查找空闲的虚拟文件描述符,如果找到的话,则直接使用,否则分配新的 VfdCache 空间。

在打开文件的时候,会尝试关闭最久未使用的文件,将位置留给最新打开的文件。

通过这种方式,Postgres 可以打开远超过系统和进程限制的文件数量,是一个非常精妙的设计。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 roseduan写字的地方 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 操作系统中的文件
  • Postgres 的 VFD 作用
  • VFD 的基本工作方式
  • 小结
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档