注意

本文档适用于 Ceph 的开发版本。

libcephfs 代理设计

问题描述

当应用程序通过 libcephfs.so 库连接到 Ceph 卷时,会在进程内部创建一个本地缓存。 libcephfs.so 的实现已经处理了缓存的内存使用情况并进行了调整,以确保它不会消耗所有可用内存。但是,如果多个进程通过库的不同实例连接到 CephFS,每个进程都将保留一个私有缓存。在这种情况下,内存管理无效,因为即使配置了内存限制,可以创建的 libcephfs 实例数量也是无限的,并且它们无法协调工作来正确控制资源使用。因此,当所有进程都 intensively 使用数据缓存时,相对容易消耗所有内存。这会导致 OOM killer 终止这些进程。

拟议解决方案

高级方法

主要思想是创建一个 libcephfs_proxy.so 库,它将提供与原始 libcephfs.so 相同的 API,但不缓存任何数据。任何当前使用 libcephfs.so 的应用程序都可以透明地使用此库(即无需代码修改),只需链接 libcephfs_proxy.so 而不是 libcephfs.so,甚至可以使用 LD_PRELOAD

还将创建一个新的 libcephfsd 守护程序。该守护程序将链接真实的 libcephfs.so 库,并监听 UNIX socket 上的传入连接。

当应用程序启动并通过 libcephfs_proxy.so 库发起 CephFS 请求时,它将通过 UNIX socket 连接到 libcephfsd 守护程序,并将所有 CephFS 请求转发给它。守护程序将使用真实的 libcephfs.so 来执行这些请求,答案将返回给应用程序,可能在 libcephfsd 进程本身中缓存数据。所有这些都将透明地发生,应用程序毫不知情。

守护程序将在不同的应用程序之间共享低级 libcephfs.so 挂载,以避免为每个应用程序创建一个实例,这会对内存产生与将每个应用程序直接链接到 libcephfs.so 库相同的效果。只有当应用程序定义的配置相同时,才会执行此操作。否则仍将创建新的独立实例。

libcephfsd 守护程序内部需要以特殊方式实现某些 libcephfs.so 函数,以隐藏与多个客户端共享同一挂载实例所引起的差异(例如 chdir/getcwd 不能直接依赖于 libcephfs.soceph_chdir()/ceph_getcwd())。

最初,将只提供 Samba VFS CephFS 模块使用的 libcephfs.so 低级接口函数子集。

通用组件设计

网络协议

由于通过 UNIX socket 的连接是到在同一台机器上运行的另一个进程,并且我们需要传递的数据非常简单,我们将避免使用通用的 XDR 编码/解码和 RPC 传输的所有开销,而是使用在代码本身中实现的非常简单的序列化。未来,我们可以考虑使用 cap'n proto (https://capnproto.org),它声称编码和解码的开销为零,并且如果将来需要修改网络协议,它将提供一种支持向后兼容性的简单方法。

功能协商

在通过 UNIX socket 的初始连接期间,客户端将发起对其要启用的功能的协商。目前它支持一个支持/要求/期望功能的位图,但它允许通过添加更多字段来扩展包含协商数据的结构,而不会破坏向后兼容性。

结构本身包含结构的版本和大小,以及支持的最低要求版本,允许另一方读取传输的数据,即使它不完全理解其内容,并调整到双方已知的最高可能版本。

客户端将发送支持功能的位图、必须可用的功能的位图(否则连接无法继续)以及客户端希望启用的功能的位图。当服务器接收到它时,它将创建双方支持功能的位图,并验证服务器要求的所有功能是否都受客户端支持。它还会将其期望的功能添加到客户端期望的功能中。此数据将发送回客户端,客户端将进行最终检查并决定最终启用的功能集。一旦此功能集发送到服务器,双方就可以开始使用启用的功能。

libcephfs_proxy.so 库设计

该库将基本上连接到 libcephfsd 守护程序正在监听的 UNIX socket,等待来自应用程序的请求,序列化所有函数参数并将它们发送到守护程序。一旦守护程序响应,它将反序列化答案并将结果返回给应用程序。

本地缓存

虽然该库的主要目的是避免每个进程上的独立缓存,但一些初步测试显示,当所有请求都通过代理守护程序时,基于元数据操作和/或小文件的 workload 的性能会大幅下降。为了尽量减少这种情况,应该实现元数据缓存。元数据缓存比数据缓存小得多,将在内存使用和性能之间提供良好的折衷。

为了以安全的方式实现缓存,需要先正确地使数据失效,然后再使其过期。目前 libcephfs.so 提供可用于实现此目的的失效通知,但其语义尚未完全理解,因此 libcephfs_proxy.so 库中的缓存将在未来版本中设计和实现。

libcephfsd 守护程序设计

守护程序将是一个常规进程,它将集中处理来自同一台机器上其他进程的 libcephfs 请求。

进程维护

由于该进程将作为独立守护程序工作,因此将提供一个简单的 systemd 单元文件来将其作为常规系统服务进行管理。未来很可能会将其集成到 cephadm 中。

如果 libcephfsd 守护程序崩溃,我们将依靠 systemd 来重新启动它。

特殊功能

由于共享低级挂载,直接将某些功能转发给 libcephfs.so 可能会返回不正确的结果,因此需要在 libcephfsd 守护程序内部以特殊方式处理这些功能以提供正确的功能。

共享底层 struct ceph_mount_info

