有没有人听说过(制作技巧)基于本地硬盘驱动器的 NFS 缓存?(主要在 FreeBSD 中)

Gun*_*dow 10 freebsd cache nfs unionfs

问题:当通过 NFS 启动二进制文件(例如 /usr/bin)时,例如在网络引导系统中,NFS 可能会很慢。RAM 缓冲区缓存可能不足以避免缓慢。

想法:似乎我们应该能够拥有一个本地磁盘缓存,它可以在从 NFS 中提取文件时将文件保存在本地。

问题:有没有人在任何 UNIX 系统上看到过类似的东西?

背景:

在 FreeBSD 中,有很多使用 unionfs 来构建惊人的堆叠文件系统的好方法。我目前在 AWS 上有一个系统,它只使用 1 GB 的磁盘,因为它通过 NFS 挂载了大部分 /usr 文件系统树。在过去,您可以轻松做到这一点,因为 /usr 不是基本引导所必需的。现在它更难了(尤其是在 AWS 上,在启动失败时你无法跳出控制台)但我通过从本地驱动器上的 /usr 树中获取最少必要的东西来进行管理,然后,当网络启动时,我在 /usr 树上挂载 NFS。

我什至有一个后门,我仍然可以在其中写入底层最小本地硬盘驱动器 /usr 树,以防我需要更新正在运行的系统上的某些内容。

真漂亮。

除了 NFS (Amazon EFS) 非常慢。并且缓冲区缓存工作得不够好。例如,用于管理 AWS 资源的 aws 命令行界面使用 Python,每次调用 aws 命令时都会吸入大量包含。运行一个简单的 aws CLI 命令需要 20 秒。即使重复运行它,您也会认为缓存、NFS 属性缓存等可能会有所帮助,但事实并非如此。

可能的解决方案(在 FreeBSD 上):

所以我想做的是在 NFS 层之上放置另一个 unionfs 层,它是一个基于本地磁盘的 UFS 文件系统。但它会在启动时开始为空,然后,每次我们从 NFS 加载任何内容时(假设现在它是稳定的二进制文件,而不是动态更新的数据),它会在磁盘上留下一个副本。

此解决方案的实施:

所以这就是我认为应该做的。在/usr/src/sys/fs/unionfs/union_vnops.c我们有这个非常简单的代码:

