# IP - TA.Netease.com - SSH:10.145.96.67:32200 - ssh -p 32200 -i C:\Users\loujiajie\.ssh\netease_loujiajie_id_rsa loujiajie@10.145.96.67 - Artlib - SSH:10.145.96.68:32200 - ssh -p 32200 -i C:\Users\loujiajie\.ssh\netease_loujiajie_id_rsa loujiajie@10.145.96.68 - Artlib S3 - NOS 桶信息 - 名称:matrixaita - 项目:artct - 成本项目:artct - 区域:GA - Endpoint:gzdev - 桶用户:p-artct-matrixaita - 域名(内网):`nos-gzdev.163nos.com` - Access Key:57SCV9Q4MPLXQ3JCL5K9 - Secret Key:4dzMcakyxW2vEhCGjEfiXHtDgxZUiy57D9NqKOOm VPS_SSH_KEY ## 文档 - S3:https://sa.nie.netease.com/console/webconsole/idc/ # 垃圾电脑服务 我现在想在局域网里的另一台Ubuntu电脑上部署Unreal Horde服务,存储 缓存以及管理各个worker、节点信息;本机性能强劲,作为worker进行具体工作。 - 电脑ip:10.219.36.57 - 用户名:netease - 密码:123 官方文档(UE5.5) - https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-in-unreal-engine?application_version=5.5 - Horde README.md https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Programs/Horde/README.md - [**Deploying Horde**](https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Programs/Horde/Docs/Deployment.md) - - 有关 Horde 的架构和组件的信息,以及部署它们的最佳实践。 **受众:** IT、系统管理员、打算修改 Horde 的程序员。 - [**Configuring and Operating Horde**](https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Programs/Horde/Docs/Config.md) - - 描述如何设置和管理 Horde。 **受众:**构建/开发运营团队、管理员。 - [**Horde Internals**](https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Programs/Horde/Docs/Internals.md) - - 描述如何构建和修改 Horde 及其架构。 **受众:**希望扩展 Horde 的开发人员。 - 视频 - [Horde and Unreal Build Accelerator: Operating at Epic Scale](https://youtu.be/ZUlwqNbYWBQ?si=DgFvrvepK67v-iTi) ## 下载地址 - 官方下载地址 - 服务端:https://github.com/EpicGames/UnrealEngine/releases/download/5.5.0-release/UnrealHordeServer.msi # 大致步骤 1. 使用docker部署服务。 2. 服务器配置。 1. 默认端口Http 13340、Http 2 13342。 1. 默认情况下,Horde配置为使用端口5000通过未加密的HTTP提供数据。在默认情况下,代理通过端口5002使用未加密的HTTP/2上的gRPC与Horde服务器通信。这些设置在服务器启动时显示在控制台上。 2. [相关配置](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-orientation-for-unreal-engine?application_version=5.5) - [服务器配置](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-settings-for-unreal-engine?application_version=5.5#%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%AE%BE%E7%BD%AE)将配置该服务器与其他服务器的通信,定义静态参数等。它由与该服务器一同部署的 `Server.json` 文件驱动。 - [完整配置参数清单](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-settings-for-unreal-engine?application_version=5.5#%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%AE%BE%E7%BD%AE) - [全局配置](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-schema-for-unreal-engine?application_version=5.5#globals)在部署后控制所有面向用户的元素,该配置存于名为 `Globals.json` 的文件中。设置好部署参数后,大多数配置都在此处完成。 3. [验证相关配置](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-authentication-tutorial-for-unreal-engine?application_version=5.5) 1. [[#OIDC身份验证]] 3. 代理机配置。详见 http://10.219.103.35:13340/docs/Landing.md 1. 配置文件位置:安装目录\Agent\Defaults\agent.json 2. # 部署笔记 ## 部署流程 - [Horde服务器](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-server-for-unreal-engine?application_version=5.5) # Perforce服务器 ssl:inner02-commit.perforce.nie.netease.com:1667 # OIDC ## Netease OIDC信息 | | | | ------------- | ---------------------------------------------------------------- | | client id | 4884b03e951711f0ad370242ac120002 | | client secret | f2862b8c5ab24085ab0883d119b631604884b354951711f0ad370242ac120002 | ## OIDC 参考代码 ```python #coding:UTF-8 """ Requirements: 1. Flask >= 0.10.1 2. requests 3. jwkest >= 1.1.7 Usage: python oidc_code_demo.py -H {listen_address} -p {listen_port} Help: python oidc_code_demo.py -h """ import os import uuid from hashlib import md5 import datetime from urllib import urlencode import json import requests from flask import Flask, request, jsonify, session, redirect from jwkest.jwk import SYMKey from jwkest.jws import JWS from jwkest.jwk import load_jwks_from_url from jwkest.jws import NoSuitableSigningKeys __revision__ = "0.01" __author__ = "chenxs@corp.netease.com" OIDC_CLIENT_ID = "" OIDC_CLIENT_SECRET = "" OIDC_PROVIDER = "https://login.netease.com/connect" OIDC_AUTHORIZATION_SERVER = "https://login.netease.com/connect/authorize" OIDC_TOKEN_ENDPOINT = "https://login.netease.com/connect/token" OIDC_USERINFO_ENDPOINT = "https://login.netease.com/connect/userinfo" OIDC_SCOPE = "openid nickname email fullname dep title empno" OIDC_REDIRECT_URI = "https://127.0.0.1:5000/finish" OIDC_JWKS_URI = "https://login.netease.com/connect/jwks" OIDC_ALG = "HS256" PYTHON_OIDC_DEMO = Flask(__name__) @PYTHON_OIDC_DEMO.route("/", methods=['GET']) def index(): """index""" if 'username' in session: body = ( u"