代理的主要目的是避免在访问相同数据时为每个进程创建一个新挂载。为了能够提供这一点,我们需要“虚拟化”挂载点,并让应用程序相信它正在使用自己的挂载,而实际上它可能正在使用共享挂载。

守护程序将跟踪用于连接到卷的 Ceph 帐户、配置文件以及在挂载卷之前完成的任何特定配置更改。只有当所有设置都与另一个已挂载实例完全相同时,才会共享挂载。守护程序不会理解 CephFS 设置或设置之间的任何潜在依赖关系。因此,将进行非常严格的比较:配置文件需要相同,并且此后进行的任何其他更改都需要设置为完全相同的值和相同的顺序,以便两个配置可以被认为是相同的。

用于确定两个配置是否相同的检查将在挂载卷之前完成(即 ceph_mount())。这意味着在配置阶段,我们可能会分配许多同时存在的挂载,但尚未挂载。然而,只有一个会成为真正的挂载。其他的将保持未挂载状态,并最终在用户卸载和释放它们后被销毁。

以下函数将受到影响

  • ceph_create

    这将分配一个新的 ceph_mount_info 结构,并且将记录提供的 id 以供将来比较潜在匹配的挂载。

  • ceph_release

    这将释放未挂载的 ceph_mount_info 结构。未挂载的结构不会与其他人共享。

  • ceph_conf_read_file

    这将读取配置文件,计算校验和并进行复制。复制将确保自计算校验和以来配置文件中没有更改,并且将记录校验和以供将来比较潜在匹配的挂载。

  • ceph_conf_get

    这将获取请求的设置,并记录它以供将来比较潜在匹配的挂载。

    尽管这看起来没有必要,但由于守护程序将配置视为黑盒,因此可能会有一些动态设置,根据外部因素返回不同的值,因此守护程序还要求任何请求的设置返回相同的值才能将两个配置视为相同。

  • ceph_conf_set

    这将记录修改后的值以供将来比较潜在匹配的挂载。

    在正常情况下,即使在挂载卷之后也可以设置某些设置。代理不会允许这样做,以避免对共享同一挂载的其他客户端产生潜在干扰。

  • ceph_init

    这将是一个空操作。调用此函数会触发分配多个资源并启动一些线程。如果此 ceph_mount_info 结构最终没有挂载,因为它与已存在的挂载匹配,那么这只会浪费资源。

    只有当在挂载时(即 ceph_mount())没有与已存在的挂载匹配时,挂载才会同时初始化和挂载。

  • ceph_select_filesystem

    这将记录选定的文件系统以供将来比较潜在匹配的挂载。

  • ceph_mount

    这将尝试查找与此 ceph_mount_info 结构定义的所有配置匹配的活动挂载。如果没有找到,它将被挂载。否则,已存在的挂载将与此客户端共享。

    未挂载的 ceph_mount_info 结构将保留并与已挂载的结构相关联。

    所有“真实”挂载都将针对卷的绝对根目录(即“/”)进行,以确保它们可以稍后与其他客户端共享,无论它们是否使用相同的挂载点。这意味着在挂载之后,守护程序需要解析并存储“虚拟”挂载点的根 inode。

    CWD(当前工作目录)也将初始化为相同的 inode。

  • ceph_unmount

    这将使客户端与已挂载的 ceph_mount_info 结构分离,并将其重新附加到关联的未挂载结构之一。如果这是挂载的最后一个用户,它将最终卸载。

    调用此函数后,客户端继续使用专用于自身的私有 ceph_mount_info 结构,因此可以安全地进行其他配置更改和操作。

将访问限制在预期的挂载点

由于有效挂载点可能与真实挂载点不匹配,因此如果不小心处理,某些函数可能会返回有效挂载点之外的 inode。为了避免这种情况并提供用户应用程序期望的结果,我们需要在 libcephfsd 守护程序内部模拟其中一些函数。

需要考虑三种特殊情况

  1. 处理以“/”开头的路径

  2. 处理包含“..”的路径(即父目录)

  3. 处理包含符号链接的路径

当找到这些特殊路径时,需要以特殊方式处理它们,以确保返回的 inode 是客户端所期望的。

以下函数将受到影响

  • ceph_ll_lookup

    lookup 接受“..”作为要解析的名称。如果父目录是“虚拟”挂载点的根目录(可能与真实挂载点不同),我们需要返回在挂载时存储的与“虚拟”挂载点对应的 inode,而不是真实的父目录。

  • ceph_ll_lookup_root

    这需要返回在挂载时存储的根 inode。

  • ceph_ll_walk

    这将在守护程序内部完全重新实现,以便能够正确解析每个路径组件和符号链接,并以正确的方式处理“/”和“..”。

  • ceph_chdir

    这将解析传入的路径,并将其连同相应的 inode 存储在当前“虚拟”挂载内部。不会调用真实的 ceph_chdir()

  • ceph_getcwd

    这将只返回从以前的 ceph_chdir() 调用中存储在“虚拟”挂载中的路径。

处理 AT_FDCWD

任何接收文件描述符的函数也可能接收特殊的 AT_FDCWD 值。这些函数需要检查该值并使用“虚拟” CWD 代替。

测试

代理对于任何已经使用 libcephfs.so 的应用程序都应该是透明的。这也适用于测试脚本和应用程序。因此,任何针对常规 libcephfs.so 库的现有测试也可以用于测试代理。

由 Ceph 基金会为您呈现

Ceph 文档是由非营利性 Ceph 基金会 资助和托管的社区资源。如果您希望支持这项工作和我们的其他努力,请考虑 立即加入