用 CloudFront + Lambda@Edge 实现“可回滚、可观测”的灰度发布

用 CloudFront + Lambda@Edge 实现“可回滚、可观测”的灰度发布

关键词:灰度发布 / canary / CloudFront / Lambda@Edge / 一致性分桶 / 粘性 Cookie / 结构化日志 / CloudWatch → 阿里云 SLS

1. 背景与目标

多前端版本(如 prebeta)并存时,我们希望满足:

  • 按比例放量(如 20% → 50% → 100%),可随时回滚;
  • 白名单直达:内测/验证用户始终走 beta
  • 新用户首访抽签 + 粘性:一旦抽中 beta 就持续命中;
  • 强观测:结构化日志,能一眼看出“今天的新用户里有多少进了 beta、粘性命中率、不同 host/路径的命中分布”等。

最终基于 CloudFront + Lambda@Edge 实现无侵入灰度,结合 CloudWatch Logs → 阿里云 SLS 做跨区聚合分析,分钟级可见发布效果。


2. 方案总览

flowchart TD
  subgraph Client
    U[用户请求<br/>Host/UA/Cookie]
  end
  subgraph CloudFront
    VR[Viewer-Request Lambda@Edge<br/>版本决策+打日志]
    OR[回源/静态对象路由]
    VP[Viewer-Response Lambda@Edge<br/>Set-Cookie hello-traffic]
  end
  subgraph Storage/Logs
    CW[CloudWatch Logs]
    SLS[阿里云 SLS(可选)]
  end

  U --> VR --> OR --> VP --> U
  VR -- console.log(JSON) --> CW
  CW -. 定时同步 .-> SLS

核心思想:请求先到 Viewer-Request,我们在这里决定该走 pre 还是 beta,并把结果透传到请求头 hello-traffic响应阶段由 Viewer-Response 把这个值写入同名 Cookie,从而形成粘性。同时在 Viewer-Request 打结构化 JSON 日志,方便后续统计分析。


3. 版本判定策略(四步)

  1. 白名单(Cookie hello-version 等于 beta) → 强制走 beta
  2. 粘性(已有 hello-trafficpre/beta) → 按已有值转发
  3. 首访抽签(无粘性):按 TRAFFIC_RATIO 做一次 1..100 分桶,命中 beta 比例(使用 <= 判断)。
  4. 其他异常情况 → 回落到 pre

我们还支持多种 seed 模式random(每次请求独立抽签) / ip / ua / ip+ua。生产更推荐 ipip+ua 以增强一致性


4. 分桶与粘性:稳定 1..100

为避免“比例抖动”,对一致性 seed(如 ip|ua)采用 FNV-1a 32 位哈希,再 mod 100 + 1 得到 1..100 的稳定桶号:

function fnv1a32(str) {
  let h = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    h ^= (str.charCodeAt(i) & 0xFF);
    h = (h * 0x01000193) >>> 0; // 16777619
  }
  return h >>> 0;
}
function bucket1to100(seed) {
  return (fnv1a32(seed) % 100) + 1; // 1..100
}
  • bucket <= TRAFFIC_RATIO → 命中 beta;否则走 pre
  • seed 取值:ip / ua / ip+ua(更稳定)/ random(每次变化)。

5. 关键代码(节选)

5.1 Viewer-Request:决策 & 路由改写 & 日志

要点

  • 仅当 hello-version === beta 才视为白名单;
  • hello-traffic(粘性)优先级高于抽签;
  • 结构化 JSON日志,字段包含:host/path/client_ip/ua_hash/variant/reason/bucket/traffic_ratio/is_new_user 等;
  • 将选中的版本写进请求头 hello-traffic,便于响应阶段“种 Cookie”。

你现有的完整实现已具备以上逻辑。

从请求头读取 hello-traffic,写入同名 Cookie(SameSite=Lax; Secure; Max-Age=90d),保持用户后续访问的稳定命中


6. 日志格式与示例

每次 Viewer-Request 输出一行 JSON,类似:

{
  "ts": "2025-11-13T07:39:11.121Z",
  "date_utc": "2025-11-13",
  "date_sgt": "2025-11-13",
  "host": "www.baidu.com",
  "path": "/sw-2025-08-20.js",
  "qs": "",
  "client_ip": "38.181.86.98",
  "ip_mask": "38.181.86.x",
  "ua_hash": "1184172738",
  "wl_cookie_name": "hello-version",
  "wl_cookie_value": "",
  "sticky_cookie_name": "hello-traffic",
  "sticky_cookie_value": "pre",
  "variant": "pre",
  "reason": "sticky",
  "seed_mode": "random",
  "bucket": "",
  "traffic_ratio": 50,
  "is_new_user": false
}

is_new_user 判定:是否已有粘性 Cookie(hello-traffic)。没有则为 true


7. 观测:CloudWatch → 阿里云 SLS(可选)

把 CloudWatch 原始事件按 NDJSON 落本地,再只抽取 INFO 后的 JSON 推送到 SLS:

  • 去重:基于 eventId 做本地去重;
  • 多区汇总:多 Region 拉取后统一推送;
  • 分钟级可见:支撑灰度观察与回滚决策。

你的工具脚本:sync_cloudwatch_logs.py(拉取)、parse_and_push.py(解析+推送)、config.py(账号/Region/函数名/窗口设置)、utils.py(日志打印)与 run.py


8. SLS 查询示例(今天的新用户、各版本占比)

image

((is_new_user:'true'))| select variant as ver, count(1) as users group by ver order by users desc

注意:SLS 字段类型为字符串时,布尔值请以字符串比较(如 is_new_user:="true")。


9. 灰度策略:怎么放量更稳

  • 20% 起步:观察 30~60 分钟,关注 4 指标
    1. 新用户 beta 占比(接近 TRAFFIC_RATIO);
    2. 错误率/5xx 变化;
    3. 性能(TTFB、静态命中率);
    4. 关键路径(/home /index.html /sw.js 等)命中差异。
  • 逐步加到 50% / 80%:每一步持续观测,准备随时一键回退(把 TRAFFIC_RATIO 设为 0 即可)。
  • 白名单不受控:核心内测用户用 hello-version=beta,不影响统计比例。

10. 成本与安全

  • 日志成本:CloudWatch 与跨区/跨云转发都会产生成本;建议设置预算与告警;
  • IAM 最小权限:Lambda 只需 logs:CreateLogStream/PutLogEvents 到指定 LogGroup;同步脚本只要读 CloudWatch Logs;
  • Cookie 安全SameSite=Lax; Secure,如不需要前端读取,可启用 HttpOnly

11. 常见坑

  • “看起来不是 60%/50%?”
    小样本下的随机抖动是正常的;使用 ip / ip+ua 的一致性 seed,或扩大样本量统计会更接近目标比例。

  • 日志跨 Region
    Lambda@Edge 在离用户最近的边缘区域执行;对应日志可能落在多地区的 CloudWatch 里。统一拉取即可。

  • SLS 没看到数据
    通常是字段解析/去重/项目或 Logstore 配置问题。先本地打印 [PREVIEW] 看提取到的 JSON,再确认 PutLogsRequestproject/logstore 正确。


12. 上线 Checklist


13. 流程图(Mermaid)

flowchart TD
  A([开始]) --> B{hello-version === 'beta'?}
  B -- 是 --> X[路由到 beta(白名单)]
  B -- 否 --> C{hello-traffic 存在?}
  C -- 否 --> D[抽签(1..100) <= TRAFFIC_RATIO ?]
  D -- 是 --> E1[variant=beta]
  D -- 否 --> E2[variant=pre]
  E1 --> H[写请求头 hello-traffic=beta]
  E2 --> H2[写请求头 hello-traffic=pre]
  C -- 是 --> F{hello-traffic == 'beta'?}
  F -- 是 --> X
  F -- 否 --> G{hello-traffic == 'pre'?}
  G -- 是 --> Y[路由到 pre(粘性)]
  G -- 否 --> Z[路由到 pre(fallback)]
  X --> R[回源/改写路径]
  Y --> R
  Z --> R
  R --> VP[Viewer-Response: Set-Cookie hello-traffic=<variant>]

14. 结语

这套方案轻改造、强观测、可回滚。借助一致性分桶 + 粘性 Cookie + 结构化日志,你可以把“灰度”做得像开关一样可控:放量看数,数不好就回退。当 beta 在指标上稳定优于 pre 时,最终只需把 TRAFFIC_RATIO 拉满即可完成切换。

posted @ 2025-11-14 13:08  Hello_worlds  阅读(7)  评论(0)    收藏  举报