版本管理与灰度发布
用生活化的比喻,让你从"手动 FTP 部署"到"能安全、可控地发布游戏版本"
前置知识:第01章 CI/CD 流水线(自动化构建部署)
阅读指南(初学者必看)
为什么你需要学习版本管理与灰度发布?
游戏发版的痛点:
- 发版后出 Bug → 影响所有玩家 → 紧急回滚
- 热更新配置错误 → 玩家看到异常数据
- 客户端和服务端版本不匹配 → 协议错误
- 强制更新太频繁 → 玩家流失
学完本章,你能回答:
- 语义化版本号怎么用?游戏版本有什么特殊点?
- 灰度发布有哪些策略?怎么选择?
- 热更新怎么管理?怎么回滚?
- 客户端和服务端版本兼容怎么保证?
- Nginx 和 K8s 怎么实现灰度?
本文结构
第一部分:语义化版本号
第二部分:灰度发布策略
第三部分:热更新管理
第四部分:版本兼容策略
一、语义化版本号
生活类比:版本号就像"手机的系统版本"——大更新改大号,小修复改小号,你知道要不要更新。
版本号格式
MAJOR.MINOR.PATCH
MAJOR:不兼容的 API 变更(强更)
例:协议变更、资源格式变更、新引擎版本
MINOR:向后兼容的功能新增(非强更)
例:新英雄、新地图、新活动
PATCH:向后兼容的问题修复(热更)
例:Bug 修复、数值调整、配置更新
游戏版本特殊点
游戏有三个独立的版本号:
1. 客户端版本:1.2.3(App Store/应用市场看到的)
2. 协议版本:5(客户端和服务端通信协议的版本)
3. 资源版本:2026042301(CDN 上的资源版本号)
版本兼容矩阵:
客户端 v1.0 + 协议 v3 → 服务器 v1.0 (协议v3) ✅
客户端 v1.0 + 协议 v3 → 服务器 v1.1 (协议v4) ✅(服务器向后兼容)
客户端 v1.1 + 协议 v4 → 服务器 v1.0 (协议v3) ❌(新功能不支持)
二、灰度发布策略
生活类比:灰度发布就像"试营业"——先让少部分人体验,没问题再全面开放。
四种灰度策略
| 策略 | 方式 | 适用场景 | 风险 |
|---|---|---|---|
| 白名单 | 指定账号先行 | 内测阶段 | 最低 |
| 百分比 | 1%→5%→10%→50%→100% | 正式发布 | 低 |
| 地区 | 先在某地区发布 | 区域性测试 | 中 |
| 服务器 | 部分服务器先更新 | 多服架构 | 中 |
灰度发布流程
1. 白名单灰度(1~2小时)
→ 内部测试账号使用
→ 验证核心功能正常
2. 1% 灰度(2~4小时)
→ 随机 1% 用户使用新版本
→ 监控错误率、延迟、崩溃率
3. 10% 灰度(4~8小时)
→ 扩大到 10%
→ 关注支付、战斗等核心功能
4. 50% 灰度(2~4小时)
→ 一半用户使用新版本
→ 对比新旧版本指标
5. 100% 全量
→ 所有用户使用新版本
→ 继续监控 24 小时
灰度监控指标
每个灰度阶段必须监控:
- 错误率:新版本 vs 旧版本
- 延迟:P50/P99 对比
- 崩溃率:前端崩溃次数
- 内存:是否有内存泄漏
- 支付:充值成功率是否正常
- 核心功能:登录/匹配/战斗成功率
- 玩家反馈:客服投诉量
Nginx 灰度发布配置
按 Cookie 灰度:
upstream backend_stable {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
upstream backend_canary {
server 192.168.1.20:8080;
}
server {
listen 80;
location / {
# 检查 Cookie 中的灰度标记
if ($http_cookie ~* "canary=true") {
proxy_pass http://backend_canary;
break;
}
# 按用户 ID 哈希,10% 流量进入灰度
set $gray_flag 0;
if ($arg_userId ~* "^[0-9]$") {
set $gray_flag 1;
}
if ($gray_flag = 1) {
proxy_pass http://backend_canary;
}
proxy_pass http://backend_stable;
}
}
K8s 滚动更新与灰度
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-server
spec:
replicas: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # 最多多启动 2 个 Pod
maxUnavailable: 1 # 最少保持 9 个 Pod 可用
template:
spec:
containers:
- name: game-server
image: game-server:v2.0
# 健康检查决定何时继续更新
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
三、热更新管理
生活类比:热更新就像"不停机修车"——车子还在跑,你换了某个零件。
热更新类型
| 类型 | 内容 | 更新方式 | 速度 |
|---|---|---|---|
| 代码热更 | Lua/JS 脚本 | 替换脚本文件 | 快(秒级) |
| 配置热更 | 数值/活动配置 | 推送新配置 | 快(秒级) |
| 资源热更 | 图片/音频/UI | CDN 差异包 | 慢(分钟级) |
| 客户端更新 | 原生代码 | 应用商店更新 | 最慢(小时~天) |
热更新流程
1. 制作更新包
- 代码:git diff → 增量包
- 配置:导出新配置 JSON
- 资源:对比资源版本 → 差异包
2. 上传到 CDN
- 更新包 → CDN
- 更新版本号配置
3. 通知客户端
- 推送更新通知
- 或下次启动时检查
4. 客户端处理
- 下载差异包
- 校验完整性(MD5/SHA256)
- 应用更新
- 重启/刷新
5. 验证
- 确认更新生效
- 监控错误率
热更新系统设计
// 客户端热更新检查
class HotUpdateManager {
async checkUpdate() {
const localVersion = await this.getLocalVersion();
const remoteVersion = await fetch('/api/version/latest').then(r => r.json());
if (remoteVersion.code > localVersion.code) {
// 需要更新
const diffList = await fetch(`/api/version/diff?from=${localVersion.code}&to=${remoteVersion.code}`)
.then(r => r.json());
for (const file of diffList) {
await this.downloadFile(file.url, file.md5);
}
await this.saveVersion(remoteVersion);
return { updated: true, needRestart: remoteVersion.needRestart };
}
return { updated: false };
}
async downloadFile(url, expectedMd5) {
const response = await fetch(url);
const blob = await response.blob();
const actualMd5 = await this.calculateMd5(blob);
if (actualMd5 !== expectedMd5) {
throw new Error(`MD5 校验失败: ${url}`);
}
await this.saveFile(url, blob);
}
}
热更新回滚
回滚流程:
1. 切换 CDN 版本号指向旧版本
2. 客户端检测到"新"版本(实际是旧版本)
3. 下载旧版本差异包
4. 应用"旧"版本覆盖
注意:
- 代码热更回滚通常很简单
- 配置热更需要确认旧配置兼容当前代码
- 资源热更回滚数据量大,需要预下载
- 更新原子化:先下载到临时目录,校验通过再替换
四、版本兼容策略
协议版本兼容
// 服务端兼容多版本协议
@Service
public class MessageRouter {
private final Map<Integer, MessageHandler> handlers = new HashMap<>();
public void handle(GameMessage message) {
int protocolVersion = message.getHeader().getVersion();
MessageHandler handler = handlers.get(protocolVersion);
if (handler == null) {
// 降级处理:使用最低支持版本
handler = handlers.get(MIN_SUPPORTED_VERSION);
}
handler.handle(message);
}
}
// 新增字段用默认值(Protobuf 天然支持)
// 删除字段不影响旧协议解析
// 修改字段类型 = 不兼容(必须升级协议版本)
数据兼容
数据库兼容:
1. 新增列:DEFAULT 值,旧代码忽略新列 ✅
2. 删除列:旧代码查询会报错 → 需要代码先适配 ❌
3. 修改列类型:需要数据迁移 ❌
推荐:只增不删,旧列标记为 @Deprecated
大版本更新时统一清理
自问自答
Q1:多久发一个版本合适?
小版本(PATCH):每周1
2次。中版本(MINOR):每月1次。大版本(MAJOR):每季度半年。节奏取决于团队规模和玩家反馈。
Q2:灰度阶段出问题怎么办?
立即暂停灰度,回滚到旧版本。不需要等灰度走完。灰度的目的就是提前发现问题。
Q3:热更新和全量更新怎么选?
Bug 修复和数值调整用热更(快、不影响玩家)。新功能和大改动用全量更新(稳定、完整)。纯 H5 游戏可以全部热更。
Q4:微信小游戏的版本管理有什么特殊?
微信小游戏有代码包大小限制(主包 4MB)。大更新需要用户手动更新(弹窗提示)。建议:主包尽量小,资源走 CDN 热更新。
Q5:怎么减少强更次数?
核心策略:协议设计时留足扩展空间。新增功能用新消息 ID,不修改旧消息。资源用热更而非打包进客户端。
Q6:灰度发布失败了怎么快速回滚? A: 1)K8s 用 rollout undo 秒级回滚;2)Nginx 灰度直接改配置 reload;3)游戏热更新可以让客户端回退到上一版本资源。关键是回滚时间要小于 5 分钟。
实践任务
- 制定版本规范:版本号规则 + 发版节奏 + 评审流程
- 实现灰度发布:按用户 ID 百分比分流到新旧版本
- 实现配置热更新:服务端推送新配置,客户端即时生效
- 实现协议兼容:服务端同时支持 v3 和 v4 协议
- 灰度回滚演练:灰度阶段发现问题,5 分钟内回滚
- 设计热更新方案:包含版本管理、差分包、MD5 校验、回滚机制
初学者常见错误
错误1:灰度发布没有监控
问题: 灰度流量进入新版本后,没有实时监控错误率。 解决: 灰度发布必须配合监控大盘,新版本指标异常立即回滚。
错误2:热更新没有版本校验
问题: 玩家下载到一半断网,游戏用了半完整的资源包,出现各种异常。 解决: 下载完成后校验 MD5,更新原子化(先下载到临时目录,校验通过再替换)。
错误3:协议变更没有版本号管理
问题: 改了协议结构但客户端没更新,导致解析错误。 解决: 协议头必须带版本号,服务端兼容多版本解析。
与其他章节的关联
| 本节内容 | 关联章节 | 关联点 |
|---|---|---|
| CI/CD | 第01章 CI/CD 流水线 | 自动化构建和部署 |
| Docker | 第02章 Docker 容器化 | 镜像版本管理 |
| 协议设计 | 3_1 第12章 游戏协议设计 | 协议版本兼容 |
| 监控 | 第06章 游戏性能监控 | 灰度阶段监控指标 |
| 热更新 | 4_1 第11章 Lua/C#脚本与热更新 | 热更新的技术实现 |
⬅️ 上一章:告警与应急响应 | ➡️ 下一章:音视频与实时通信