生态-UOS CDN与服务器部署
篇章:09-生态篇-集成与协同 状态:已完成 阅读时间:约 20 分钟
一、引言
1.1 本章在系列中的定位
YooAsset 的热更新能力离不开稳定、高效的资源服务器。服务器/CDN 部署是 YooAsset 落地的"最后一公里":无论 YooAsset 本身设计多完美,如果 CDN 选型不当、缓存策略错误、域名配置有误,玩家在下载时仍会"卡 99%"或"下不动"。
本章系统讲解 YooAsset 的服务器/CDN 部署方案,包括 UOS(Unity Online Service)、阿里云 CDN、腾讯云 CDN、AWS CloudFront 等主流选择,并给出可落地的部署配置。
1.2 本章要解决的核心问题
- 主流 CDN 厂商如何选型?
- CDN 的缓存策略如何配置?
- 服务器端版本目录如何组织?
- 如何优化带宽、降低 CDN 成本?
1.3 阅读前置要求
- 了解 YooAsset 的清单与 Bundle 概念
- 了解 HTTP 协议、HTTPS 证书
- 了解至少一家云厂商(阿里云/腾讯云/AWS)
二、CDN 选型
2.1 主流 CDN 对比
| 厂商 | 国内覆盖 | 海外覆盖 | 价格 | 特点 |
|---|---|---|---|---|
| 阿里云 CDN | ★★★★★ | ★★★ | 中 | 国内首选,节点丰富 |
| 腾讯云 CDN | ★★★★★ | ★★★ | 中 | 国内次选,南方节点优 |
| AWS CloudFront | ★★★ | ★★★★★ | 中-高 | 海外首选 |
| Cloudflare | ★★★ | ★★★★ | 低 | 免费版可用,全球加速 |
| 七牛云 | ★★★★ | ★★★ | 低 | 性价比高 |
| UOS (Unity) | ★★★ | ★★★ | 集成 Unity 生态 | 海外游戏专属 |
| 自建 CDN | 自定 | 自定 | 极高 | 极少项目 |
2.2 选型原则
2.2.1 国内项目
- 首选:阿里云 CDN + 阿里云 OSS 作为源站
- 次选:腾讯云 CDN + 腾讯云 COS
- 理由:节点最全、价格合理、运维工具完善
2.2.2 海外项目
- 首选:AWS CloudFront + S3
- 次选:Cloudflare CDN + R2
- 理由:全球节点、BGP 优化、与中国大陆隔离
2.2.3 全球项目
- 混合方案:国内用阿里云、海外用 CloudFront
- 智能解析:根据玩家 IP 解析到最近的 CDN
2.3 UOS CDN
UOS(Unity Online Service)是 Unity 官方提供的一站式资源管理服务。它为 Unity 开发者提供:
| 服务 | 说明 |
|---|---|
| CDN 分发 | 全球 CDN 节点 |
| 版本管理 | 资源版本管理后台 |
| 数据统计 | 下载量、命中率、地区分布 |
| 安全 | 防 DDoS、HTTPS |
| 与 Unity Asset Manager 集成 | Editor 内一键上传 |
UOS 适用场景:
- 海外项目,特别是 Unity Asset Manager 用户
- 中小团队,不想自建运维
- 需要全球化分发
三、CDN 配置
3.1 域名配置
3.1.1 域名结构
推荐使用多级域名结构:
资源域名
├── res-cdn.example.com ← 资源主域名(指向 CDN)
├── res-cdn-bj.example.com ← 备用域名
└── res-origin.example.com ← 源站域名(仅运维使用)
或者按版本分:
res-v1.cdn.example.com ← v1 版本
res-v2.cdn.example.com ← v2 版本
res-latest.cdn.example.com ← 最新版本
3.1.2 DNS 配置
智能解析配置(以阿里云为例):
主机记录: res-cdn.example.com
线路类型: 境内/境外/电信/联通/移动
记录值:
├── 境内 → 阿里云 CDN CNAME (res-cdn.example.com.cdn.dnsv1.com)
├── 境外 → CloudFront CNAME (d111111abcdef8.cloudfront.net)
└── 默认 → 阿里云 CDN CNAME
3.2 源站配置
3.2.1 源站选型
| 源站类型 | 适用 | 推荐 |
|---|---|---|
| OSS(阿里云) | 国内 | ★★★★★ |
| COS(腾讯云) | 国内 | ★★★★★ |
| S3(AWS) | 海外 | ★★★★★ |
| 自建 Nginx | 私有部署 | ★★ |
3.2.2 源站目录结构
oss://my-bucket/yooasset/
├── version_index.json ← 版本索引
├── v1.0.0/
│ ├── PackageManifest_1.0.0.hash
│ ├── PackageManifest_1.0.0.bytes
│ └── bundles/
│ ├── bundle_01
│ ├── bundle_02
│ └── ...
├── v1.0.1/
│ ├── PackageManifest_1.0.1.hash
│ ├── PackageManifest_1.0.1.bytes
│ └── bundles/
└── v1.0.2/
├── PackageManifest_1.0.2.hash
├── PackageManifest_1.0.2.bytes
└── bundles/
为什么按版本分子目录:
- 方便回滚:直接切换到旧版本
- 避免冲突:不同版本可以同时存在
- 灰度发布:可以同时提供多个版本
3.3 缓存策略配置
3.3.1 文件类型与缓存时间
不同文件类型应设置不同缓存时间:
| 文件类型 | 推荐缓存时间 | 原因 |
|---|---|---|
*.hash | 0 秒 | 每次必须回源 |
*.bytes(清单) | 0-300 秒 | 频繁更新,需校验 |
*.bundle | 365 天 | Hash 命名,永不过期 |
version_index.json | 0 秒 | 必须回源 |
*.html | 0 秒 | 静态页面 |
*.png(非 Bundle 内) | 7-30 天 | 中等更新频率 |
3.3.2 Hash 命名策略
YooAsset 默认使用Hash 命名(OutputNameStyle.HashName):
bundle_01 → bundle_a1b2c3d4e5f6
↑
Hash 前 12 位
优势:
- 文件名自带版本信息
- 可以永久缓存(CDN 不会缓存错版本)
- 不需要复杂的缓存刷新逻辑
3.3.3 CDN 缓存配置(阿里云为例)
缓存配置
─────────
优先级 1: 后缀 .hash
缓存时间: 0 秒
权重: 100
优先级 2: 后缀 .bytes
缓存时间: 300 秒
权重: 80
优先级 3: 文件名包含 "version_index"
缓存时间: 0 秒
权重: 90
优先级 4: 后缀 .bundle
缓存时间: 365 天
权重: 70
优先级 5: 全局默认
缓存时间: 30 天
权重: 0
3.4 HTTPS 配置
3.4.1 证书申请
| 厂商 | 证书类型 | 价格 |
|---|---|---|
| Let's Encrypt | DV SSL | 免费 |
| 阿里云 | DV/OV/EV | 100-5000 元/年 |
| 腾讯云 | DV/OV/EV | 100-5000 元/年 |
| Cloudflare | DV/OV/EV | 0-5000 元/年 |
推荐:Let's Encrypt(免费)或阿里云免费 DV 证书。
3.4.2 强制 HTTPS
# Nginx 配置
server {
listen 80;
server_name res-cdn.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name res-cdn.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
3.4.3 HSTS 配置
# 启用 HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
四、服务器端版本管理
4.1 版本目录结构
4.1.1 推荐结构
{yooasset-cdn-root}/
├── version_index.json
├── latest.txt ← 指向最新版本
├── v1.0.0/ ← 完整版本
│ ├── manifest/
│ │ ├── PackageManifest_1.0.0.hash
│ │ ├── PackageManifest_1.0.0.bytes
│ │ └── ...
│ └── bundles/
│ ├── bundle_a1b2c3d4
│ ├── bundle_e5f6g7h8
│ └── ...
├── v1.0.1/
├── v1.0.2/
├── v1.0.3/
└── v1.0.4/ ← 当前最新
4.1.2 版本索引文件
version_index.json 是版本管理的核心文件:
{
"latest": "1.0.4",
"stable": "1.0.3",
"gray": "1.0.5-rc.1",
"versions": [
{
"version": "1.0.0",
"releaseDate": "2024-01-01",
"status": "archived",
"size": 350000000
},
{
"version": "1.0.1",
"releaseDate": "2024-01-15",
"status": "archived",
"size": 360000000
},
{
"version": "1.0.3",
"releaseDate": "2024-02-15",
"status": "stable",
"size": 380000000
},
{
"version": "1.0.4",
"releaseDate": "2024-03-01",
"status": "active",
"size": 400000000
}
]
}
4.2 版本上传
4.2.1 上传脚本
public class VersionUploader
{
public static async Task UploadVersion(string buildOutputDir, string version)
{
var cdnRoot = "https://res-cdn.example.com/yooasset/";
var targetUrl = $"{cdnRoot}v{version}/";
// 1. 上传 manifest 文件
var manifestDir = Path.Combine(buildOutputDir, "manifest");
await UploadDirectory(manifestDir, $"{targetUrl}manifest/");
// 2. 上传 bundle 文件
var bundlesDir = Path.Combine(buildOutputDir, "bundles");
await UploadDirectory(bundlesDir, $"{targetUrl}bundles/");
// 3. 更新 version_index.json
await UpdateVersionIndex(cdnRoot, version);
// 4. 更新 latest.txt
await UpdateLatestFile(cdnRoot, version);
// 5. CDN 缓存刷新
await RefreshCDNCache($"{targetUrl}manifest/");
}
static async Task UpdateVersionIndex(string cdnRoot, string newVersion)
{
// 读取现有索引
var indexUrl = $"{cdnRoot}version_index.json";
var currentIndex = await GetJsonAsync<VersionIndex>(indexUrl);
// 添加新版本
currentIndex.Latest = newVersion;
currentIndex.Versions.Add(new VersionInfo
{
Version = newVersion,
ReleaseDate = DateTime.UtcNow,
Status = "active",
Size = CalculateVersionSize()
});
// 上传
var newIndexJson = JsonSerializer.Serialize(currentIndex);
await UploadText($"{cdnRoot}version_index.json", newIndexJson);
}
}
4.2.2 CDN 缓存刷新
文件上传到 OSS 后,CDN 节点可能仍是旧版本。需主动刷新:
public class CDNCacheRefresher
{
public static async Task RefreshAsync(string urlPath, string vendor = "aliyun")
{
switch (vendor)
{
case "aliyun":
await RefreshAliyunCDN(urlPath);
break;
case "tencent":
await RefreshTencentCDN(urlPath);
break;
case "aws":
await RefreshAWSCloudFront(urlPath);
break;
}
}
static async Task RefreshAliyunCDN(string urlPath)
{
// 调用阿里云 CDN API
var request = new RefreshCachesRequest
{
ObjectPath = urlPath
};
var response = await _aliyunClient.GetAcsResponse(request);
}
}
4.3 版本回滚
public class VersionRollback
{
public static async Task RollbackToVersion(string targetVersion)
{
var cdnRoot = "https://res-cdn.example.com/yooasset/";
// 1. 读取 version_index.json
var index = await GetVersionIndex(cdnRoot);
// 2. 验证目标版本存在
if (!index.Versions.Any(v => v.Version == targetVersion))
{
throw new Exception($"版本 {targetVersion} 不存在");
}
// 3. 切换 latest
index.Latest = targetVersion;
await UploadVersionIndex(cdnRoot, index);
// 4. 更新 latest.txt
await UploadText($"{cdnRoot}latest.txt", targetVersion);
// 5. 刷新 CDN 缓存
await CDNCacheRefresher.RefreshAsync($"{cdnRoot}version_index.json");
await CDNCacheRefresher.RefreshAsync($"{cdnRoot}latest.txt");
}
}
五、带宽优化
5.1 传输压缩
5.1.1 HTTP 压缩
CDN 默认支持 Gzip / Brotli 压缩。对文本资源启用:
text/* → Gzip / Brotli
application/json → Gzip / Brotli
application/octet-stream → 不压缩
注意:YooAsset 的 Bundle 已经是二进制压缩格式(LZ4/LZMA),无需再启用 Gzip,重复压缩反而浪费 CPU。
5.1.2 Bundle 压缩算法选择
YooAsset 构建时的压缩选择:
| 算法 | 压缩率 | CPU 消耗 | 推荐 |
|---|---|---|---|
| Uncompressed | 1.0x | 0 | 几乎不推荐 |
| LZ4 | ~1.5x | 低 | 推荐:速度快,压缩率尚可 |
| LZMA | ~2.5x | 高 | 不推荐(构建+解压慢) |
5.1.3 自定义压缩
如果项目对体积极度敏感,可以自定义压缩:
public class CustomCompressor
{
public static byte[] Compress(byte[] data)
{
// 使用 Brotli(比 Gzip 高 10-20% 压缩率)
using var output = new MemoryStream();
using var brotli = new BrotliStream(output, CompressionLevel.Optimal);
brotli.Write(data, 0, data.Length);
return output.ToArray();
}
}
5.2 分片下载
5.2.1 HTTP Range 分片
YooAsset 已支持 HTTP Range 断点续传。CDN 必须正确响应 Range 请求:
# Nginx 配置 - 启用分片下载
location /yooasset/ {
root /var/www/cdn;
add_header Accept-Ranges bytes; # 关键:声明支持 Range
}
5.2.2 客户端分片合并
public class ShardDownloader
{
public async Task DownloadShards(string url, int shardCount)
{
// 1. HEAD 请求获取文件大小
var headRequest = UnityWebRequest.Head(url);
await headRequest.SendWebRequest();
long totalSize = long.Parse(headRequest.GetResponseHeader("Content-Length"));
// 2. 计算分片
var shardSize = totalSize / shardCount;
var shards = new List<(long start, long end)>();
for (int i = 0; i < shardCount; i++)
{
long start = i * shardSize;
long end = (i == shardCount - 1) ? totalSize - 1 : (i + 1) * shardSize - 1;
shards.Add((start, end));
}
// 3. 并行下载
var shardData = new ConcurrentDictionary<int, byte[]>();
var tasks = shards.Select(async (shard, index) =>
{
var data = await DownloadShard(url, shard.start, shard.end);
shardData[index] = data;
});
await Task.WhenAll(tasks);
// 4. 合并
return shardData.Values.SelectMany(d => d).ToArray();
}
}
5.3 P2P 加速
对于超大型 MMO 项目(如 10GB+ 资源),可以引入 P2P 加速:
P2P 加速架构
─────────
├── Tracker 服务器(协调 P2P 连接)
├── CDN 源站(兜底)
├── 玩家 A(已下载)
├── 玩家 B(已下载)
└── 玩家 C(下载中)
│
├── 从 CDN 下载 30% 数据
├── 从 A/B 各下载 35% 数据
└── 合并得到完整数据
YooAsset 的 P2P 集成:
- 替换
IFileSystem实现 - 自定义下载逻辑
- 与现有 CDN 配合作为兜底
六、服务器部署最佳实践
6.1 推荐部署架构
[最终用户]
│
▼
[智能 DNS]
│
├── 国内 → 阿里云 CDN
└── 海外 → CloudFront
│
▼
[CDN 边缘节点]
│
├── 命中缓存 → 直接返回
└── 未命中 → 回源
│
▼
[源站 - OSS/S3]
│
└── 静态文件存储
6.2 容灾方案
6.2.1 多 CDN 备份
public class MultiCDNLoadBalancer
{
private readonly List<string> _cdnUrls = new()
{
"https://cdn1.example.com",
"https://cdn2.example.com",
"https://cdn3.example.com"
};
public async Task<byte[]> DownloadWithFallback(string filePath)
{
// 并行尝试多个 CDN,最快的赢
var tasks = _cdnUrls.Select(async cdn =>
{
try
{
return await DownloadFromCDN(cdn, filePath);
}
catch
{
return null;
}
});
var results = await Task.WhenAll(tasks);
return results.FirstOrDefault(r => r != null);
}
}
6.2.2 OSS 跨区域复制
OSS Bucket (主) → OSS Bucket (备)
华东1(杭州) 华东2(上海)
↓ 自动复制
异地区域
6.3 安全考虑
6.3.1 防盗链
// 客户端请求时携带加密签名
public class SignatureService
{
public string GenerateSignedUrl(string filePath, long expireTime)
{
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string key = "your-secret-key";
// 计算签名
string sign = ComputeMD5($"{filePath}{timestamp}{key}");
// 拼接签名 URL
return $"{filePath}?sign={sign}&t={timestamp}";
}
}
CDN 配置防盗链(以阿里云为例):
Referer 防盗链:
├── 白名单:*.example.com
└── 黑名单:恶意域名
URL 鉴权:
└── 签名算法:MD5(Timestamp + 文件路径 + 密钥)
6.3.2 访问控制
CDN 访问控制
├── IP 黑名单:封禁恶意 IP
├── 地域限制:仅允许中国大陆访问
├── UA 限制:仅允许移动 UA
└── 限速:单 IP 限速 10MB/s
6.4 监控与告警
6.4.1 关键指标
| 指标 | 告警阈值 |
|---|---|
| CDN 命中率 | < 90% 告警 |
| 5xx 错误率 | > 0.1% 告警 |
| 平均下载时长 | > 5s 告警 |
| 带宽使用 | > 80% 配额告警 |
| 缓存刷新失败 | 立即告警 |
6.4.2 监控工具
- 阿里云监控:免费,提供详细图表
- Prometheus + Grafana:开源,灵活
- Datadog:商业,强大
- Sentry:错误监控
七、实战部署案例
7.1 案例一:MMO 项目
MMO 项目部署
────────────
[用户层]
├── iOS / Android / PC
└── Web 客户端
[CDN 层]
├── 国内:阿里云 CDN(华东、华北、华南)
├── 海外:CloudFront(美西、美东、欧洲、东南亚)
└── 多 CDN 备份:腾讯云 CDN
[源站层]
├── 国内主源:阿里云 OSS(华东 1)
├── 国内备源:阿里云 OSS(华东 2)
└── 海外源:AWS S3
[监控层]
├── 阿里云监控 + Grafana
├── Sentry 错误收集
└── 自研业务监控
7.2 案例二:休闲小游戏
休闲小游戏部署
──────────────
[用户层]
└── 微信 / 抖音 / QQ
[CDN 层]
├── 微信:微信 CDN(白名单)
├── 抖音:抖音 CDN
└── 通用:腾讯云 CDN
[源站层]
└── COS(腾讯云)
[成本]
├── 月下载量:10TB
├── CDN 费用:约 2000 元/月
└── 源站存储:约 50 元/月
八、总结
8.1 本章要点回顾
- CDN 选型:国内阿里云/腾讯云,海外 CloudFront,全球混合
- 缓存策略:Hash 文件不缓存、Bundle 永久缓存、版本索引不缓存
- 版本管理:每个版本独立目录,通过 version_index.json 切换
- 带宽优化:HTTP Range 分片、Bundle 压缩(LZ4)、多 CDN 备份
8.2 与前后章节的关联
- 前章:09-03 解决了与其他框架的集成
- 本章:解决"资源如何发布到玩家手中"的部署问题
- 后章:10 章开始进入实战篇,从零搭建 YooAsset 项目
8.3 实践建议
- 域名要稳定:尽量使用
cdn.example.com这种稳定域名,避免改域名 - Hash 命名:YooAsset 启用 Hash 命名 + 永久缓存策略
- 多 CDN 备份:避免单 CDN 故障导致全服停服
- 监控先行:上线第一天就要有监控,没有监控就是裸奔
- 回滚预案:每次发布都要预留回滚能力,并演练
上一篇:与GameFramework/xAsset等框架集成 下一篇:环境搭建与集成