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

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

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

主意:看来我们应该能够拥有一个本地磁盘缓存,该缓存可以在从 NFS 拉取文件时在本地保存文件。

问题:有谁在 UNIX 系统上见过类似的东西吗?

背景:

在 FreeBSD 中,有很多使用 unionfs 构建令人惊叹的堆栈文件系统的好方法。我目前在 AWS 上有一个系统,它仅使用 1 GB 磁盘,因为它的大部分 /usr 文件系统树都是通过 NFS 挂载的。在过去,您可以轻松地做到这一点,因为基本启动不需要 /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;
    }

(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;
};

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

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;

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

在这种情况下,我们现在只需要将新的读时复制策略选项添加到 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);

所以,这将在 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

更多改进:逐出缓存条目

现在我注意到我可能想添加另一种白化模式,即:从不。即,我应该能够从上层删除文件,具有从缓存中逐出文件的效果,但没有从下层屏蔽文件的白化效果,因此它看起来是空的。这是在 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;

然后在 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);

那么可能还有一些关于 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);

这也应该做驱逐的事情。

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


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

我还发现 /var/cache/usr 目录仍然直接可用,因此只需从那里删除文件即可从缓存中逐出!这意味着我们根本不需要搞乱白化设置。

相反,我应该提出一种自动缓存驱逐策略,如果 unionfs_copyfile(...) 调用返回错误“设备上没有剩余空间”,则从缓存中删除旧 atime 的文件,驱逐旧文件直到空间回收,然后重试该操作。非常简单(除了查找旧 atime 的文件)。

穷人的简单缓存驱逐

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

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

答案1

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

我刚刚浏览了手册页man 5 nfs。我偶然发现了这个选项金融服务委员会

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

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

相关内容