UE5 :Zen as Cook Storage 探究和实例测试

一、摘要

本文介绍官方的《使用 Zen Storage Server 作为虚幻引擎的烘焙输出存储》,利用Lyra项目,一并介绍BuildGraph中相关的ZenExportSnapshotTask、ZenImportOplogTask、ZenImportSnapshotTask三个节点

二、Zen Storage 文档总结

从上文的官方文档进行总结有下面几点

1、目的与优势

  • 解决性能瓶颈:传统烘焙会生成数百万个小文件,导致文件系统读写、杀毒软件扫描(巨硬的Defender)及元数据更新缓慢。Zenserver 将数据聚合存储,可以大幅减少文件系统开销。

  • 支持新功能:是 Zen Streaming和 Cooked Data Snapshots 的基础。本文不展开关于Zen Stream的内容,Snapshots也不进行深入解释。可以查看另外两篇官方文档或者等作者拖更。

  • 空间优化:通过内容寻址存储实现跨项目、跨平台的重复数据删除,节省磁盘空间,降本增效(。)

2、使用方法

文档中写目前在 UE 5.5 中默认关闭,反正我看了5.7也是默认关的。

开启方式:Project Settings -> Packaging -> Use Zen Server as cooked output store

UE5ZenasCookStorage与BuildGraphZenNodes-2026-01-14-23-35-58

或者在DefaultGame.ini中添加:

1
2
[/Script/UnrealEd.ProjectPackagingSettings]
bUseZenStore=True

注意:必须启动Use Io Store,但是不必须依赖 Use Pak File,具体差异不在这里赘述。

开启后,项目的 Saved/Cooked 目录下不再有具体的资产文件(如 .uasset),只有一个 ue.projectstore 索引文件。实际数据存储在 AppData 或自定义路径下。

3、数据管理

Zenserver 作为一个服务运行,统一管理所有项目的数据。
清理机制:基于引用的垃圾回收。如果 ue.projectstore 文件被删除或超过 n 天未修改,对应的数据会被自动清理。

4、对Stage的影响

启动了Zen进行Storage的时候,Stage的时候UAT会从Zenserver读取数据。

打包为.pak的情况,沈城的最终.pak和不使用zenserver的情况一致,Stage结果也不会包含ue.projectstore

pak也不启用,使用散文件的话,Stage产出的目录将不会有实际的文件,只会有一个ue.projectstore,运行时将会通过该文件连接到Zenserver,进行流式传输。流式传输的情况本文不深入讨论。

三、使用Lyra测试

前置配置

我在WSL上运行Zenserver,作为中心存储,下文称作Remote Zen,为了避免端口冲突,我手动设置了8559,不是默认的8558

在DefaultEngine中配置了如下选项:

1
2
3
4
5
6
7
8
9
[Zen.AutoLaunch]
DataPath=E:/UnrealProject/LyraStarterGame/Zen/Data

[Zen.ConnectExisting]
HostName=localhost
Port=8558

[StorageServers]
Shared=(Host="http://172.28.34.131:8559", Namespace="lyra.ddc", Remote=true)

将本地ZenData目录指向了一个绝对路径,这里也可以填写相对路径,注意这个参数会在执行BuildGraph的Zen相关节点时作为ZenData的目录。虽然可以通过环境变量和命令行参数进行覆盖,不过为了简单和尽量避免配置问题的角度,我建议只在这一个地方设置ZenData的位置

这里通过StorageServers设置了Shared ddc的位置

当我启动Editor完成了Shader编译之后,可以在Zen Dashboard中看到:

image.png

ddc会在cook过程中被利用以加速cook进程。

此时已经完成了本地的zenserver启动的配置和远程zenserver的启动配置,通过设置Shared DDC确认本地能访问到远程Zen并成功写入ddc

配置BuildGraph

这里给出一个简短的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph">

