09-04-生态-UOS CDN与服务器部署

生态-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 配置

智能解析配置(以阿里云为例):

makefile 复制代码
主机记录: 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 源站目录结构
python 复制代码
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):

markdown 复制代码
bundle_01 → bundle_a1b2c3d4e5f6
                ↑
                Hash 前 12 位

优势:

  • 文件名自带版本信息
  • 可以永久缓存(CDN 不会缓存错版本)
  • 不需要复杂的缓存刷新逻辑
3.3.3 CDN 缓存配置(阿里云为例)
yaml 复制代码
缓存配置
─────────
优先级 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 复制代码
# 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 配置
nginx 复制代码
# 启用 HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

四、服务器端版本管理

4.1 版本目录结构

4.1.1 推荐结构
python 复制代码
{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 是版本管理的核心文件:

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 上传脚本
csharp 复制代码
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 节点可能仍是旧版本。需主动刷新:

csharp 复制代码
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 版本回滚

csharp 复制代码
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 压缩。对文本资源启用:

bash 复制代码
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 自定义压缩

如果项目对体积极度敏感,可以自定义压缩:

csharp 复制代码
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 复制代码
# Nginx 配置 - 启用分片下载
location /yooasset/ {
    root /var/www/cdn;
    add_header Accept-Ranges bytes;  # 关键:声明支持 Range
}
5.2.2 客户端分片合并
csharp 复制代码
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 加速:

css 复制代码
P2P 加速架构
─────────
├── Tracker 服务器(协调 P2P 连接)
├── CDN 源站(兜底)
├── 玩家 A(已下载)
├── 玩家 B(已下载)
└── 玩家 C(下载中)
    │
    ├── 从 CDN 下载 30% 数据
    ├── 从 A/B 各下载 35% 数据
    └── 合并得到完整数据

YooAsset 的 P2P 集成:

  • 替换 IFileSystem 实现
  • 自定义下载逻辑
  • 与现有 CDN 配合作为兜底

六、服务器部署最佳实践

6.1 推荐部署架构

css 复制代码
[最终用户]
    │
    ▼
[智能 DNS]
    │
    ├── 国内 → 阿里云 CDN
    └── 海外 → CloudFront
    │
    ▼
[CDN 边缘节点]
    │
    ├── 命中缓存 → 直接返回
    └── 未命中 → 回源
    │
    ▼
[源站 - OSS/S3]
    │
    └── 静态文件存储

6.2 容灾方案

6.2.1 多 CDN 备份
csharp 复制代码
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 跨区域复制
scss 复制代码
OSS Bucket (主)   →  OSS Bucket (备)
   华东1(杭州)         华东2(上海)
       
   ↓ 自动复制
       
   异地区域

6.3 安全考虑

6.3.1 防盗链
csharp 复制代码
// 客户端请求时携带加密签名
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 配置防盗链(以阿里云为例):

scss 复制代码
Referer 防盗链:
├── 白名单:*.example.com
└── 黑名单:恶意域名

URL 鉴权:
└── 签名算法:MD5(Timestamp + 文件路径 + 密钥)
6.3.2 访问控制
bash 复制代码
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 项目

css 复制代码
MMO 项目部署
────────────
[用户层]
├── iOS / Android / PC
└── Web 客户端

[CDN 层]
├── 国内:阿里云 CDN(华东、华北、华南)
├── 海外:CloudFront(美西、美东、欧洲、东南亚)
└── 多 CDN 备份:腾讯云 CDN

[源站层]
├── 国内主源:阿里云 OSS(华东 1)
├── 国内备源:阿里云 OSS(华东 2)
└── 海外源:AWS S3

[监控层]
├── 阿里云监控 + Grafana
├── Sentry 错误收集
└── 自研业务监控

7.2 案例二:休闲小游戏

css 复制代码
休闲小游戏部署
──────────────
[用户层]
└── 微信 / 抖音 / 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 实践建议

  1. 域名要稳定 :尽量使用 cdn.example.com 这种稳定域名,避免改域名
  2. Hash 命名:YooAsset 启用 Hash 命名 + 永久缓存策略
  3. 多 CDN 备份:避免单 CDN 故障导致全服停服
  4. 监控先行:上线第一天就要有监控,没有监控就是裸奔
  5. 回滚预案:每次发布都要预留回滚能力,并演练

上一篇 :与GameFramework/xAsset等框架集成 下一篇 :环境搭建与集成

相关推荐
️公子12 小时前
可插拔 13 款实时棋牌对战平台实践:房间容器、Plugin SDK 与幂等账本
websocket·typescript·游戏开发·monorepo·插件架构·棋牌平台·实时对战
SmalBox1 天前
09-03-生态-与GameFramework-xAsset等框架集成
unity3d·游戏开发
SmalBox2 天前
09-02-生态-小游戏平台适配
unity3d·游戏开发
ZzT2 天前
vibe coding 做小游戏·玩法重做篇:只留自走棋、去掉羁绊,用电脑自动对局调难度
ai编程·游戏开发
SmalBox3 天前
09-01-生态-YooAsset+HybridCLR热更新方案
unity3d·游戏开发
房西杭4 天前
AttrMgr 属性管理器(传奇MMO Lua服务端)
游戏开发
超爱吃士力架5 天前
程序员的副业 IP 探索之路----小白用AI做了个游戏,发出去第一天居然有300多个人玩!!!
游戏开发
SmalBox5 天前
08-05-热更新-完整热更新流程实战
unity3d·游戏开发
只睡四小时6 天前
Three.js Sub-tick:漏判 60.7%→0
开发语言·javascript·性能优化·ecmascript·游戏开发·three.js·碰撞检测