Horde 使用补充:Artifact 去重效果与带宽瓶颈

Horde 使用补充:Artifact 去重效果与带宽瓶颈

前言

上一篇文章记录了从 GitLab CI 迁移到 Horde 的过程,以及 BuildGraph、Perforce、UGS、Artifact 和 UBA 等能力在实际项目中的使用情况。当时对 Horde 的基本判断是:它并不是一个各方面都成熟的通用 CI 产品,但作为 UE 项目的构建基础设施,整体方向符合项目需求。

经过一段时间的持续运行,一些在迁移和探索阶段不容易察觉的问题开始逐渐显现。其中最值得补充的是 Horde Artifact 的去重机制。

早期已经注意到 Horde Storage 采用了内容寻址存储,也因此默认认为不同构建版本之间的重复内容会自动得到复用。但从实际存储占用来看,这项能力并没有表现出预期中的效果。

后续重新检查 BuildGraph 配置和 CreateArtifact Task 后,才发现此前遗漏了一个重要参数:Dedupe

这个参数带来的变化很明显,但实际收益并不只取决于 Horde。产物的拆分方式、相邻版本之间的变化范围,以及 Horde Server 自身的网络吞吐,都会直接影响最终效果。

被遗漏的 Dedupe 参数

项目早期的 BuildGraph 主要参考 Unreal Engine 官方提供的 Template 进行搭建,构建、打包和 Artifact 上传流程都能够正常运行。

在这个阶段,关注点主要集中在以下几个方面:

  • 构建流程能否完整迁移到 BuildGraph;
  • Agent 是否能够稳定完成任务;
  • 构建产物能否正常上传和下载;
  • UGS 能否获取对应版本的 Editor;
  • Windows 和主机平台能否正常产出包体。

由于 Artifact 的基本功能已经能够使用,并且对 Horde Storage 的认知一直是“基于 CAS 的存储系统”,因此没有继续深入检查 CreateArtifact Task 的具体实现和全部参数。

但在官方的 BuildGraph Task 文档中,CreateArtifact 还提供了一个可选的 Dedupe 参数,其作用是复用前一个 Artifact 中已经存在的 Blob。

配置方式本身很简单:

1
2
3
4
5
6
<CreateArtifact
Name="$(ArtifactName)"
Type="$(ArtifactType)"
BaseDir="$(ArtifactBaseDir)"
Files="$(ArtifactFiles)"
Dedupe="true" />

在没有显式开启该参数时,虽然 Horde Storage 本身仍然建立在内容寻址存储之上,但从项目实际观察到的结果看,不同构建版本之间并没有获得预期中的 Blob 复用效果。每个版本的 Artifact 基本都在独立增加存储占用。

这也是探索阶段比较容易出现的一类问题:系统具备某项底层能力,并不代表当前使用的调用路径已经自动启用了这项能力。

Dedupe 又不是 CreateArtifact 的必填参数。如果主要根据官方示例完成流程搭建,而没有继续阅读 Task 文档或源码,很容易在流程已经正常运行后忽略它。

开启 Dedupe 后的存储变化

目前项目的完整包体约为 44GB,并被拆分为三千多个 Pak。日常构建频率约为每小时一次,Artifact 保留周期为 14 天。

按照当前构建频率计算,14 天内至少会积累约 140 个完整包体。如果所有版本都独立占用空间,逻辑总量至少在 6TB 以上。

开启 Dedupe 后,同一周期内这些 Artifact 的实际占用只有 800GB 多。

项目 当前情况
单个完整包体 约 44GB
Pak 数量 3000+
构建频率 约每小时一次
Artifact 保留周期 14 天
保留版本数量 至少约 140 个
未去重时的逻辑总量 至少约 6TB
开启 Dedupe 后的实际占用 800GB 多

这项收益对于高频构建的大型项目较为直接。

游戏包体本身体积较大,同时还需要保留一定数量的历史版本,用于测试、问题复现和版本回退。如果每个版本都按照完整包体独立存储,磁盘占用会随着构建频率近似线性增长。

在当前产物结构下,开启 Dedupe 后,磁盘增长速度主要由相邻版本之间实际发生变化的内容决定,而不再完全由每次构建的完整包体大小决定。

较高收益来自细粒度 Pak 拆分

Dedupe 并不是一个对所有 Artifact 都能产生相同效果的开关。

当前能够获得较高命中率,一个重要前提是包体已经拆分为三千多个 Pak。