static int
unionfs_open(struct vop_open_args *ap)
{
    ...
    if (targetvp == NULLVP) {
        if (uvp == NULLVP) {
            if ((ap->a_mode & FWRITE) && lvp->v_type == VREG) {
                error = unionfs_copyfile(unp,
                    !(ap->a_mode & O_TRUNC), cred, td);
                if (error != 0)
                    goto unionfs_open_abort;
                targetvp = uvp = unp->un_uppervp;
            } else
                targetvp = lvp;
        } else
            targetvp = uvp;
    }
Run Code Online (Sandbox Code Playgroud)

如果我们正在访问一个(ap->a_mode & FWRITE)仅在下层进行写入的文件,这部分将在上层进行复制(uvp == NULLVP) && lvp->v_type == VREG

想要尝试添加一个功能来为每个文件创建一个副本,即使是只读访问,这似乎很简单。然后它也会制作该副本,下次我们将从磁盘读取该文件。

为此,我将在/usr/src/sys/fs/unionfs/union.h 中添加一个新选项,我将添加一个新选项,即复制策略:

/* copy policy of upper layer */
typedef enum _unionfs_copypolicy {
       UNIONFS_COPY_ON_WRITE = 0,
       UNIONFS_COPY_ALWAYS
} unionfs_copypolicy;

struct unionfs_mount {
    struct vnode   *um_lowervp; /* VREFed once */
    struct vnode   *um_uppervp; /* VREFed once */
    struct vnode   *um_rootvp;  /* ROOT vnode */
    unionfs_copypolicy um_copypolicy;
    unionfs_copymode um_copymode;
    unionfs_whitemode um_whitemode;
    uid_t       um_uid;
    gid_t       um_gid;
    u_short     um_udir;
    u_short     um_ufile;
};
Run Code Online (Sandbox Code Playgroud)

坦率地说,我想将所有这些模式作为空间的位域来处理。无论如何,有了这个,我现在可以将上面的代码更改为:

unp = VTOUNIONFS(ap->a_vp);
ump = MOUNTTOUNIONFSMOUNT(ap->a_vp->v_mount);
...

    if (targetvp == NULLVP) {
        if (uvp == NULLVP) {
            if (((ap->a_mode & FWRITE) || (ump->um_copypolicy == UNIONFS_COPY_ALWAYS)) && lvp->v_type == VREG) {
                error = unionfs_copyfile(unp,
                    !(ap->a_mode & O_TRUNC), cred, td);
                if (error != 0)
                    goto unionfs_open_abort;
                targetvp = uvp = unp->un_uppervp;
Run Code Online (Sandbox Code Playgroud)

这应该是所有需要的。也就是说,希望所有处理属性和影子目录的事情都从函数 unionfs_copyfile 内部处理,正如它应该的那样。

现在在这种情况下,我们只需要将新的 copy-on-read 策略选项添加到 mount_unionfs 中,它也很好地位于内核模块中/usr/src/sys/fs/unionfs/union_vfsops.c

static int
unionfs_domount(struct mount *mp)
{
    int     error;
    ...
    u_short     ufile;
    unionfs_copypolicy copypolicy;
    unionfs_copymode copymode;
    unionfs_whitemode whitemode;
    ...
    ufile = 0;
    copypolicy = UNIONFS_COPY_ON_WRITE; /* default */
    copymode = UNIONFS_TRANSPARENT; /* default */
    whitemode = UNIONFS_WHITE_ALWAYS;
    ...
        if (vfs_getopt(mp->mnt_optnew, "copypolicy", (void **)&tmp,
            NULL) == 0) {
            if (tmp == NULL) {
                vfs_mount_error(mp, "Invalid copy policy");
                return (EINVAL);
            } else if (strcasecmp(tmp, "always") == 0)
                copypolicy = UNIONFS_COPY_ALWAYS;
            else if (strcasecmp(tmp, "onwrite") == 0)
                copypolicy = UNIONFS_COPY_ON_WRITE;
            else {
                vfs_mount_error(mp, "Invalid copy policy");
                return (EINVAL);
            }
        }

        if (vfs_getopt(mp->mnt_optnew, "copymode", (void **)&tmp,
            ...
        }
        if (vfs_getopt(mp->mnt_optnew, "whiteout", (void **)&tmp,
            ...
        }
    }
    ...

    UNIONFSDEBUG("unionfs_mount: uid=%d, gid=%d\n", uid, gid);
    UNIONFSDEBUG("unionfs_mount: udir=0%03o, ufile=0%03o\n", udir, ufile);
    UNIONFSDEBUG("unionfs_mount: copypolicy=%d, copymode=%d, whitemode=%d\n", copypolicy, copymode, whitemode);
Run Code Online (Sandbox Code Playgroud)

所以,这将在 FreeBSD 中完成我想要的,我现在需要获取我系统的源代码,应用此补丁,重新编译 unionfs.ko 内核模块并将其交换到我的系统中,看看它是否可以工作。

# Custom /etc/fstab for FreeBSD VM images
/dev/gpt/rootfs  /        ufs      rw      1       1
/dev/gpt/varfs   /var     ufs      rw      1       1
fdesc            /dev/fd  fdescfs  rw      0       0
proc             /proc    procfs   rw      0       0
/usr             /.usr    nullfs   rw      0       0
fs-xxxxxxxx.efs.rrrr.amazonaws.com:/ /usr nfs rw,nfsv4,minorversion=1,oneopenown,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,late,bg 0 0
/var/cache/usr   /usr     unionfs rw,copypolicy=always 0 0
Run Code Online (Sandbox Code Playgroud)

更多改进:驱逐缓存条目

现在我注意到我可能想添加另一个 whiteout 模式,即:从不。即,我应该能够从上层删除文件,效果是从缓存中驱逐文件,但没有从下层屏蔽文件的白化效果,因此它看起来是空的。这就是在 union.h 中添加 UNIONFS_WHITE_NEVER 的方法:

/* whiteout policy of upper layer */
typedef enum _unionfs_whitemode {
       UNIONFS_WHITE_ALWAYS = 0,
       UNIONFS_WHITE_WHENNEEDED,
       UNIONFS_WHITE_NEVER
} unionfs_whitemode;
Run Code Online (Sandbox Code Playgroud)

然后在 union_vnops.c 中:

static int
unionfs_remove(struct vop_remove_args *ap)
{
    ...
    if (uvp != NULLVP) {
        /*
         * XXX: if the vnode type is VSOCK, it will create whiteout
         *      after remove.
         */
        if (ump == NULL || ump->um_whitemode == UNIONFS_WHITE_ALWAYS ||
            (lvp != NULLVP && ump->um_whitemode != UNIONFS_WHITE_NEVER))
            cnp->cn_flags |= DOWHITEOUT;
        error = VOP_REMOVE(udvp, uvp, cnp);
    } else if (lvp != NULLVP && ump->um_whitemode != UNIONFS_WHITE_NEVER)
        error = unionfs_mkwhiteout(udvp, cnp, td, path);
Run Code Online (Sandbox Code Playgroud)

然后也可能有一些关于 rmdir 的东西。

static int
unionfs_rmdir(struct vop_rmdir_args *ap)
{
    ...
    if (uvp != NULLVP) {
        if (lvp != NULLVP) {
            error = unionfs_check_rmdir(ap->a_vp, cnp->cn_cred, td);
            if (error != 0)
                return (error);
        }
        ump = MOUNTTOUNIONFSMOUNT(ap->a_vp->v_mount);
        if (ump->um_whitemode == UNIONFS_WHITE_ALWAYS || 
            (lvp != NULLVP && ump->um_whitemode != UNIONFS_WHITE_NEVER))
            cnp->cn_flags |= DOWHITEOUT;
        error = unionfs_relookup_for_delete(ap->a_dvp, cnp, td);
        if (!error)
            error = VOP_RMDIR(udvp, uvp, cnp);
    }
    else if (lvp != NULLVP && ump->um_whitemode != UNIONFS_WHITE_NEVER)
        error = unionfs_mkwhiteout(udvp, cnp, td, unp->un_path);
Run Code Online (Sandbox Code Playgroud)

这也应该做驱逐的事情。

但在我做这一切之前,我想知道,是否存在人们已经找到的现有技巧?


PS:这是我的完整差异和测试结果。https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=251363

简短的回答是:它实际上工作得很好,还有一件事我不清楚:unionfs 不接受块设备,但需要一个目录!所以这真的很酷,您甚至不需要创建设备。我已经更新了我建议的 fstab,虽然我可能根本不会使用它,因为它必须延迟到 NFS 的后期安装之后。所以最好删除它并稍后打开这个基于 unionfs 的缓存,例如在 /etc/rc.local 中,它是如此简单:

mount -t unionfs -o copypolicy=always /var/cache/usr /usr
Run Code Online (Sandbox Code Playgroud)

我还发现 /var/cache/usr 目录仍然可以直接使用,因此可以通过从那里删除文件来简单地从缓存中逐出!这意味着我们甚至根本不需要弄乱 whiteout 设置。

相反,如果 unionfs_copyfile(...) 调用返回错误“设备上没有剩余空间”,我应该提出一个自动缓存逐出策略来从缓存中删除旧文件,逐出旧文件直到空间被回收,然后重试该操作. 很容易(除了找到旧的文件)。

穷人的轻松缓存驱逐

只需find /var/cache/usr -atime 2 -exec rm \{\}\;每隔几天运行一次即可删除那些一天未访问的项目。

一个更有趣的更深层次的问题可能是,是否可以通过在读取块时将块写入上层来提高 unionfs_copyfile(...) 函数的效率。甚至可以做整个面向块的事情,这样如果下层的文件是稀疏的,它也会在上层保持稀疏。

Nil*_*ils 1

NFS v3 或 v4.x 并不慢。所以我假设您谈论的是 NFS v2。

我刚刚浏览了手册页man 5 nfs。我偶然发现了选项fsc

这似乎可以完成你想要使用的cachefilesd。您可能可以在 /dev/shm 上找到该缓存,这应该会进一步加快速度。

我记得在 Solaris 中做过这样的事情,在那里我有一个 700 MB RAM 缓存,用于通过 NFS 向许多并发客户端提供 CD。