22.12-灰度发布

要点

  • 灰度发布(Canary Release)是把新版本逐步推给一小部分用户,观察没问题后再全量发布
  • 目的是降低风险——新版本有 bug 也只影响少数用户
  • Workers 支持通过权重分配流量实现灰度
  • Vercel 支持通过 Preview Deployments 做灰度验证

内容

1. 什么是灰度发布

传统的部署是「一刀切」——新版本部署后,所有用户立即使用新版本。如果新版本有 bug,所有用户都受影响。

灰度发布的思路不同:

10% 用户 → 新版本(v2)
90% 用户 → 旧版本(v1)

观察一段时间后:
50% → v2
50% → v1

再观察:
100% → v2
0% → v1

如果在灰度阶段发现问题,可以立即停止灰度并回滚,影响范围很小。

2. 灰度发布的策略

常见的灰度策略:

策略说明适用场景
按比例随机分配一定比例的用户通用
按地域先在某个地区发布地域性服务
按用户组内部员工先试用功能验证
按请求头带特定 header 的请求走新版本测试

3. Cloudflare Workers 的灰度

Workers 可以通过权重分配流量:

// wrangler.jsonc
{
  "routes": [
    { "pattern": "api.example.com", "zone_name": "example.com" }
  ]
}

在 Cloudflare Dashboard 里:

Workers & Pages → 你的 Worker → Traffic → 可以配置多个版本的流量权重

但 Workers 原生的灰度能力有限。更灵活的方式是用 Cloudflare 的 Rules 或 Workers 代码自己实现。

3.1 用代码实现灰度

// src/index.ts
export default {
  async fetch(request: Request, env: Env) {
    const userId = request.headers.get('X-User-Id')
 
    // 10% 的用户走新版本
    if (shouldUseNewVersion(userId)) {
      return newApp.fetch(request, env)
    }
 
    return oldApp.fetch(request, env)
  },
}
 
function shouldUseNewVersion(userId: string | null): boolean {
  if (!userId) return false
 
  // 用 hash 保证同一用户总是走到同一版本
  const hash = hashCode(userId)
  return hash % 100 < 10  // 10% 的用户
}
 
function hashCode(str: string): number {
  let hash = 0
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i)
    hash |= 0
  }
  return Math.abs(hash)
}

4. Vercel 的灰度

Vercel 的灰度通过 Preview Deployments 和 Edge Config 实现:

4.1 Preview Deployments

每个 PR 自动部署一个预览版本,可以让特定用户先测试:

PR #123 → 预览版本 https://xxx.vercel.app

让内部团队或测试用户先访问预览版本,确认没问题后再合并到 main。

4.2 Edge Config

Vercel 的 Edge Config 是一个全局配置存储,可以用来做 feature flag:

// app/api/feature/route.ts
import { get } from '@vercel/edge-config'
 
export async function GET() {
  const newFeatureEnabled = await get('new_feature_enabled')
 
  if (newFeatureEnabled) {
    return newFeature()
  }
 
  return oldFeature()
}

在 Vercel Dashboard 里修改 Edge Config,可以即时切换 feature flag,不需要重新部署。

5. Feature Flags

Feature flag 是灰度发布的一种简化形式——用开关控制新功能是否启用:

// src/lib/feature-flags.ts
export const FLAGS = {
  NEW_CHAT_UI: 'new_chat_ui',
  AI_MODEL_V2: 'ai_model_v2',
  DARK_MODE: 'dark_mode',
}
 
export function isFeatureEnabled(flag: string, userId?: string): boolean {
  // 简单实现:按用户 ID hash 分桶
  if (!userId) return false
 
  const hash = hashCode(userId)
 
  switch (flag) {
    case FLAGS.NEW_CHAT_UI:
      return hash % 100 < 20  // 20% 的用户
    case FLAGS.AI_MODEL_V2:
      return hash % 100 < 50  // 50% 的用户
    default:
      return false
  }
}
// src/routes/chat.ts
app.post('/chat', async (c) => {
  const userId = c.get('userId')
 
  if (isFeatureEnabled(FLAGS.AI_MODEL_V2, userId)) {
    return callNewModel(c)
  }
 
  return callOldModel(c)
})

6. 灰度发布的监控

灰度期间需要重点监控:

  1. 错误率:新版本的错误率是否高于旧版本
  2. 延迟:新版本的响应时间是否正常
  3. 业务指标:转化率、用户活跃度等是否受影响
// 对比新旧版本的指标
app.use('*', async (c, next) => {
  const startedAt = Date.now()
  const version = c.get('version')  // 'v1' or 'v2'
 
  await next()
 
  const duration = Date.now() - startedAt
 
  // 记录指标
  log.info('http.request', {
    version,
    status: c.res.status,
    duration,
    path: c.req.path,
  })
})

7. 灰度发布的流程

一个典型的灰度发布流程:

1. 代码合并到 main,但不全量部署
2. 部署到 staging 环境,跑自动化测试
3. 部署到 production,但只分配 5% 流量
4. 观察 1 小时,检查错误率和业务指标
5. 没问题 → 增加到 20%
6. 观察 4 小时
7. 没问题 → 增加到 50%
8. 观察 1 天
9. 没问题 → 全量发布 100%

如果任何阶段发现问题:

立即停止灰度,回滚到旧版本
分析问题,修复后重新走灰度流程

8. 适用场景

灰度发布适合:

  1. 重大功能变更:UI 改版、架构升级
  2. 高风险改动:数据库迁移、支付逻辑变更
  3. AI 模型切换:新模型的效果需要验证
  4. 性能优化:需要对比新旧版本的性能数据

不需要灰度的场景:

  1. 小修复:文案修改、bug fix
  2. 向后兼容的改动:加字段、加接口
  3. 内部工具:用户量小,影响可控

9. 小结

灰度发布的核心:

  1. 逐步放量:从 5% → 20% → 50% → 100%
  2. 监控指标:错误率、延迟、业务指标
  3. 快速回滚:发现问题立即停止
  4. Feature Flags:简化版的灰度,用开关控制

灰度发布是降低上线风险的重要手段,特别是对 AI 项目来说,模型切换和功能变更需要谨慎验证。