OpenID Connect 鐧诲綍鎴愬姛銆�


" u"鎮ㄧ殑鐢ㄦ埛鍚嶆槸锛�%s
" u"鎮ㄧ殑鍏ㄥ悕鏄細%s
" u"鎮ㄧ殑閭鏄細%s
" u"鎮ㄧ殑鑱屼綅鏄細%s
" u"鎮ㄧ殑閮ㄩ棬鏄細%s
" u"鎮ㄧ殑宸ュ彿鏄細%s
") % ( session['username'], session.get('fullname', ''), session.get('email', ''), session.get('title', ''), session.get('dep', ''), session.get('empno', '')) body += u"鎴虫垜閲嶆柊鐧诲綍" return body else: return u"鎴虫垜鐧诲綍" @PYTHON_OIDC_DEMO.route("/login", methods=['GET']) def login(): """AuthN Request""" session.clear() now = datetime.datetime.now().strftime("%s") session['uid'] = uuid.uuid4().hex session['state'] = session['uid'] session['nonce'] = md5(session['uid'] + now).hexdigest() authn_request_params = { 'response_type': 'code', 'client_id': OIDC_CLIENT_ID, 'state': session['state'], 'nonce': session['nonce'], 'scope': OIDC_SCOPE, 'redirect_uri': OIDC_REDIRECT_URI, #'prompt': 'login', 'display': 'touch', } redirect_url = "?".join([ OIDC_AUTHORIZATION_SERVER, urlencode(authn_request_params)]) return redirect(redirect_url) def token_request(code): """2. Token Request""" params = { 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': OIDC_REDIRECT_URI, 'client_id': OIDC_CLIENT_ID, 'client_secret': OIDC_CLIENT_SECRET, } _resp = requests.post(OIDC_TOKEN_ENDPOINT, data=params) return json.loads(_resp.text) def id_token_verify(id_token, nonce=None): """3. id token verify""" now = int(datetime.datetime.now().strftime("%s")) if OIDC_ALG == "HS256": signed_keys = [SYMKey(key=OIDC_CLIENT_SECRET)] else: signed_keys = load_jwks_from_url(OIDC_JWKS_URI) try: plain_id_token = JWS().verify_compact(id_token, signed_keys) except NoSuitableSigningKeys: # logger the id_token please return {'error': 'can not verify the id token'} print "idtoken: %s" % plain_id_token if nonce: if (not plain_id_token.has_key('nonce')) or ( plain_id_token['nonce'] != nonce): return {'error': 'id token nonce not correct'} if plain_id_token['iss'] != OIDC_PROVIDER: return {'error': 'id token iss not correct'} if plain_id_token['aud'] != OIDC_CLIENT_ID: return {'error': 'id token aud not correct'} if now >= int(plain_id_token['exp']): return {'error': 'id token expired'} return {'id_token': plain_id_token} @PYTHON_OIDC_DEMO.route("/finish", methods=['GET']) def finish(): """ 1. AuthN Response 2. Token Request 3. id token verify 4. userinfo request 5. login user """ # 1. AuthN Response try: code = request.args.get('code') if session['state']: state = request.args.get('state') if state != session['state']: return u"闈炴硶璇锋眰" except ValueError: return u"闈炴硶璇锋眰" # 2. Token Request token = token_request(code) print "token: %s" % token if token.has_key('error'): return u"鍑洪敊浜嗭細%s" % str(token) # 3. id token verify id_token_verified = id_token_verify(token['id_token']) if id_token_verified.has_key('error'): return id_token_verified['error'] else: id_token = id_token_verified['id_token'] print "id_token: %s" % id_token # 4. userinfo request _req_session = requests.Session() _req_session.headers.update({ "Authorization": "Bearer %s" % token['access_token']}) userinfo_req = _req_session.get(OIDC_USERINFO_ENDPOINT) userinfo = json.loads(userinfo_req.text) # login the user session['username'] = userinfo['nickname'] session['email'] = userinfo['email'] session['title'] = userinfo.get('title', '') session['empno'] = userinfo.get('empno', '') session['dep'] = userinfo.get('dep', '') session['fullname'] = userinfo.get('fullname', '') return redirect("/") if __name__ == "__main__": import sys import argparse parser = argparse.ArgumentParser( usage='%(prog)s [options]', version='%(prog)s ' + str(__revision__)) parser.add_argument( '-H', '--host', dest='host', type=str, help="Specify listening adress, default is 127.0.0.1") parser.add_argument( '-p', '--port', dest='port', type=int, help="Specify listening port, default is 5000") parser.add_argument( '-c', '--client_id', dest='client_id', type=str, help="oidc client_id is required.") parser.add_argument( '-s', '--client_secret', dest='client_secret', type=str, help="oidc client_secret is required.") args = parser.parse_args() host = args.host or '127.0.0.1' port = args.port or 5000 OIDC_REDIRECT_URI = "http://%s:%s/finish" % (host, port) OIDC_CLIENT_ID = args.client_id OIDC_CLIENT_SECRET = args.client_secret if not OIDC_CLIENT_ID or not OIDC_CLIENT_SECRET: parser.print_help() parser.exit() PYTHON_OIDC_DEMO.secret_key = "this is a random secret" PYTHON_OIDC_DEMO.debug = True PYTHON_OIDC_DEMO.run(host=host, port=port) ``` # 本机部分 根据搜索结果,你通过 Claude 部署 Unreal Horde 的工作主要在以下目录进行: ## 主要工作目录 ┌──────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐ │ 目录 │ 内容 │ ├──────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ C:\Users\loujiajie\horde-deploy\ │ 部署脚本主目录 — 26 个 Python 脚本 + docker-compose.yml + 配置文件 │ ├──────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ C:\Users\loujiajie\docs\superpowers\ │ 设计文档和实施计划(2026-04-17) │ └──────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘ ## 部署目标 - 远程 Ubuntu 服务器 10.219.36.57:Docker Compose 运行 Horde Server + MongoDB + Redis,远程路径 /home/netease/docker/UnrealHorde/ - 本地 Windows Agent:注册在 C:\Users\loujiajie\AppData\Local\Epic Games\Horde\Agent\,Agent ID HIH-D-X41334 ## 相关 Claude 会话 - Claude 计划文件 C:\Users\loujiajie\.claude\plans\vivid-jingling-elephant.md 记录了 UBA 远程编译加速配置 - Claude 会话记录在 C:\Users\loujiajie\.claude\projects\C--Users-loujiajie\ 下 D:\UnrealHorde\ 目录虽然存在但是空的(已被回收站中有删除痕迹),实际部署工作都在 C:\Users\loujiajie\horde-deploy\ 完成。 # 当前状态以及后续部署 > 更新日期:2026-04-22 ## 架构概览 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Horde Server (Linux Docker) │ │ 10.219.36.57 │ │ ┌───────────┐ ┌──────────────────┐ ┌───────┐ ┌──────────────┐ │ │ │ MongoDB │ │ Horde Server │ │ Redis │ │ Nginx │ │ │ │ :27017 │ │ :5001→5000 │ │ :6379 │ │ :5000(未用) │ │ │ │ (7.0.5) │ │ :5002 (gRPC) │ │(6.2) │ │ │ │ │ └───────────┘ └──────────────────┘ └───────┘ └──────────────┘ │ │ Docker Compose: /home/netease/docker/UnrealHorde/ │ │ 数据目录: ./data/server.json, ./data/globals.json │ └─────────────────────────────────────────────────────────────────────┘ │ :5001 HTTP API │ :5002 gRPC ▼ ▼ ┌──────────────────┐ ┌──────────────────────┐ │ Agent 1 (本机) │ │ Agent 2 (远程) │ │ HIH-D-X41334 │ │ HIH-D-X13864 │ │ 10.219.103.35 │ │ 10.219.32.45 │ │ Ryzen 9 9900X │ │ Ryzen 9 9900X │ │ 64GB RAM │ │ 62GB RAM │ │ Windows 11 │ │ Windows 11 │ │ UBA 发起方 │ │ UBA 远程 Worker │ │ 端口: 1345(入站) │ │ 端口: 7000-7002(入站)│ └──────────────────┘ └──────────────────────┘ ``` ## 当前已完成配置 ### Horde Server (v5.7.0-46863086) - **部署方式**: Docker Compose (horde-server + mongodb + redis + nginx) - **访问地址**: http://10.219.36.57:5001 (Dashboard + API) - **gRPC 端口**: 5002 (Agent 通信) - **认证方式**: Anonymous(免登录,已移除 OIDC) - **管理员**: loujiajie - **遥测**: MongoDB 存储,1天保留 ### Horde Agent | 属性 | Agent 1 (本机) | Agent 2 (远程) | |------|---------------|---------------| | ID | HIH-D-X41334 | HIH-D-X13864 | | IP | 10.219.103.35 | 10.219.32.45 | | CPU | AMD Ryzen 9 9900X (12C/24T) | AMD Ryzen 9 9900X (12C/24T) | | RAM | 64GB | 62GB | | GPU | RTX 5070 | RTX 5070 | | 安装路径 | `C:\Program Files\Epic Games\Horde\Agent\` | 同左 | | Server URL | 注册表配置,非 JSON | 同左 | | Pool | Win-UE5 | Win-UE5 | | 注册方式 | enrollment token | 同左 | ### UBA 远程分布式编译 - **状态**: 已验证可用 - **效果**: 91 个任务分发到远程机器,编译耗时 58.94s vs 本机 70.74s(提升约 17%) - **防火墙端口要求(双向)**: - 发起方 (本机): **1345-1400 入站** — UBA SessionServer 监听,远程 UbaClient 连回 - 远程 Worker: **7000 入站** (Compute)、**7001 入站** (UbaAgent Listen)、**7002 入站** (Proxy) - **远程机器依赖**: 已安装 MSVC Build Tools (14.44.35207) + Windows SDK ### 已加载的 Server 插件 | 插件 | 状态 | 说明 | |------|------|------| | tools | 已加载 | 工具分发(Agent、UGS 安装包等) | | build | 已加载 | CI/CD 构建(未配置 project/stream) | | storage | 已加载 | 内容存储(默认文件系统后端) | | compute | 已加载 | 远程计算/UBA(已在使用) | | symbols | 已加载 | 符号服务器(未配置) | | secrets | 已加载 | 密钥管理(未配置) | | ddc | 已加载 | Derived Data Cache(未配置) | | analytics | 已加载 | 编辑器遥测分析(未配置) | ## 配置文件位置 ### 服务器端 (10.219.36.57) - Docker Compose: `/home/netease/docker/UnrealHorde/docker-compose.yml` - Server 配置: `/home/netease/docker/UnrealHorde/data/server.json`(重启生效) - Global 配置: `/home/netease/docker/UnrealHorde/data/globals.json`(热重载) - Nginx 配置: `/home/netease/docker/UnrealHorde/nginx/nginx.conf`(当前未做反向代理) ### Agent 端 (Windows) - 安装目录: `C:\Program Files\Epic Games\Horde\Agent\` - Server URL: **通过注册表配置**,非 appsettings.json - 路径: `HKLM\SOFTWARE\Epic Games\Horde\AgentSettings` → `server` 值 - Agent 日志: `%ProgramData%\Epic Games\Horde\Agent\Logs\` ## 后续可部署功能 ### 优先级 1: Perforce CI/CD 集成(如果需要自动化构建) **价值**: 每次 P4 提交自动触发编译+测试、Preflight 预提交检测、UGS 预编译 Editor 分发 **前置条件**: 需要 Perforce Server(公司已有 `ssl:inner02-commit.perforce.nie.netease.com:1667`) **配置方式**: 修改 `globals.json` ```json { "plugins": { "build": { "perforceClusters": [{ "name": "Default", "servers": [{ "serverAndPort": "ssl:inner02-commit.perforce.nie.netease.com:1667" }], "credentials": [{ "userName": "horde-svc", "password": "xxx" }] }], "projects": [{ "id": "farming-game", "path": "farming-game.project.json" }] } } } ``` 还需要编写 `project.json` 和 `stream.json` 定义构建任务(可参考 `/app/Defaults/ue5.project.json` 模板)。 **注意**: Horde 仅支持 Perforce Streams,不支持传统分支。 ### 优先级 2: Editor Telemetry / Analytics(零外部依赖) **价值**: 可视化团队编译时间、Shader 编译、Editor 启动时间等指标 **UE 项目端** — `Config/DefaultEngine.ini` 添加: ```ini [StudioTelemetry.Provider.HordeAnalytics] ProviderModule=AnalyticsET APIKeyET=HordeAnalytics.Dev APIServerET=http://10.219.36.57:5001/ APIEndpointET=api/v1/telemetry/engine UsageType=EditorAndClient ``` **Horde 端** — `globals.json` 添加: ```json { "plugins": { "analytics": { "stores": [{ "id": "engine", "include": ["$(HordeDir)/Defaults/default-metrics.telemetry.json"] }] } } } ``` ### 优先级 3: DDC 共享缓存(多人协作时有价值) **价值**: 避免每台机器重复编译 Shader、重复 Cook Asset **两种方案**: 1. **使用 Horde 内置 DDC 端点** — Horde Server DDC 插件已加载,配置 UE 指向即可 2. **部署独立 Zen DDC Server** — 更适合大规模团队,独立服务 **评估**: 当前 2 台机器做 C++ 编译为主,DDC 主要优化 Shader/Asset 场景。团队扩大后优先考虑。 ### 优先级 4: UGS (UnrealGameSync) **价值**: 美术无需本地编译 Editor,直接从 Horde 下载预编译二进制 **前置条件**: 必须先配好 Perforce CI/CD 集成(需要 Incremental Build 任务产出 PCB) **安装**: Horde Dashboard → Tools → Downloads 下载 UGS 安装包,安装时选 "Horde" 并填写 Server URL ### 优先级 5: 其他(按需) | 功能 | 何时需要 | 配置复杂度 | |------|---------|-----------| | **Symbol Server** | 需要分析 crash dump 时 | 低 — globals.json 加 symbols store | | **Secrets 管理** | 启用 CI/CD 需要存凭据时 | 低 — globals.json 加 secrets 条目 | | **Storage 外部后端** | 构建产物/日志量大时 | 中 — 可配 S3/Azure/NFS 替代本地磁盘 | | **Compute 网络优化** | 多网段 Agent 时 | 低 — globals.json 按 CIDR 配置网络 | | **HTTPS + OIDC** | 需要安全认证时 | 高 — 需配 nginx 反向代理 + SSL + OIDC redirect_uri 注册 | ## 已知问题 1. **Nginx 未配置**: 当前 nginx 容器使用默认配置,未做反向代理。如需 HTTPS 需重新配置。 2. **MapViewOfFile 分页错误**: 本机编译时偶发 9-38 次 paging error,可能需增大 page file。 3. **Agent HIH-D-X13864 (远程) 经常 Offline**: 该 Agent 状态不稳定,可能需要检查 HordeAgent 服务自动重启。 4. **OIDC 已移除**: 当前 Anonymous 模式无访问控制,内网使用可接受,外网暴露需重新配置认证。