DirectPV的故事

The Story of DirectPV

2020年,MinIO 为基于 Kubernetes 的 MinIO 存储部署实现了直接持久卷 (DirectPV)。DirectPV 类似于 LocalPV,但它是动态供应的。

在这篇文章中,我将描述创建 DirectPV 时的一些有趣的架构决策。但在深入了解架构细节之前,让我们先快速回顾一下直接持久卷与网络持久卷。

什么是 DirectPV?

DirectPV 是 Kubernetes 容器存储接口 (CSI) 驱动程序,用于直接连接存储。它用于为工作负载供应本地卷。简单来说,使用 DirectPV 的工作负载将直接使用本地磁盘,而不是远程磁盘。与基于 SAN 或 NAS 的 CSI 驱动程序(网络 PV)不同,后者在数据路径中添加了另一层复制/擦除编码和额外的网络跳跃。这额外的一层解耦导致了复杂性的增加和性能下降。

使用 DirectPV,您可以发现、格式化和挂载驱动器,使它们准备好进行供应。一旦驱动器初始化,就可以使用“directpv-min-io”存储类将工作负载的卷调度到驱动器上。

更多信息,请参考 https://github.com/minio/directpv

设计挑战

“简单是终极的复杂。” — 列奥纳多·达·芬奇

除了解释 DirectPV 的功能和用例外,我还想介绍开发阶段的一些有趣的架构决策。

以下是 DirectPV 设计阶段面临的一些主要挑战。

挑战 1:匹配驱动器的本地状态和远程状态

DirectPV 在 Kubernetes 自定义资源中保存和维护本地驱动器状态。某些状态(如驱动器名称和排序)在节点重启或驱动器热插拔期间可能不会持久化。需要持续保持本地设备状态与 DirectPV 记住的驱动器状态同步,因为它处理诸如格式化、挂载等敏感操作。这是至关重要的部分,如果处理不当,可能会导致数据丢失。

早期版本的 DirectPV 通过识别驱动器的 WWID、序列号和分区 UUID 等属性来匹配本地驱动器(来自主机)和远程驱动器(Kubernetes 驱动器资源)。

这种方法存在以下问题

(a) 探测设备成本更高,占用资源更多,并产生负面影响。

我们执行 IOCTL 调用以读取驱动器的 S.M.A.R.T(自监控、分析和报告技术)序列号信息。作为副作用,这适得其反,产生了过多的 CHANGE udev 事件,导致 DirectPV udev 监听器中的 CPU 出现峰值。

(b) 根据驱动器的属性匹配驱动器至关重要

如前所述,需要持续保持驱动器与本地状态同步,因为 DirectPV 执行本地主机级别的操作,如格式化、挂载等。早期版本的 DirectPV 使用多个属性(如 WWID、序列号、主次设备号等)来匹配驱动器。由于不同驱动器供应商之间的差异,此类状态对于匹配来说并不可靠。并非所有驱动器供应商都提供关于驱动器的相同信息。这里的风险在于,驱动器可能会被错误地格式化,并且可能被不正确地映射到卷上。一个这样的示例在 此处 解释。

解决方案

现在,我们已经看到了探测设备以获取信息和保持本地和远程驱动器状态同步的潜在挑战。我们发现了一种简化 DirectPV 管理驱动器范围的方法。

我们从管理集群中的所有驱动器转向仅管理提供给 DirectPV 的驱动器。设计进行了一些小的修改以实现此目的。

# 列出集群中可用的驱动器
kubectl directpv drives ls

# 选择 DirectPV 应该管理和格式化的驱动器
kubectl directpv drives format --drives /dev/sd{a...f} --nodes directpv-{1...4}

如上所示,在 DirectPV 的旧版本中,服务器启动后,它会探测集群中所有节点中的所有驱动器,并在 Kubernetes(自定义资源)中创建相应的驱动器对象。这些驱动器对象被列出并格式化。如前所述,如果驱动器状态不可靠,这可能会导致错误地格式化驱动器。

# 探测并保存驱动器信息到 drives.yaml 文件。
$ kubectl directpv discover

# 初始化选定的驱动器。
$ kubectl directpv init drives.yaml

最新版本的 DirectPV 执行按需探测。当用户执行命令 `kubectl directpv discover` 时,它会实时探测集群中的驱动器。与旧版设计不同,这是无状态的,并且不会在 Kubernetes 中保存任何驱动器状态。用户查看探测到的驱动器的输出,然后通过发出 `kubectl directpv init` 命令将它们添加到 DirectPV 中。

作为初始化过程的一部分,驱动器将使用唯一的的文件系统 UUID 进行格式化。执行成功后,将使用 UUID 创建一个驱动器对象。此 FSUUID 对于“已初始化”的驱动器是唯一的,我们可以可靠地使用它进行匹配。它提供了一个强有力的证据,即如果已初始化驱动器的 FSUUID 在主机上因篡改或被任何外部进程覆盖而发生变化,则无法匹配这些驱动器,并将被视为损坏的驱动器。我们不再需要除此之外的其他任何成本更高的探测。

挑战 2:杀手式声明性格式化和重试

根据定义,Kubernetes 中的事件控制器是声明性的,在维护资源的期望状态方面发挥着至关重要的作用。它在任何错误上都会重试,直到达到期望状态。这在 Kubernetes 中一直是一种成功的方法来管理和维护资源的状态。

DirectPV 的旧版本采用了类似的方法。驱动器(自定义资源)事件控制器侦听格式化驱动器请求并做出反应。当格式化失败时,控制器会以递增的回退进行重试。当驱动器状态在定期重试期间发生变化时,这种方法是不安全的。这就像“派出一个杀手去追杀磁盘并不断重试”。

解决方案

我们更改了设计以遵守以下规则

  • 格式化不进行自动重试
  • 在格式化之前验证驱动器并确保 100% 正确

初始化(格式化)是实时的,没有自动重试。最重要的是,不会对缓存的驱动器状态执行任何关键的驱动器操作。

如果初始化成功或失败,错误将被传达,用户将重试初始化。

提供给 init 命令的配置文件将包含驱动器哈希,这些哈希对于驱动器是唯一的。在格式化之前,DirectPV 服务器将验证请求中的此哈希是否与计算出的哈希匹配。如果不匹配,则会出错,并抱怨状态已更改。

总结

"简单不仅仅是去除杂乱。它关乎于将清晰度带入复杂性…" - 阿南德·巴布·佩里亚萨米

DirectPV 代表了 MinIO 客户在 Kubernetes 上运行 MinIO 存储的一大进步。实现这一目标并非易事。本文探讨了 MinIO 工程师在实现 DirectPV 时面临的两个高级挑战。这些挑战是杀手式重试以及将本地状态与驱动器的远程状态进行匹配。

如果您有任何疑问,请务必在 Slack 上联系我们!