<Option Name="ProjectFile" DefaultValue="" Description="Path to .uproject"/>
<Option Name="ProjectName" DefaultValue="" Description="Project Name (e.g. Lyra)"/>
<Option Name="TargetName" DefaultValue="$(ProjectName)" Description="Target Name (e.g. LyraGame)"/>
<Option Name="ZenPublishHost" DefaultValue="" Description="Zen Server URL"/>
<Option Name="BuildChange" DefaultValue="00000" Description="ChangeList Number"/>

<Agent Name="LocalZenTest" Type="Win64">

<Node Name="CompileAll">
<Compile Target="ShaderCompileWorker" Platform="Win64" Configuration="Development"/>
<Compile Target="UnrealPak" Platform="Win64" Configuration="Development"/>

<Compile Target="$(ProjectName)Editor" Project="$(ProjectFile)" Platform="Win64" Configuration="Development"/>

<Compile Target="$(TargetName)" Project="$(ProjectFile)" Platform="Win64" Configuration="Development"/>
</Node>

<Node Name="CookAndPush" Requires="CompileAll">
<Cook Project="$(ProjectFile)" Platform="Windows" Arguments="-cookincremental -unversioned -compressed"/>

<Log Message="Pushing to Zen Host: $(ZenPublishHost)"/>

<ZenExportSnapshot
Project="$(ProjectFile)"
Platform="Windows"
DestinationStorageType="Zen"
DestinationZenHost="$(ZenPublishHost)"
DestinationIdentifier="$(ProjectName).dev.$(BuildChange)"
SnapshotDescriptorFile="$(RootDir)/Saved/ZenDesc/$(ProjectName).json"
/>
</Node>

</Agent>
</BuildGraph>

此处由于Lyra的Target.cs的命名和uproject的名字不一致,所以只好额外处理一下单独传递了ProjectName和TargetName

在上面这个BuildGraph中,先是进行了所有的编译工作,然后使用Cook节点进行cook,最后使用ZenExportSnapshot节点进行oplog的Snapshot的导出和上传

其中Cook的参数cookincremental顺便测试了5.7新引入的incremental cook功能,相关文章可以看这个 UE5.7 解析:Incremental Cooking决策逻辑 

对应使用这个BuildGraph的powershell脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] Starting BuildGraph execution..."

