技术博客

Zen Server 详解:本地与远端部署

第 83 / 100 篇 · 2026-03-09 · ue5 · 资产管线ZenCook

第 9040 篇讲了 DDC 的键和后端链,这一篇把 UE5.3 里 DDC 的终点——Zen——单独拆出来。Zen 在 Engine/Source/Programs/Zen,可执行体是 UnrealZenStore(旧名 ZenServer)。它不是又一个缓存插件,而是把"派生数据 + Cook 产物 + 补丁 diff"三件事统一到同一个内容寻址存储上的基础设施。

和老 HTTP DDC 的本质区别

老共享 DDC 是"远程文件夹"思路:键哈希出路径,PUT/GET 一个文件。Zen 换成内容寻址存储(CAS):对象按内容的哈希(UE5.3 用 Blake3)存放,元数据(record)和数据分离,小对象聚合存储。

这个区别看起来是实现的,其实是结构性的:

  • 去重是免费的:两个包引用了同一份中间纹理,Zen 里只有一份数据;补丁 Cook 时"没变"意味着哈希不变,直接复用旧数据,补丁 pak 里只放变化的 record;
  • 键和路径解耦:键是生成器语义(TEXTURE + 版本 + 后缀),值按内容哈希落盘,中间隔着一层记录表。老 DDC 里键和文件名一一对应,改键策略就要搬数据,Zen 里只是多一条 record;
  • 快照极便宜:snapshot 不是数据拷贝,是 oplog 上的一个位置指针——"这个状态等于 oplog 第 N 条之前所有记录的效果"。内容寻址让快照零成本,这是增量 Cook 和补丁 diff 能成立的地基。

本地部署:随项目走的默认形态

默认配置下,Cook 会在项目目录里起一个本地 Zen 实例(数据放项目 Saved 下),Cook 结束后可保留可清理。本地形态回答的问题是"这台机器上,重复 Cook 能不能增量":

  • 首次全量 Cook 把所有派生数据灌进本地 store;
  • 之后的迭代 Cook 只重算内容哈希变了的资产,命中部分直接复用 record;
  • IoStore 容器(.utoc/.ucas)的生成也走 Zen,容器里的 chunk 引用 Zen 里的数据块,所以"重打包"不再意味着重写全部字节。

对单人开发,本地 Zen 就已经把日常 Cook 压到分钟级。瓶颈转移到另一个问题:换机器、进 CI、团队多人,每台机器各存一份,重复劳动按人头放大——这就要远端共享。

远端部署:CI 灌库,开发机拉取

远端形态是一个常驻的 UnrealZenStore 服务(内网服务器或构建机上常驻进程),开发机和 CI 通过 HTTP 接口访问同一数据目录。两种典型拓扑:

  • CI 为中心:每日夜间全量 Cook 把派生数据灌进共享 Zen,开发机白天增量 Cook 大部分查询命中远端,本地只剩会话级 memory cache。这是性价比最高的接法,因为 CI 反正要做全量;
  • 多机直连:没有 CI 的小团队,指定一台开发机常驻 Zen,其余人指向它。注意这台机器的数据目录要放持久卷,并且备份——它一旦丢了,全团队第一次 Cook 回到全量。

配接方式在 UAT/命令行参数里指(-ddc=... 选择后端与目标地址),具体 ini 键随版本有变动,以所用引擎分支的 DerivedDataCache 配置为准。原则不变:远端 Zen 在 DDC 后端链里取代老 HTTP DDC 的位置,查询先本地后远端,miss 重算后按策略回写。

Zen 部署拓扑:内容寻址存储 + oplog,快照只是指针 开发机 A 本地 Zen 开发机 B 本地 Zen CI 全量 Cook 夜间灌库 远端 Zen 服务 CAS:内容 → Blake3 哈希 record:键 → 内容哈希 相同内容自动去重 小对象聚合存储 oplog(日志) 所有写入按序追加 snapshot = 位置指针 增量 Cook 的对照基线 补丁 diff 的数据源 快照零成本:不拷数据,只记"oplog 走到哪了" 对账方式:新 Cook 与 snapshot 逐 record 比哈希,变的才进补丁容器

oplog 与快照:增量的对账协议

Zen 的所有写入追加在 oplog(操作日志)里,store 的任意状态可以表示为"oplog 消费到第 N 条"。这让两件事变得便宜:

  • 增量 Cook:基线 Cook 打完 snapshot,之后的 Cook 把新 record 和 snapshot 对账,哈希没变的一律跳过——跳过的是整个"压缩 + 写容器"的流程,不只是省压缩 CPU;
  • 补丁生成:对账结果就是补丁内容本身。变化 record 的集合直接决定补丁容器体积,这也是为什么 9020 篇说"没有 snapshot 补丁就退化成全量"。

这套协议的前提是写入只追加、不改写:内容寻址的对象不可变,改内容就是新哈希新对象,老对象靠 GC 清理。所以 Zen 的空间回收也是确定性的——没有 snapshot 引用的 record 就是垃圾。

部署要点和坑

  • 数据目录放持久卷:本地 Zen 默认在项目 Saved 下,CI/远端实例一定要显式指定到挂载盘上,容器环境尤其注意;
  • 首次接远端是一次全量同步:CI 灌库那次会跑很久,但只做一次;之后开发机的日常 Cook 主要受网络延迟影响,Zen 的批量接口和压缩传输就是为此准备的;
  • 分支隔离:跟老 DDC 一样,不同引擎版本/分支的键空间不同,混用一个 Zen 只是各存各的、互相不命中,不会出错,但磁盘涨得快。按分支分 store 命名空间更干净;
  • 清理要有 snapshot 意识:手动删数据目录等于丢基线,下一次增量 Cook 退化为全量。运维上把"定期清无引用 record"交给 Zen 自己的 GC,而不是 rm 目录。

验证手段

  • Cook 日志里确认 Zen 后端启用和命中情况(LogDerivedDataCache / Zen 相关通道),命中率异常低先查分支和配置是否一致;
  • UnrealPak -List 对比基线容器与补丁容器的内容差异,和 Zen 对账结果应该一致——不一致说明有东西绕过了 Zen(比如某个生成器没走 DDC);
  • 磁盘水位用 Zen 自带的统计接口看 record 分布,别让"无引用对象"靠人肉判断。

Zen 的思路值得记住:把"缓存"升级成"带日志的只追加存储",快照、增量、diff 全都变成日志位置的比较。理解了 oplog,就理解了 UE5 Cook 管线为什么敢把补丁体积控制在 MB 级。