22 KiB
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
- NOS 桶信息
VPS_SSH_KEY
文档
垃圾电脑服务
我现在想在局域网里的另一台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
-
- 有关 Horde 的架构和组件的信息,以及部署它们的最佳实践。
受众: IT、系统管理员、打算修改 Horde 的程序员。
- 有关 Horde 的架构和组件的信息,以及部署它们的最佳实践。
-
- Configuring and Operating Horde
-
- 描述如何设置和管理 Horde。
**受众:**构建/开发运营团队、管理员。
- 描述如何设置和管理 Horde。
-
- Horde Internals
-
- 描述如何构建和修改 Horde 及其架构。
**受众:**希望扩展 Horde 的开发人员。
- 描述如何构建和修改 Horde 及其架构。
-
- Deploying Horde
- 视频
下载地址
- 官方下载地址
大致步骤
- 使用docker部署服务。
- 服务器配置。
- 代理机配置。详见 http://10.219.103.35:13340/docs/Landing.md
- 配置文件位置:安装目录\Agent\Defaults\agent.json
部署笔记
部署流程
Perforce服务器
ssl:inner02-commit.perforce.nie.netease.com:1667
OIDC
Netease OIDC信息
| client id | 4884b03e951711f0ad370242ac120002 |
| client secret | f2862b8c5ab24085ab0883d119b631604884b354951711f0ad370242ac120002 |
OIDC 参考代码
#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"</br><h2>OpenID Connect 鐧诲綍鎴愬姛銆<E5A79B></h2></br>"
u"鎮ㄧ殑鐢ㄦ埛鍚嶆槸锛<E6A7B8>%s</br>"
u"鎮ㄧ殑鍏ㄥ悕鏄細%s</br>"
u"鎮ㄧ殑閭鏄細%s</br>"
u"鎮ㄧ殑鑱屼綅鏄細%s</br>"
u"鎮ㄧ殑閮ㄩ棬鏄細%s</br>"
u"鎮ㄧ殑宸ュ彿鏄細%s</br>") % (
session['username'], session.get('fullname', ''),
session.get('email', ''), session.get('title', ''),
session.get('dep', ''), session.get('empno', ''))
body += u"<a href='/login'>鎴虫垜閲嶆柊鐧诲綍</a>"
return body
else:
return u"<a href='/login'>鎴虫垜鐧诲綍</a>"
@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
{
"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 添加:
[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 添加:
{
"plugins": {
"analytics": {
"stores": [{
"id": "engine",
"include": ["$(HordeDir)/Defaults/default-metrics.telemetry.json"]
}]
}
}
}
优先级 3: DDC 共享缓存(多人协作时有价值)
价值: 避免每台机器重复编译 Shader、重复 Cook Asset
两种方案:
- 使用 Horde 内置 DDC 端点 — Horde Server DDC 插件已加载,配置 UE 指向即可
- 部署独立 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 注册 |
已知问题
- Nginx 未配置: 当前 nginx 容器使用默认配置,未做反向代理。如需 HTTPS 需重新配置。
- MapViewOfFile 分页错误: 本机编译时偶发 9-38 次 paging error,可能需增大 page file。
- Agent HIH-D-X13864 (远程) 经常 Offline: 该 Agent 状态不稳定,可能需要检查 HordeAgent 服务自动重启。
- OIDC 已移除: 当前 Anonymous 模式无访问控制,内网使用可接受,外网暴露需重新配置认证。