> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/20160910-huan-ying-lai-dao-freebsd-11/geli-and-zfs-improvements.md).

# GELI 与 ZFS 改进

* 原文：[GELI and ZFS Improvements](https://freebsdfoundation.org/wp-content/uploads/2016/10/GELI-and-ZFS-Improvements.pdf)
* 作者：**Allan Jude**

FreeBSD 11 带来了许多新内容，但本文将聚焦于我亲身参与的若干贡献。这些改动的重点在于改进 FreeBSD 系统的启动方式和系统更新的管理方式。

## BCache 改进

多年来，FreeBSD 加载器实现了简单的块缓存，通过避免多次物理读取同一扇区来提升性能。这个缓存最初开发时，大多数系统从单个磁盘启动，或者从暴露给操作系统作为单个逻辑磁盘的 RAID 卷启动。随着 ZFS 的出现，加载器常常需要访问多个磁盘，才能读取加载内核和启动操作系统所需的全部数据。Illumos 开发者 Toomas Soome 正在将 FreeBSD 加载器移植到 Illumos，以替换他们当前使用的古老 grub 版本。他对块缓存做了若干改进，并回馈给了 FreeBSD。这些改进包括将缓存从 16 KB 扩大到 16 MB、实现基本的预读缓存，并使其支持多个设备。16 MB 缓存在启动时检测到的设备数量之间均分。这在未来可能还能进一步优化。

预读特性利用了一个事实：对于旋转介质，读取适度数量的连续扇区与只读取单个扇区相比几乎不花额外时间。当下一个扇区被读取时，直接从缓存返回。如果磁盘上两个不同区域被并发读取，这能带来巨大的性能提升。此外，新增支持缓存从 CD 和 DVD 设备读取的数据，此前这些数据未缓存。这一改动对那些通过 IPMI 等远程介质系统启动 FreeBSD 安装镜像的人影响尤为显著。得益于预读缓存，通过 LAN 经 IPMI 从虚拟 CD（ISO 镜像）启动时加载内核的部分从 27 秒降至 7 秒。在远程场景下差异更为明显。没有块缓存时，在延迟 60ms 的远程服务器上安装 FreeBSD，一次从 CD 读取单个 2K 块，直到前一个块的读取操作完成（60+ms 后）才请求下一个块。有了新的预读缓存和其他 bcache 改进，从远程 CD 加载内核的时间从 12 分钟以上降至 5 分钟以内。现在当我要在延迟 225ms 的新加坡构建新服务器时，我非常感激 Toomas 的辛勤工作。经过若干其他贡献后，Toomas Soome 正式作为提交者加入 FreeBSD 项目。

## ZFS 启动环境菜单

启动环境的基本概念和工作机制此前已有介绍（参见本刊 2015 年 9/10 月刊）。简要回顾，“启动环境”（BE）是备用根文件系统。ZFS 提供低成本的快照和克隆，这意味着在升级等操作系统变更之前，操作员可以拍摄快照，以便在升级失败时将系统回退到该快照。这些快照可用于创建克隆，即可写的快照。有了这些克隆可供选择，操作员可以尝试不同版本的操作系统，而不会干扰当前安装的版本。某些文件系统（如 **/home**）在不同启动环境之间是共享的。这确保了即使操作员尝试不同版本的 FreeBSD，相同的 home 目录在所有版本中都可用。这也意味着如果回退升级，home 目录中更新的内容不会丢失。

下面是我开发机器上存在的启动环境：

```sh
NAME USED AVAIL REFER MOUNTPOINT
zroot/ROOT 11.3G 2.51T 392M legacy
zroot/ROOT/9.0_router 392M 2.51T 392M legacy
zroot/ROOT/9.1_before_upgrade 16.0K 2.51T 1.92G
zroot/ROOT/9.1_freebsd 822M 2.51T 822M
zroot/ROOT/9.1_pcbsd 848M 2.51T 2.05G
zroot/ROOT/9.2_beta1 16.0K 2.51T 848M
zroot/ROOT/9.2_pcbsd 16.0K 2.51T 2.10G
zroot/ROOT/before_10_stable_2014-09-11 16.0K 2.51T 2.68G
zroot/ROOT/10_stable_2014-03-24 647K 2.51T 3.35G
zroot/ROOT/11_1100093 16.0K 2.51T 3.35G
zroot/ROOT/11_r295359_zfsdebug 368K 2.51T 3.38G
zroot/ROOT/default 8.95G 2.51T 3.42G
```

如你所见，我把这台机器就地升级，从 9.0-RELEASE 一路到 11-CURRENT。甚至还有一个来自我在 11-CURRENT 中调试 ZFS 内存消耗问题时的 BE。在这个 BE 中，内核包含大量额外的 DTrace 探针，用于追踪问题源头。调试结束后，我可以重启回到标准系统，即 10-STABLE。只需重启就能在两者之间切换，同时维护共同的 **/home** 目录，且不需要将剩余磁盘空间分散到不同分区，这非常强大。

最初，管理 BE 的唯一方法是使用 **beadm(1)** 工具。这带来了明显的问题——如果系统无法启动，如何运行 beadm 工具将活动启动环境切换回先前可用的系统？PC-BSD 采用了类似 Illumos 的方案，beadm 工具生成 BE 列表并存入配置文件，由 GRUB 启动加载器读取。虽然这大体上能工作，但它需要使用非标准的启动加载器，且当配置文件与现实不同步时会出问题。GRUB 配置文件从设置 GRUB 时活动的原始启动环境加载模块，如果删除该 BE 而配置文件未正确更新，可能导致系统无法启动。

需要更简单、更可靠的方案。FreeBSD 加载器已经能够从 ZFS 读取并列出可用的数据集，这是 `lszfs` 加载器命令的一部分。我以该命令为模板，创建了新函数，用分页的启动环境列表填充一组环境变量。这种方法的主要优势是从实时文件系统读取，因此始终准确。在 Devin Teske 的帮助下，用 forth 编写的加载器菜单系统现在能显示菜单，并允许你选择哪个启动环境作为根文件系统。在 Toomas Soome 的帮助下又做了进一步改进。缺点是，如果系统以 EFI 启动，则不支持加载器菜单系统，因为我们的 EFI 加载器缺少所需的串口模拟代码。又是 Toomas Soome 出手相助，实现了缺失的特性，ZFS 启动环境菜单在 FreeBSD 加载器中对 BIOS 和 EFI 启动均可用。

## GELIBoot

现在 ZFS BE 几乎在任何配置下都受支持，除了用户选择整盘加密的情况。当包含根文件系统的存储池被加密时，需要单独的 **/boot** 文件系统（可以是第二个 ZFS 存储池，或小的 UFS 分区）。这个明文分区用于存放启动加载器、内核和 GELI、ZFS 等模块。系统需要能够自举，这要求读取启动加载器，启动加载器再读取并执行内核，内核加载模块以访问加密文件系统。问题在于这种“两个存储池”的设置破坏了 ZFS 启动环境。当内核位于根文件系统之外时（根文件系统是拍摄快照和克隆以创建各启动环境的基础），就无法在 BE 之间切换并加载匹配的内核。即使在两个存储池之间管理类似的快照，也会复杂且容易出错。需要更好的方案。

作为天真的初级开发者，我问自己“能有多难？”作为初始实现，复制了一份 gptzfsboot，命名为 gptgeliboot。最初的想法是创建能从加密的 UFS 或 ZFS 文件系统启动的单一 bootcode。boot2 代码中的一些细微差异，和无法明确定义同时使用 UFS 和 ZFS 文件系统的系统的行为，导致放弃该方案。于是决定改为在每个现有的 GPT bootcode 中实现可选的 GELI 支持。

首先需要确定启动分区是否经过 GELI 加密。与几乎所有 GEOM 类一样，GELI 将其元数据存储在 provider 的最后一个扇区（通常是分区），以避免与存储在磁盘最后一个扇区的 GPT 分区表备份冲突。任务看似简单——读取分区表，确定分区的起始 LBA 和大小，然后读取该分区的最后一个扇区。在 bootcode 中工作最困难的部分是没有错误报告机制，甚至连 `panic()` 都没有。几乎唯一能用的就是 `printf()`，当出现问题时系统直接挂起，除非你设法让 BTX 加载器崩溃，那样会得到汇编指令指针等的转储。这使得开发非常迭代，几乎是暴力试错：做一处改动，构建，安装，重启，失败，加 printf，构建，安装，重启，失败，重复。当然，必须控制 printf 的数量，因为没有分页器；一旦数据滚出屏幕顶部，就永远消失了。

确定分区包含加密数据后，需要读取 GELI 元数据以确定用哪种算法解密。然后必须用用户提供的密码解密主密钥的加密副本，只有此时才能从分区读取。现有的 bootcode 结构恰好便于在确定分区上存在 GELI 时，将解密添加到常规读取函数中。

添加了 GELI 的所有依赖（SHA256、SHA512、HMAC、AES-CBC 和 AES-CTS）后，遇到了第一个大障碍。boot2 文件（gptzfsboot）的大小从 47KB 增长到 90KB。boot1 代码（gptldr）是一段 512 字节的汇编，负责将 boot2 加载到内存中的特定位置并执行，它只加载 boot2 的前 64KB。因为这段代码在 16 位实模式下运行，这是能一次管理的最大数据块。经过多次失败尝试，并向社区许多资深成员求助后，Colin Percival 终于解决了问题，扩展了 gptldr 以加载指定数量的 32KB boot2 段，便于将来扩展。

为了复用 GELI 和 OpenCrypto（内核的 AES 实现，用于 IPSEC）的现有代码，需要做一些改动。GELI 代码中的一些声明和 ifdef 需要移动，以使其能为用户空间编译。完成后，我开始着手将 OpenCrypto 框架按算法拆分为独立文件，而不是单个庞大的文件。这样就能复用这些代码，而无需为加载器引入另一份 AES-XTS 副本。

令我惊讶的是，GELI 源代码非常优雅，易于理解，即使是对密码学几乎无经验的初级开发者也是如此。完成这些工作后，现在可以从加密的 UFS 或 ZFS 文件系统启动 FreeBSD，唯一的明文是微小的 freebsd-boot 分区（gptboot 或 gptzfsboot）的内容。

至此系统能够启动，但提示用户输入加密口令的次数过多。测试系统是两块磁盘的 ZFS 镜像。gptzfsboot 会为两块磁盘分别提示输入密码，然后加载加载器。加载器随后为每块磁盘提示输入口令，并加载内核。在 mountroot 提示处，内核为每块磁盘提示输入口令，最后系统启动。磁盘稍多时，这很快就变得极其繁琐。

Colin Percival、Devin Teske 和 Kris Moore 早已受类似问题困扰并开发了方案。Colin 实现了 `kern.geom.eli.boot_passcache` sysctl，缓存用户在 mountroot 提示处输入的密码，并在启动过程中测试每块新磁盘时尝试复用。Kris Moore 在 Colin 的帮助下扩展了该功能，允许在 GRUB2 启动加载器中输入的口令通过内核环境传递给 GELI，这样如果密码正确，就能避免在 mountroot 阶段重新提示输入密码。这避免了 mountroot 密码提示被后来的设备附加通知淹没的问题。Devin Teske 为 `loader.conf` 添加了选项 `geom_eli_passphrase_prompt`，使 FreeBSD 加载器提前提示用户输入 GELI 口令，并通过环境传递给内核，与 PC-BSD 的 GRUB2 的做法相同，目的同样是避免 mountroot 提示。GELI 内核模块在单用户模式启动前小心地将环境中的口令清零。GELIBoot 中实现的密码提示也有类似的缓存机制，会自动尝试先前输入的口令，只有失败时才给用户三次输入正确口令的机会。

GELIBoot 代码最早需要口令，以便从加密磁盘读取加载器。这引出了明显的问题：如何将口令从 boot2 阶段传递给加载器。答案在于 gptzfsboot，其中设置了标志 KARGS\_FLAGS\_EXTARG，告诉加载器在常规参数集之后寻找额外参数。在此处它会找到 struct zfs\_boot\_args，其中包含正在启动的存储池和根文件系统等信息。该结构的第一个成员是 `size`，设置为 sizeof(struct zfs\_boot\_args)。这使得加载器能安全访问结构的新成员，方法是先检查该成员的 offsetof() 不大于加载器定义的该结构的 sizeof()。这使得版本不匹配的 bootcode 和加载器仍能协同工作。当向结构末尾添加新成员时，对该成员的访问受此机制保护。利用这一设计模式，向 zfs\_boot\_args 结构添加了新成员，用于将 GELI 口令从 boot2 传递给加载器。它同样在下一个启动阶段开始时小心清零。

随后开始更新 FreeBSD 安装器，为用户创建此配置。存在若干限制。GELIBoot 系统目前只适用于 GPT 格式的磁盘。不支持 MBR 格式的磁盘，因为 boot2 代码大小有类似限制。最初并不清楚 ZFS 的磁盘上格式是否允许安装更大的 bootcode。与 Toomas Soome 讨论后，我了解到 ZFS 磁盘标签为启动代码留出了 3.5MB 空间，因此扩展 MBR boot2 代码是可能的。随着 ZFS 增加新特性，这可能本来就是必需的，因此 GELIBoot 很可能在 FreeBSD 11.1 中扩展以支持 MBR。目前 FreeBSD 的 EFI 启动加载器不支持 GELI。编写原始 EFI ZFS 支持的另一位开发者 Eric McCorkle 正在做这项工作，预计也会包含在 FreeBSD 11.1 中。不过，如果使用 GPT 并通过传统 BIOS 方式启动，安装器将创建单个完全加密的 ZFS 存储池并从中启动。在所有其他情况下，使用先前的方法——用第二个明文存储池存放加载器和内核。

另一个限制是目前只支持 GELI 口令。GELI 主密钥用 PKCS#5 v2 从口令派生的密钥加密。GELI 也支持使用密钥文件（或口令与密钥文件的组合），但 GELIBoot 不支持密钥文件。未来计划支持存储在外部介质（如 USB 设备）上的密钥文件。在 GELIBoot 的 UEFI 版本中，Eric 计划支持将密钥材料存入 TPM。关于我实现 GELIBoot 的更多经历，可参阅 AsiaBSDCon 2016 的论文，地址：`http://www.allanjude.com/bsd/AsiaBSDCon2016_geliboot_pdf1a.pdf`。

## 自动启动恢复

FreeBSD 包含一套名为 nanobsd 的工具集，已被许多项目（包括 FreeNAS 和 pfSense）用于构建设备。它具有固件式的升级功能：系统设置为两个分区，执行升级时将新系统安装到非活动分区，并设置分区表标志。下次启动时，包含较新系统映像的分区将被启动，并在其上设置失败标志。如果启动成功，标志将被清除。如果启动失败，当系统重新加电时，启动加载器会看到失败标志并自动回退到原始映像启动。

人们希望 ZFS 也能有类似的系统，而不需要单独的分区。基本思路是设置存储池属性 `altbootfs`，指示要尝试启动的新启动环境。当启动加载器检测到该属性时，会标记分区为失败，并从备用启动环境启动。如果系统正常启动，启动脚本会清除失败标志并将该启动环境提升为默认。如果系统未能正确启动，下次启动时会启动默认启动环境，其中包含升级前的系统。这能保护设备和服务器免于因升级失败而需要人工干预恢复。

## ZFS RAID 10

FreeBSD 安装器自 10.1 起支持创建镜像 ZFS 存储池；但如果系统包含大量磁盘且选择镜像选项，安装器会创建包含所有磁盘的单个镜像。虽然这在 8 块或更多磁盘时提供极佳的冗余，但可能并非用户本意。在 FreeBSD 11 中，安装器新增 RAID-10 选项，创建由成对磁盘组成的 ZFS 镜像。必须选择偶数块磁盘才能使用此选项。拥有 8 块磁盘的系统现在会创建由 4 组镜像、每组 2 块磁盘组成的存储池。这是性能最高的配置，尤其在 IOPS 方面。镜像组还提供最大的灵活性。可以成对添加磁盘，而不需要整个额外的 RAID-Z vdev。在预算受限的配置中，这允许在可用空间不足时购买少量磁盘逐步扩充。镜像组还允许通过用更大的磁盘替换较小的磁盘来获得额外空间。一旦镜像的两块成员都被单独替换并完成 resilver，额外空间即可用。而 8 块磁盘的 RAID-Z 则需要替换全部 8 块磁盘才能获得额外空间。

## 还有更多

这段时间 FreeBSD 社区的其他人也没有闲着，最近完成了许多出色的工作。FreeBSD 11 还将包含：

* ZFS ARC 现在可在运行时调整大小，便于更精细地控制 ZFS 使用的内存量
* 改进了 ZFS 与 VFS 层的交互，在内存压力下提升响应速度，并允许 ZFS 在需要时向系统归还更多内存
* ZFSd 现已可用；该守护进程管理热备盘并自动重新附加临时脱离的磁盘
* ZFS 现在支持 SHA512 和 Skein 校验和算法
* 内核中的 SHA2 实现已被性能更高的版本取代
* bhyve 现在支持 Windows 客户机，并有 VNC 后端提供视频控制台访问
* libxo 允许基本系统中的许多实用程序输出 JSON、XML 或 HTML，而不仅是纯文本
* Ifconfig 现在支持多种输出格式，包括以点分十进制和 CIDR 表示法打印子网掩码，以及传统的十六进制输出
* 默认启用更多功能，包括 Netmap 和 IPSec

## 未来展望

虽然 11 是一个重要里程碑，但未来还有更多值得期待。11.x 分支采用新的支持模型，每个点版本在下一个点版本发布后仍受支持三个月。这一新模型将允许更频繁的发布，并大大减轻安全团队和 Ports 团队支持两年前旧版本的负担。许多新功能将在 11.1 中提供，但部分重大变更需要等到 12。未来几个月我希望看到以下功能落地：

* 打包的基本系统
* UEFI 加载器中的 GELI
* 加载器支持更多 ZFS 特性
* ZFS 压缩 ARC
* ZFS 压缩 send/recv
* ZFS scrub/resilver 速度改进
* 安装器创建更大的 EFI 分区
* 更方便地管理 EFI 分区及其内容
* 安装器将交换分区放在数据分区之前，便于数据分区扩展
* 安装器可选择安装基本软件包集（图形环境）
* libucl（标准化的配置文件格式）用于基本系统中许多实用程序和守护进程的配置文件
* libxo 用于更多实用程序
* 更多实用程序库化（ifconfig、netstat）

***

**Allan Jude** 是 ScaleEngine Inc.（全球 HTTP 和视频流 CDN）的运营副总裁，在 FreeBSD 上大量使用 ZFS。他也是视频播客 BSDNow\.tv（与 Kris Moore 共同主持）和 TechSNAP.tv 的主持人。他是 FreeBSD src 和 doc 提交者，于 2016 年夏天当选 FreeBSD 核心团队成员。Allan 与 Michael W. Lucas 合著了《FreeBSD Mastery: ZFS》和《FreeBSD Mastery: Advanced ZFS》。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20160910-huan-ying-lai-dao-freebsd-11/geli-and-zfs-improvements.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
