版本管理与灰度发布

用生活化的比喻,让你从"手动 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):每周12次。中版本(MINOR):每月1次。大版本(MAJOR):每季度半年。节奏取决于团队规模和玩家反馈。

Q2:灰度阶段出问题怎么办?

立即暂停灰度,回滚到旧版本。不需要等灰度走完。灰度的目的就是提前发现问题。

Q3:热更新和全量更新怎么选?

Bug 修复和数值调整用热更(快、不影响玩家)。新功能和大改动用全量更新(稳定、完整)。纯 H5 游戏可以全部热更。

Q4:微信小游戏的版本管理有什么特殊?

微信小游戏有代码包大小限制(主包 4MB)。大更新需要用户手动更新(弹窗提示)。建议:主包尽量小,资源走 CDN 热更新。

Q5:怎么减少强更次数?

核心策略:协议设计时留足扩展空间。新增功能用新消息 ID,不修改旧消息。资源用热更而非打包进客户端。

Q6:灰度发布失败了怎么快速回滚? A: 1)K8s 用 rollout undo 秒级回滚;2)Nginx 灰度直接改配置 reload;3)游戏热更新可以让客户端回退到上一版本资源。关键是回滚时间要小于 5 分钟。


实践任务

  1. 制定版本规范:版本号规则 + 发版节奏 + 评审流程
  2. 实现灰度发布:按用户 ID 百分比分流到新旧版本
  3. 实现配置热更新:服务端推送新配置,客户端即时生效
  4. 实现协议兼容:服务端同时支持 v3 和 v4 协议
  5. 灰度回滚演练:灰度阶段发现问题,5 分钟内回滚
  6. 设计热更新方案:包含版本管理、差分包、MD5 校验、回滚机制

初学者常见错误

错误1:灰度发布没有监控

问题: 灰度流量进入新版本后,没有实时监控错误率。 解决: 灰度发布必须配合监控大盘,新版本指标异常立即回滚。

错误2:热更新没有版本校验

问题: 玩家下载到一半断网,游戏用了半完整的资源包,出现各种异常。 解决: 下载完成后校验 MD5,更新原子化(先下载到临时目录,校验通过再替换)。

错误3:协议变更没有版本号管理

问题: 改了协议结构但客户端没更新,导致解析错误。 解决: 协议头必须带版本号,服务端兼容多版本解析。


与其他章节的关联

本节内容 关联章节 关联点
CI/CD 第01章 CI/CD 流水线 自动化构建和部署
Docker 第02章 Docker 容器化 镜像版本管理
协议设计 3_1 第12章 游戏协议设计 协议版本兼容
监控 第06章 游戏性能监控 灰度阶段监控指标
热更新 4_1 第11章 Lua/C#脚本与热更新 热更新的技术实现

⬅️ 上一章:告警与应急响应 | ➡️ 下一章:音视频与实时通信