$process = Start-Process -FilePath "F:\UE_Release\Engine\Build\BatchFiles\RunUAT.bat" `
-ArgumentList @(
"BuildGraph",
"-Script=`"E:\UnrealProject\LyraStarterGame\Graph\Tasks\PackageWithZenSnapshot.xml`"",
"-Target=`"CookAndPush`"",
"-set:ProjectFile=`"E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject`"",
"-set:ProjectName=`"Lyra`"",
"-set:TargetName=`"LyraGame`"",
"-set:ZenPublishHost=`"http://172.28.34.131:8559`"",
"-set:BuildChange=00002"
) `
-NoNewWindow -PassThru -Wait

$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] BuildGraph execution completed with exit code: $($process.ExitCode)"
exit $process.ExitCode

进行Cook

通过log来确认zen的作用,下面直接给出一部分log

1
2
3
4
5
6
7
8
9
10
11
12
[2026-01-16 21:40:52] Starting BuildGraph execution...
Running AutomationTool...
Using bundled DotNet SDK version: 8.0.412 win-x64
Starting AutomationTool...
Parsing command line: BuildGraph -Script=E:\UnrealProject\LyraStarterGame\Graph\Tasks\PackageWithZenSnapshot.xml -Target=CookAndPush -set:ProjectFile=E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject -set:ProjectName=Lyra -set:TargetName=LyraGame -set:ZenPublishHost=http://172.28.34.131:8559 -set:BuildChange=00002
Initializing script modules...
Total script module initialization time: 0.22 s.
Using C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe
Executing commands...
****** [1/2] CompileAll

Running: F:\UE_Release\Engine\Binaries\ThirdParty\DotNet\8.0.412\win-x64\dotnet.exe "F:\UE_Release\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" -Target="ShaderCompileWorker Win64 Development -Manifest=F:\UE_Release\Engine\Intermediate\Build\Manifest-1-ShaderCompileWorker-Win64-Development.xml" -Target="UnrealPak Win64 Development -Manifest=F:\UE_Release\Engine\Intermediate\Build\Manifest-2-UnrealPak-Win64-Development.xml" -Target="LyraEditor Win64 Development -Project=E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject -Manifest=F:\UE_Release\Engine\Intermediate\Build\Manifest-3-LyraEditor-Win64-Development.xml" -Target="LyraGame Win64 Development -Project=E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject -Manifest=F:\UE_Release\Engine\Intermediate\Build\Manifest-4-LyraGame-Win64-Development.xml" -log="F:\UE_Release\Engine\Programs\AutomationTool\Saved\Logs\UBA-ShaderCompileWorker-Win64-Development.txt"

可能不是很容易看,但是不难注意到BuildGraph的第一个Node:CompileAll启动了,正确通过set设置了一些参数

接下来是cook的一部分log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Reading tag "CompileAll":"#CompileAll" from local file F:\UE_Release\Engine\Saved\BuildGraph\CompileAll\Tag-CompileAll.xml
Reading tag "CompileAll":"" manifest from F:\UE_Release\Engine\Saved\BuildGraph\CompileAll\Manifest.xml
Compiling GameFeaturePlugins in branch UE5
Running UnrealEditor Cook for project E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject
Commandlet log file is F:\UE_Release\Engine\Programs\AutomationTool\Saved\Cook-2026.01.16-21.41.03.txt
Running: F:\UE_Release\Engine\Binaries\Win64\UnrealEditor-Cmd.exe "E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject" -run=Cook -TargetPlatform=Windows -Unversioned -cookincremental -unversioned -compressed -abslog="F:\UE_Release\Engine\Programs\AutomationTool\Saved\Cook-2026.01.16-21.41.03.txt" -stdout -CrashForUAT -unattended -NoLogTimes
…………………………
…………………………
…………………………
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Read zen version cache file from 'F:/UE_Release/Engine/Saved/Zen/zen.version', version: '5.7.6-202510061608-windows-x64-release-9a1ebb9'
LogZenServiceInstance: Display: Read zen version cache file from 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zen.version', version: '5.7.6-202510061608-windows-x64-release-9a1ebb9'
LogZenServiceInstance: Display: Installed service at 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zenserver.exe' is up to date
LogZenServiceInstance: Display: Launching executable 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zenserver.exe', working dir 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install', data dir 'E:/UnrealProject/LyraStarterGame/Zen/Data', args '--port 8558 --data-dir E:\UnrealProject\LyraStarterGame\Zen\Data --http asio --gc-cache-duration-seconds 1209600 --gc-interval-seconds 21600 --gc-low-diskspace-threshold 2147483648 --cache-bucket-limit-overwrites --quiet --http-forceloopback --owner-pid 7576 --child-id Zen_7576_Startup'
LogZenServiceInstance: Display: Unreal Zen Storage Server HTTP service at http://[::1]:8558 status: OK!.
LogDerivedDataCache: Display: ZenLocal: Using ZenServer HTTP service at [::1] with namespace ue.ddc status: OK!.
LogDerivedDataCache: Display: ../../../Engine/DerivedDataCache: Performance: Latency=0.01ms. RandomReadSpeed=883.90MBs, RandomWriteSpeed=296.18MBs. Assigned SpeedClass 'Local'
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Zen utility process has been running for 2.0 seconds without completing, still waiting...
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenStore: Display: Establishing oplog 'LyraStarterGame.247567c8/EditorDomain'
LogZenStore: Display: Zen project 'LyraStarterGame.247567c8' already exists
LogZenStore: Display: Zen oplog 'LyraStarterGame.247567c8/EditorDomain' already exists
LogZenStore: Display: Establishing oplog 'LyraStarterGame.247567c8/Windows'
LogZenStore: Display: Zen project 'LyraStarterGame.247567c8' already exists
LogZenStore: Display: Zen oplog 'LyraStarterGame.247567c8/Windows' already exists
LogZenStoreWriter: Display: Fetching oplog...
LogZenStoreWriter: Display: Fetching file manifest...
LogZenStoreWriter: Display: Fetched '5738' file(s) from oplog 'LyraStarterGame.247567c8/Windows'
LogZenStoreWriter: Display: Fetched '4050' packages(s) from oplog 'LyraStarterGame.247567c8/Windows'
LogCook: Display: Packages Cooked: 0, Packages Incrementally Skipped: 3819, Packages Skipped by Platform: 219, Total Packages: 4038
Pushing to Zen Host: http://172.28.34.131:8559
Running: F:\UE_Release\Engine\Binaries\Win64\ZenLaunch.exe "E:\UnrealProject\LyraStarterGame\LyraStarterGame.uproject" -SponsorProcessID=52616
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Launching zen utility 'F:/UE_Release/Engine/Binaries/Win64/zen.exe service status'.
LogZenServiceInstance: Display: Read zen version cache file from 'F:/UE_Release/Engine/Saved/Zen/zen.version', version: '5.7.6-202510061608-windows-x64-release-9a1ebb9'
LogZenServiceInstance: Display: Read zen version cache file from 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zen.version', version: '5.7.6-202510061608-windows-x64-release-9a1ebb9'
LogZenServiceInstance: Display: Installed service at 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zenserver.exe' is up to date
LogZenServiceInstance: Display: Launching executable 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zenserver.exe', working dir 'C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install', data dir 'E:/UnrealProject/LyraStarterGame/Zen/Data', args '--port 8558 --data-dir E:\UnrealProject\LyraStarterGame\Zen\Data --http asio --gc-cache-duration-seconds 1209600 --gc-interval-seconds 21600 --gc-low-diskspace-threshold 2147483648 --cache-bucket-limit-overwrites --quiet --http-forceloopback --owner-pid 51344 --child-id Zen_51344_Startup'
LogZenServiceInstance: Display: Unreal Zen Storage Server HTTP service at http://[::1]:8558 status: OK!.
Took 4.42s to run ZenLaunch.exe, ExitCode=0
Running: F:\UE_Release\Engine\Binaries\Win64\zen.exe project-create --hosturl http://172.28.34.131:8559 lyrastartergame.oplog
Took 1.30s to run zen.exe, ExitCode=1
Running: F:\UE_Release\Engine\Binaries\Win64\zen.exe oplog-export --embedloosefiles --zen http://172.28.34.131:8559 --hosturl http://localhost:8558 --target-project lyrastartergame.oplog --target-oplog lyra.dev.00002.windows LyraStarterGame.247567c8 Windows
Took 5.39s to run zen.exe, ExitCode=0
Saving file list to F:\UE_Release\Engine\Saved\BuildGraph\CookAndPush\Tag-CookAndPush.xml

BUILD SUCCESSFUL

这一段有点长,但是不难看出其中有大量和Zen有关的log,一点一点看:

先,系统对 Zen 组件进行可用性与版本一致性验证:多次调用 zen.exe service status,并分别从引擎侧缓存路径 F:/UE_Release/Engine/Saved/Zen/zen.version 与用户侧安装路径 C:/Users/muche/AppData/Local/UnrealEngine/Common/Zen/Install/zen.version 读取版本号,确认已安装的 zenserver.exe 与期望版本一致。随后在本机启动 Zen Storage Server,数据目录被绑定到项目级路径 E:/UnrealProject/LyraStarterGame/Zen/Data,并以 --http-forceloopback 将 HTTP 服务限制在回环地址 [::1]:8558,表征该实例被定位为本地单机服务而非对外暴露的共享节点。服务健康检查通过 “status: OK!” 明确给出。

之后的LogDerivedDataCache表示同一个Zen用做了DDC后端,是前面我设置过的。

再往后,先后建立 LyraStarterGame.247567c8/EditorDomainLyraStarterGame.247567c8/Windows 两个 oplog,“project already exists”“oplog already exists”是因为这个log不是我第一次运行,已经存在了。LogZenStoreWriter 触发 oplog的fetch,从Windows的oplog中取到 5738 个文件条目与 4050 个 package 记录。这些是后续进行incremental cook所需的关键信息。

其中这一行Packages Cooked: 0, Packages Incrementally Skipped: 3819, ... Total Packages: 4038,明确显示出本次Cook没有进行包的处理,全部跳过了。这句是本地cook完成的一个分界线,前面的zen是在操作local zen,后面是和remote zen进行交互。

日志尾部可以看到remote的地址http://172.28.34.131:8559,是前面我设置的wsl的zen的地址,端口是我自定义的8559,随后BuildGraph的Node实际调用了本地的zen.exe执行了project-createoplog-export。其中project-create和oplog-export在目标存在的时候会exit 1这是zen设计如此,可以增加--force或者-f来避免返回1,在实际使用上不存在特殊的差别。

对于zen.exe oplog-export --embedloosefiles --zen http://172.28.34.131:8559 --hosturl http://localhost:8558 --target-project lyrastartergame.oplog --target-oplog lyra.dev.00002.windows LyraStarterGame.247567c8 Windows,解释一下几个参数的注释

  • --embedloosefiles:功能描述: “Export additional files referenced by path as attachments”(将通过路径引用的附加文件导出为附件)
  • --zen:目标zen的地址和端口,remote的,中心的那个
  • --hosturl:本机的zen
  • --target-project:目标的project的name,指的是zen里的project,后面有图解释
  • --target-oplog
    • lyra.dev.00002.windows:本地的要导出到目标的oplog的id,这个id是BuildGraph中计算后传递给Cook节点的,可以高度自定义
    • LyraStarterGame.247567c8:本地的zen的project的name,是uproject名字和路径的hash计算出的,计算逻辑在Zen的Node对应的cs的代码里
    • Windows:本地cook的platform作为oplop的name,本地cook会持续更新这个oplog

Remote Zen仪表盘

zen可以通过访问url/dashboard看到一些信息。

当cook完全结束的时候,可以在远程的http://172.28.34.131:8559/dashboard/?看到如下一些东西:

image.png

可以看到上面是不同oplog的projects,如果你在一台中心的zen上存放了不同的project就会有多个了
下面是ddc的namespace,有一个引擎的和一个lyra本身的,至于为什么叫ue4我不知道

我们进入lyrastartergame.oplog看一下

image.png

可以看到上面info表示的是档期啊project的信息,其中remote上只有id,因为下面都是路径相关的

下方的oplogs是我在进行测试的时候上传的三个不同id的Snapshot

进入最新的一个lyra.dev.00002.windows查看一下

点进去之后默认是list视图,我直接截图了tree视图

image.png

可以看到content下的tree结构,和对应资源的size和rawsize信息

进入一个具体的贴图/game/characters/heroes/mannequin/textures/manny/t_manny_01_msk看一下:

image.png

这就是一个完整的资源的数据和属性了,可以看到data里是uasset和ubulk

而本地zen,http://localhost:8558/dashboard/?page=project&project=LyraStarterGame.247567c8中看到的如下

image.png

这里能看到本地的项目的一些路径信息,oplogs是按照平台进行持续写入的

关于oplog、Snapshot不在这里赘述具体的定义。

总结

Zen可以作为Shared DDC的后端服务。

Zen可以将cook数据进行cas存储节省空间,需要在项目中配置Use Zen Server as cooked output storeUse to Store,cook将不会产生散文件或者pak。

Zen可以通过oplog进行快照导出,从一个Zen同步到另一个Zen,在多设备进行cook的时候可以通过导入不同CI产生的Snapshot,尽量让打包机的cook进行incremental,跳过更多packages,减少cook时间。

Zen可以在浏览器中访问dashboard看到不同oplog的具体信息。

Zen的Export操作和Import操作在使用UAT进行的时候是非异步的,同步完需要的差异数据之后才会继续。

其他

关于Zen的其他东西且待下回分解


UE5 :Zen as Cook Storage 探究和实例测试
http://muchenhen.com/posts/4823/
作者
木尘痕
发布于
2026年1月13日
许可协议