从 GitLab CI 到 Horde:一个 UE5 项目的构建系统迁移复盘
从 GitLab CI 到 Horde:一个 UE5 项目的构建系统迁移复盘
前言
我们项目是一款基于 Unreal Engine 5(UE5)的大型游戏。在使用 Horde 之前,项目的构建流程主要依赖 GitLab CI;更早也接触过 Jenkins,也了解了一下 TeamCity。真正使用 Horde 半年多之后,我们对它的优点、局限和落地成本有了更具体的认识。
这篇文章不是 Horde 部署教程,也不是 Horde、TeamCity、Jenkins 与 GitLab CI 的完整横向对比。
迁移前
我们早期使用 GitLab CI 的方式比较直接:通过 YAML 描述流水线,再在流水线中执行 UE 相关命令。
当时主要构建的是 PC 版 Editor 的 Precompiled Binaries(给美术、策划使用的预编译 Editor 包),完整游戏包体的产出并不频繁。多数时候 GitLab CI 承担的是“构建并分发 Editor”的职责。
这个阶段的问题不是 GitLab CI 完全不能用,而是它对 UE 项目的构建语义理解很弱。最初我们对 BuildGraph 的认知也不充分,更多是通过命令行串起需要的步骤,比如编译 Editor、Cook、编译 Game 等。随着项目复杂度上升,流水线里逐渐混入了更多项目自定义的代码生成、预处理与构建前后置逻辑。这些逻辑散落在 YAML、cmd、脚本和 API 调用中,维护成本开始上升。
GitLab CI 阶段还有几个比较典型的问题:
第一,YAML 本身的维护体验一般。普通服务端项目的流水线或许还好,但 UE 项目的构建步骤多、参数多、平台多,再叠加项目自定义逻辑后,YAML 很容易变成难读难改的脚本编排。
第二,项目代码和美术资产统一使用 Perforce 管理,而 Git 仓库基本只是作为 GitLab CI pipeline 的入口存在。这意味着 GitLab CI 并没有真正承载版本控制,只是在 P4 旁边提供了一个构建执行入口。
第三,Perforce 工作空间的同步、UGS badge、构建产物分发、流水线触发等都需要额外处理。比如 workspace sync 要自己实现,UGS badge 需要单独对接,流水线触发也依赖 API 和自定义机器人。
所以,当时的 GitLab CI 解决了“能不能自动构建 Editor”的问题,但没有很好地解决“如何管理一个 UE 项目的构建工作流”的问题。
为什么选择 Horde
当时我们也考虑过 TeamCity,但没有实际试用。最终选择 Horde 的原因比较直接:它是 Epic 官方方案,也是 Epic 内部使用的构建系统。
对 UE 项目来说,这一点有很强的吸引力。Jenkins、GitLab CI、TeamCity 都是通用 CI/CD 系统,可以通过脚本或插件适配 UE;而 Horde 的设计起点就是 UE 工作流,天然围绕 BuildGraph、Perforce、UGS、UBA、Agent、Stream、Template 这些概念展开。
这并不意味着 Horde 在所有方面都比通用 CI 更成熟。实际上,后续的使用体验也证明,它在一些通用产品化能力上并不完善。但从选型角度看,如果项目本身已经使用 Perforce,并且希望把 UE 构建、UGS、构建产物分发、远程编译等能力整合起来,Horde 的方向是合理的。
我们当时的迁移,并不是从 Git 工作流切换到 Perforce 工作流。项目从一开始就用 Perforce 管理代码和资产,Git 仓库仅用于触发 GitLab CI。迁移到 Horde,本质上是将构建执行入口从 GitLab CI 迁移到一个更贴近 P4 / UE / UGS 的系统里。
3. 迁移后:构建流程被“推”进 BuildGraph
迁移到 Horde 后,我们目前主要用它处理几类任务:
- Editor PCB 构建;
- Windows 平台打包;
- PS5 平台打包;
- 代码提交后自动触发 Editor 构建。
每日构建目前仍然依赖独立的定时程序触发,自动化测试暂时还没有接入。
Horde 带来的最大变化,并不是 Web UI 换了,也不是触发按钮从 GitLab 变成了 Horde,而是构建流程的中心从 YAML / cmd 迁移到了 BuildGraph。
Horde 的架构基本要求通过 BuildGraph 实际驱动构建任务。即使某些底层逻辑仍然是 cmd、bat 或项目自定义脚本,也需要组织进 BuildGraph 节点里。换句话说,使用 Horde 之后,BuildGraph 基本绕不开。
早期在 GitLab CI 里,我们可以把命令直接塞进 YAML 里执行;迁移到 Horde 后,必须把流程显式地建模成 BuildGraph 的节点、依赖、参数和产物。这个过程一开始会增加学习成本,但后续带来的结构化收益比较明显。
对 UE 项目来说,BuildGraph 比 YAML + cmd 更适合描述复杂构建流程。它可以更自然地表达节点依赖、平台差异、构建产物、条件分支和 UGS badge。结合 C# 自定义 Task,也可以比较方便地处理项目特定逻辑。
即使不使用 Horde,只是把原来的 cmd 流程迁移到 BuildGraph,构建流程也会清晰很多。但如果只用 GitLab CI + BuildGraph,仍然没有 Horde 方便,因为 Horde 额外接管了 P4 workspace、构建调度、Artifact、UGS 分发和 UBA 等平台层能力。
Horde 做得比较好的地方
半年多的使用下来,Horde 比较明显的收益主要集中在 UE 工作流整合上。
第一,P4 workspace 相关的事情被平台吸收了一部分。以前流水线里需要关心 workspace 如何同步、构建机本地状态如何维护、命令在哪个目录执行。迁移到 Horde 后,这些事情至少有一部分从业务脚本中剥离了出去,使用者可以更专注于构建任务本身。
第二,UGS 结合更自然。Editor PCB 的构建结果、badge 和构建状态可以更贴近团队日常使用的 UE 工具链,而不是只停留在 CI 页面里。
第三,Artifact 管理体验比较好。Horde Server 基于 CAS,对重复内容比较友好,也有 GC 机制。对于 UE 项目这种大产物场景,这比简单将文件堆在共享目录里更合适。通过 UGS 分发 Editor PCB 时,整体速度和体验也比较满意。
第四,UBA 是一个很明确的加分项。相比之前使用的编译加速工具,UBA 更贴合 UE 项目。接入后 Editor 编译时间有明显下降,而且几乎没有维护成本。由于 UBA 本身依赖 Horde 调度 Compute 资源,如果已经引入 Horde,UBA 基本可以视为一个很值得顺势接入的能力。
从这个角度看,Horde 最值得肯定的地方,不是它作为一个通用 CI 产品有多成熟,而是它把 BuildGraph、Perforce workspace、UGS、Artifact 分发、UBA 这些 UE 大项目真正需要的能力串在了一起。单独看每一块,它未必都完美;但组合起来之后,它确实比在通用 CI 上自己拼装一套 UE 工作流更接近目标形态。
部署和使用中遇到的坑
Horde 的官方属性给了我们选型信心,但落地时也能明显感受到:官方不等于低门槛。
很多细节并不在文档中,需要看示例、读源码,甚至通过实际运行行为去确认。Template 本身因为有 example,理解成本不算特别高;真正耗时的是 Server 部署、Agent 配置,以及一些没有充分暴露配置项的行为。
认证和用户管理不够开箱即用
Horde 提供了几种认证方式,但对我们来说都不是特别方便。它自身也没有提供一个简单完整的账号管理系统,而我们实际需要的只是比较基础的管理员 / 普通用户管理。
这类问题会让人感觉 Horde 更像是默认你已经有一套内部认证体系,或者团队能接受它现有的认证方式。对于只想简单管理用户权限的团队来说,这部分体验并不算完整。
Windows Agent 的运行身份问题
Agent 注册后,我们遇到过几次 workspace 更新失败的问题,其中包括编码相关问题。
另一个更典型的问题是 Windows Service 的默认运行身份。Agent 作为 Windows 系统服务运行时,默认以 Local System 身份运行,这会导致 BuildGraph 中某些操作出现权限问题。普通命令行下能跑通的流程,服务化之后不一定能跑通,因为用户身份、网络权限、磁盘权限、环境变量都变了。
我们最后通过手动在 cmd 中启动 Agent 的方式绕过了这个问题。
自定义 UGS 更新需要理解 Horde 的分发机制
UGS 更新可以通过 Horde Server 分发,但这里涉及 bundled 工具。如果团队对 UGS 做过修改,就需要理解 Horde Server 如何提供和更新这些工具。
我们最后是通过阅读源码理解相关机制,再结合 Horde Server 配置和 API 解决。这个问题本身不是完全不能处理,但不属于非常直观的配置项。
Pool 和 Agent 分组存在隐性规则
Horde 的 pool 默认通过配置文件里的 condition 来匹配 Agent。我们一开始希望自定义 Agent 所属 pool,后来翻代码才发现,可以将 condition 设置为 null,再手动给 Agent 添加 pool。
最终我们通过修改 Horde Server 配置,把相关 condition 覆盖为 null,然后手动管理 Agent 所属 pool。
这个例子比较典型:有些能力不是没有,但它未必通过文档或 UI 很清楚地暴露出来。
日志体验仍有不足
日志方面主要有两个问题。
一个是 Web 上中文可能出现乱码,对中文团队来说,这会影响排查体验。虽然有ESP的帖子回复说可以通过culture=en来修改语言,但是实际测试还是会有部分中文log出现,并且不是我们自己写的log而是引擎的log。
另一个是实时日志问题。由于 Serilog 的缓冲机制,网页上有可能过一段时间后就看不到实时日志。如果希望从根上调整,需要修改 Server 代码并重新部署。考虑到修改 Horde Server 源码会引入后续升级维护成本,我们没有选择动这一块。
部分配置写死在代码中
有些上传 Artifact 的任务属于 Horde 自身代码,超时时间是写死的。如果需要调整,也必须修改源码。
对于希望把 Horde 当成成熟通用 CI 产品使用的团队来说,这一点需要提前预期。
前端状态没有按用户隔离
Horde Server 前端网页有一些状态不是按用户隔离的。比如色彩模式,一个人改了之后,其他人再次访问时也会受到影响。
不要盲目照搬官方 BuildGraph Template
迁移过程中还有一个比较重要的经验:BuildGraph 节点并不是拆得越细越好。
UE 官方给出的 template 中,有些流程会由一个 Agent 编译完 Editor,然后上传产物,再由其他 Agent 下载产物继续执行后续节点。这个模型在设计上是合理的,也更模块化。但在我们的内网环境下,实际效果并不理想。
如果一个 Agent 编译完成后,需要先上传到 Horde Server,再由另一个 Agent 下载继续执行,网络和存储吞吐就会变成瓶颈。在我们的环境里,这个传递过程的耗时甚至超过了同一台 Agent 本地继续执行的时间。
所以我们最后没有完全照搬官方 template,而是把一个完整 Build 流程尽量收敛到同一个 Agent 节点中,避免不同 Agent 接力导致大量中间产物传递。
BuildGraph 和 Horde 允许我们拆节点、做依赖、做并行,但大型 UE 项目的中间产物并不轻。如果网络、存储、Horde Server 吞吐跟不上,过度拆分反而会吞掉并行化收益。
Horde 的 Agent 调度不够精细
Horde 的 Agent 调度主要基于 pool。这个模型可以表达“哪些机器能跑哪些任务”,但不太适合表达“哪些机器是稀缺资源,应该优先留给特定任务”。
我们遇到过一个典型例子。
当时有几台机器属于 GameBuild pool,其中部分机器安装了 PS5 SDK。一次 GameBuild 流水线需要在同一个 CL 上同时产出 Windows 和 PS5 包。当时一台 PS5 打包机正在执行其他任务,Horde 还可以调度另外两台机器:一台普通 GameBuild 机器,一台同时具备 GameBuild 和 PS5 能力的机器。
理想情况下,Windows 任务应该分配给普通 GameBuild 机器,把具备 PS5 能力的机器留给 PS5 构建。但实际调度中,Windows 任务被分配到了同时具备 GameBuild 和 PS5 能力的机器上,导致另一台普通机器长时间闲置,而 PS5 构建任务反而在等待设备。
这个问题本质上不是某一次调度偶然不理想,而是 Horde 的 pool 模型不太适合表达稀缺资源优先级。
我们最后没有试图让调度器变得更聪明,而是反过来调整构建机能力:让 GameBuild pool 内的机器尽量都具备多平台构建能力。这样即使 Horde 调度不够精细,也不会因为某台特殊机器被占用而导致平台任务长时间等待。这个方案牺牲了一部分机器配置成本,但换来了调度稳定性。
因此,我现在对 Horde Agent 调度的理解是:它更适合调度一组能力相对同质的 Agent,而不是精细管理稀缺构建资源。如果团队有主机平台 SDK、特殊设备、特殊许可证或特殊硬件,需要提前设计 pool 和构建机能力,不能完全指望调度器自动做最优选择。
关于 TeamCity
我们当时考虑过 TeamCity,但没有实际试用,所以这里不做 Horde 和 TeamCity 的结论性比较。
从定位上看,TeamCity 是成熟的通用 CI/CD 平台,并且现在也有 Unreal Engine 相关支持。它在用户体系、权限管理、日志展示、Agent 指定、构建配置维护等方面,理论上可能会比 Horde 更接近传统 CI 产品体验。
但这些只是基于产品定位的推测。没有在真实 UE 项目中试过 TeamCity,就不能简单判断它一定更适合或一定不适合。
我个人仍然对 TeamCity 的 UE 方案保持好奇,尤其是它在 Agent 指定、稀缺构建机调度、权限和日志体验上的表现。但在这篇文章里,我只讨论我们实际使用过的 Horde,不做没有实践支撑的横评。
什么情况下考虑 Horde
重要前提:项目是否主要使用 Perforce。
Horde 对 P4 workspace、CL、构建同步、UGS 这套工作流支持得更自然。如果代码和资产都在 P4 上,Horde 的很多能力可以顺着用起来。如果代码在 Git、资产在 P4,那么 Horde 仍然可以用,但 Git 侧同步、版本对应关系、触发逻辑就需要团队自己处理,整体价值会被打折。
UGS 是加分项,但不是绝对前提。如果团队使用 UGS,Horde 的 badge、PCB 分发、构建状态反馈会更完整;如果不用 UGS,Horde 仍然可以作为 BuildGraph 驱动的构建调度系统存在。
项目规模方面,我不认为只有大项目能用 Horde。小项目也可以用,只是可能用不到它的全部能力。大项目也撑得住,因为 Horde 本来就是面向复杂 UE 工作流设计的。
真正需要判断的是团队是否已经需要这些能力:
- 稳定产出 Editor PCB;
- Windows / 主机平台打包;
- Perforce workspace 自动管理;
- UGS badge 和构建结果反馈;
- 构建产物分发与清理;
- 远程编译加速;
- 可维护的 BuildGraph 构建流程。
如果这些需求已经出现,Horde 就值得考虑。
如果已经使用 Horde,建议同时考虑接入 UBA。对 UE 项目来说,这是 Horde 体系里非常有价值的一部分。
迁移是否值得
对我们项目来说,迁移到 Horde 是比较值得的。
即使不使用 Horde,只是把原来 GitLab CI 里的 cmd 流程迁移到 BuildGraph,体验也会比纯脚本拼装更好。BuildGraph 能让 UE 构建流程更结构化,也更容易表达节点、依赖、平台和产物。
但只用 GitLab CI + BuildGraph,仍然没有 Horde 方便。Horde 额外处理了 Perforce workspace、任务调度、Artifact、UGS 分发和 UBA,这些能力对 UE 项目很关键。
当然,Horde 也不是一个开箱即用、各方面都成熟的通用 CI 产品。它在账号管理、权限控制、日志体验、前端状态隔离、部分配置项暴露、Agent 精细调度等方面都有不足。实际落地时,团队需要接受一定的踩坑成本,也要有阅读源码、理解实现细节、通过配置或流程规避问题的准备。
Horde 更像 UE 构建基础设施,而不是普通 CI 产品的替代品。
如果你的项目主要在 P4 上,并且需要 BuildGraph、UGS、Editor PCB 分发、平台打包、Artifact 管理和 UBA,那么 Horde 的方向是对的。它未必在所有产品细节上都让人舒服,但它解决的是 UE 大项目真正会遇到的问题。
如果你的项目只是想简单跑几条构建命令,或者代码主要托管在 Git 上,又没有 UGS / UBA / PCB 分发需求,那么 Horde 的接入成本可能会显得偏高。
对我们来说,Horde 不是完美方案,但它比继续在 GitLab CI 上堆 YAML、cmd 和 API 更符合 UE 项目的长期构建需求。