在每小时构建一次的情况下,相邻版本之间通常只有部分代码和资源发生变化。大量 Pak 的内容在连续多个版本中保持不变,因此可以直接复用已有 Blob,只有发生变化的部分需要增加新的存储数据。

也就是说,当前的存储收益实际来自两个条件的共同作用:

  1. CreateArtifact 显式开启了 Dedupe
  2. 最终产物具有足够细且相对稳定的文件边界。

如果只有第一个条件,但最终产物仍然是少量大型文件,实际收益可能十分有限。

这也解释了为什么在最初的探索阶段,即使更早发现 Dedupe 参数,也未必能够立即得到现在的效果。当时尚未采用当前的分 Chunk 策略,包体没有被拆分为数量较多的 Pak,相邻版本之间可直接复用的文件边界远少于现在。

因此,不能将当前节省的存储空间完全归因于 Horde,也不能简单理解为增加一个参数后,任何项目都能获得相同量级的效果。

更准确的结论是:

Horde 提供了跨 Artifact 复用 Blob 的能力,而包体拆分策略决定了这项能力能够发挥到什么程度。

PS 平台的反例

PS 平台的最终产物提供了一个较为明显的反例。

该平台最终生成的主要包体是一个大型 .pkg 文件。实际测试中,即使相邻版本在内容层面的变化并不大,不同版本的 .pkgDedupe 下仍然几乎没有产生有效命中。

每个版本依然会占据接近完整包体大小的存储空间。

目前没有对 .pkg 内部的数据布局和版本间二进制差异进行进一步拆解,因此无法直接判断具体是哪一层处理导致了低命中率。但至少从最终结果来看,单一大型 .pkg 并不适合当前的 Artifact 去重方式。

这个现象也说明,CAS 识别的是最终数据中的可复用内容,而不是资源或游戏逻辑层面的“相同”。

两个版本可能只修改了少量游戏内容,但如果最终输出文件的二进制布局发生了较大变化,存储系统并不能根据业务语义判断其中哪些资源实际没有改变。

对于需要评估 Artifact 去重收益的项目,不能只关注完整包体大小,还需要关注以下问题:

  • 最终产物由多少个文件组成;
  • 文件边界在不同版本之间是否稳定;
  • 单个文件内部的少量变化是否会导致整体二进制发生较大改变;
  • 压缩、签名和平台封装后,输出结果是否仍然具有可复用性;
  • 构建频率是否足够高,使相邻版本之间保持较小差异。

因此,大文件并不天然意味着更高的去重收益。相比文件大小,产物结构和版本间的数据稳定性更重要。

对游戏分发的附带影响

细粒度 Pak 拆分带来的收益并不只体现在 Horde Artifact 存储上。

同一套产物结构后来也被用于基于 Unsync 的游戏包分发。由于相邻版本之间大量 Pak 保持不变,分发时不再需要重复传输整个完整包体,只需要处理发生变化的部分。

不过,这部分收益主要来自 Pak 拆分策略和 Unsync 的同步能力,并不属于 Horde Artifact 本身的功能,因此不适合直接计入 Horde 的优势。

两者之间更准确的关系是:相同的产物结构同时改善了 Artifact 的去重效果和游戏包的增量分发效率。

这也进一步说明,构建、存储和分发并不是完全独立的三个环节。对最终产物结构进行调整,可能同时影响多个基础设施系统的表现。

存储占用下降后,带宽成为新的瓶颈

Dedupe 解决的主要是 Artifact 长期保留时的存储占用,但并不会消除构建完成时的上传压力。

在当前环境中,多数 Agent 使用千兆内网,而 Horde Server 需要同时承接所有 Agent 的 Artifact 上传,以及客户端和其他 Agent 的 Artifact 下载。

单台 Agent 的千兆带宽看起来并不低,但 Horde Server 面对的是多台构建机的汇聚流量。

当多台 Agent 在接近的时间完成构建,并同时开始上传几十 GB 的产物时,Horde Server 如果同样只有千兆带宽,很容易成为整个链路的瓶颈。

实际运行中已经出现过由于带宽不足导致 Artifact 上传时间过长,最终触发超时的情况。

这类问题在排查时容易被误认为是以下原因:

  • Agent 状态异常;
  • Horde Server 处理能力不足;
  • Artifact Task 本身不稳定;
  • 反向代理配置存在问题;
  • 上传超时时间设置过短。

但如果服务端网络已经长时间跑满,仅调整任务配置或重试通常无法解决根本问题。

在多数 Agent 使用千兆网络的情况下,Horde Server 所在机器更适合配置至少 10Gbps,也就是万兆级别的内网收发能力。

万兆并不是 Horde 的通用最低配置,而是当前这种负载下较为合理的容量规划:

  • 单个完整产物约 44GB;
  • 构建频率较高;
  • 存在多台 Agent;
  • 多个任务可能同时上传;
  • Horde Server 还需要同时提供 Artifact 下载。

服务端网络不能只按照单台 Agent 的峰值配置,而应按照并发上传和下载产生的聚合流量配置。

万兆网络并不是唯一条件

将 Horde Server 升级为万兆网络后,网络链路的容量会得到明显改善,但网卡带宽并不是整个 Artifact 链路中唯一可能出现的瓶颈。

完整的数据路径通常还包括:

  • Agent 本地磁盘读取;
  • Agent 网络出口;
  • 交换机端口和上联链路;
  • 反向代理;
  • Horde Server 网络接口;
  • Horde Server 或存储节点的磁盘写入;
  • Artifact 后端存储;
  • 数据库及相关元数据操作。

其中任何一层吞吐不足,都可能表现为上传速度下降或任务超时。

因此,在处理 Artifact 上传问题时,更适合同时观察以下指标:

  • Horde Server 的网络收发速率;
  • Agent 的上传速率;
  • 服务端磁盘读写吞吐和延迟;
  • 同一时间的上传任务数量;
  • 反向代理和服务端是否存在连接或请求超时;
  • Artifact 存储所在磁盘的剩余容量和性能。

如果服务端已经具备万兆网络,但存储设备只能持续写入较低的速度,最终上传吞吐仍然会受磁盘限制。

反过来,如果磁盘性能足够,但 Horde Server 只有千兆入口,多 Agent 并发时仍然会首先耗尽网络带宽。

Artifact 基础设施的容量规划需要将网络和存储作为一条完整链路考虑,而不是单独提高某一项硬件规格。

对早期结论的修正

上一篇文章中曾将 Artifact 管理列为 Horde 做得比较好的部分,主要依据是 Horde Server 基于 CAS,并且具有产物管理、生命周期和分发能力。

这个判断本身没有发生根本变化,但经过后续使用,需要增加几个限制条件。

首先,底层采用 CAS,不代表当前 BuildGraph 流程已经获得了预期的跨版本去重效果。对于 CreateArtifact,需要明确检查 Dedupe 参数是否开启。

其次,去重效果并不只由 Horde 决定。产物拆分越细、文件边界越稳定、相邻版本差异越小,复用效果通常越容易体现。单一大型平台包可能几乎无法受益。

最后,存储空间和传输吞吐是两个不同问题。即使 Dedupe 大幅降低了长期磁盘占用,Artifact 在生成和分发时仍然会形成较高的瞬时网络负载。服务端需要按照多 Agent 汇聚流量进行网络规划。

因此,对 Horde Artifact 更完整的理解应当是:

Horde 提供了适合大型 UE 项目的产物存储基础,但能否真正获得较高的存储和分发效率,仍然取决于 BuildGraph 配置、产物结构,以及服务端基础设施是否匹配实际负载。

总结

这次补充主要修正了探索阶段对 Artifact 的两个认识。

一方面,Horde Storage 具备内容寻址和数据复用能力,但在 CreateArtifact 流程中,仍然需要显式开启 Dedupe,才能得到预期中的跨版本 Blob 复用效果。

在约 44GB 包体、三千多个 Pak、每小时一次构建和 14 天保留周期的情况下,至少 6TB 的逻辑包体最终只占用了 800GB 多的实际空间。这项效果主要建立在细粒度 Pak 拆分和相邻版本差异较小的基础上。

另一方面,存储占用的下降并不会同步解决 Artifact 上传时的带宽压力。多台千兆 Agent 并发上传时,Horde Server 需要承接聚合流量。对于当前规模,服务端配置万兆内网更接近合理的基础条件,否则大产物上传可能由于耗时过长而触发超时。

Horde 的不少能力都存在类似特征:架构方向比较适合 UE 项目,但真正落地后的效果,往往取决于一些没有被充分强调的配置和工程条件。

官方 Template 可以作为理解 Horde 工作流的入口,但不宜直接视为适合所有生产环境的最终方案。流程跑通之后,仍然需要结合产物结构、构建频率、Agent 数量、网络吞吐和存储成本,对每一条数据链路进行重新验证。


Horde 使用补充:Artifact 去重效果与带宽瓶颈
http://muchenhen.com/posts/13783/
作者
木尘痕
发布于
2026年8月7日
许可协议