极简灰度发布实现新老风控系统切流
一、背景描述
公司风控服务即将上线全新版本(新风控),为降低全量切流带来的未知风险,决定采用灰度发布策略:
- 先让少量用户流量切入新风控系统
- 验证无误后逐步放大比例,直至全量
- 一旦异常可秒级回滚到老风控系统
约束条件:
- 入口统一由 Nginx 集群承载
- 老/新风控均为 HTTP 服务,仅 IP 端口不同
- 必须保证同一用户始终落入同一版本(用户维度一致性)
- 低延迟、无外部依赖、运维简单
二、总体思路与原理
- 在 Nginx 层通过 Lua 模块维护一张共享内存字典
key =user_name
,value =old | new
- 用户首次请求按灰度比例随机计算版本并写回字典,后续请求直接读取字典,实现永久绑定
- 通过调整灰度比例变量即可完成放量/回滚,仅 reload 即可生效
- 共享字典重启会丢失,但允许少量"重新选路";如零容忍可对接 Redis 做二级存储
三、方案优点
- 0 外部依赖,<1 ms 延迟
- 单核可扛 5w+ QPS
- 回滚只需改一行配置 + reload(秒级)
- 不改业务代码,不分库分表
四、网络拓扑与链路
User → Nginx(OpenResty) → 根据user_name查字典→
├─ old → 192.168.0.10:8080 老风控
└─ new → 192.168.0.11:8080 新风控
五、实操步骤(Step by Step)
0. 环境准备
- 安装 OpenResty(已集成 Lua 模块)
- 确认
nginx -V
含--with-http_lua_module
1. 编辑 nginx.conf(http 段顶层)
# 1. 共享内存,10 MB 约可存 80 万用户
lua_shared_dict risk_gray 10m;# 2. 上游集群
upstream risk_old { server 192.168.0.10:8080 max_fails=2 fail_timeout=10s; }
upstream risk_new { server 192.168.0.11:8080 max_fails=2 fail_timeout=10s; }
2. 在业务 location 中嵌入 Lua 逻辑
location ^~ /api/risk {access_by_lua_block {-- 取用户名(优先 header,其次参数,其次 cookie)local user = ngx.req.get_headers()["X-User-Name"]or ngx.var.arg_user_nameor ngx.var.cookie_user_nameif not user or user == "" thenngx.exit(403) -- 无用户标识直接拒绝endlocal dict = ngx.shared.risk_graylocal ver = dict:get(user)-- 首次访问:按灰度比例计算if not ver thenlocal gray_rate = 5 -- 初始 5% 可逐步放大local h = tonumber(ngx.crc32_long(user)) % 100 + 1ver = (h <= gray_rate) and "new" or "old"dict:set(user, ver, 2592000) -- 30 天过期endngx.var.target_ver = ver -- 传给 content 阶段}# 占位变量,content 阶段使用set $target_ver "";proxy_pass http://$target_ver;proxy_set_header X-Risk-Version $target_ver; -- 方便后台区分版本proxy_connect_timeout 1s;proxy_read_timeout 3s;
}
3. 灰度放量
- 观察新风控错误率、耗时、日志
- 修改
gray_rate = 10 → 20 → 50 → 100
- 每次执行:
nginx -t && nginx -s reload
4. 紧急回滚
方案 A(瞬间生效)
把 Lua 里 ver = "old"
写死,reload → 所有用户立即回老风控
方案 B(温和)
把 gray_rate
改 0,新用户全部落老版本,旧映射 30 天后自然淘汰
5. 全量切流(灰度完成)
确认 100% 无异常后,下线老风控节点,将 upstream risk_old
的 IP 改为新风控,注释掉 Lua 段,恢复普通 proxy_pass
,再次 reload,即完成全量切换。
六、监控与核对
项目 | 方法 |
---|---|
比例核对 | 打印 $target_ver 到 access_log,用 ELK/Grafana 统计 new 占比 |
性能对比 | 给新风控返回头加 X-Risk-Cost: ms ,与老版本对比 P99 |
异常告警 | 新风控错误率 >1% 立即触发告警,执行回滚脚本 |
共享内存监控 | 通过 ngx.shared.risk_gray:free_space() 定期上报,低于 20% 扩容 |
七、常见问题 FAQ
Q1 共享内存重启会丢吗?
会丢,但灰度数据允许“重新选路”;可把过期时间调到 30 天降低概率。若零容忍,把 dict:get/ set
换成 redis:get/ set
即可。
Q2 想按"订单号"而不是用户名?
把 Lua 里 user = ...
改成 order_id = ...
即可,其余逻辑不变。
Q3 灰度过程中想加白名单?
在计算 ver
前加一行
if user == "zhangsan" or user == "lisi" then ver = "new" end
Q4 后端想获取版本标识?
已通过 proxy_set_header X-Risk-Version $target_ver
透传,后台直接取 header。
八、附录:一键 reload 脚本(可选)
#!/bin/bash
set -e
nginx -t
nginx -s reload
echo "Nginx reload OK, gray_rate=$(grep gray_rate /usr/local/openresty/nginx/conf/nginx.conf | awk '{print $3}')"
九、结论
采用 “Nginx + Lua + 共享字典” 方案,可在不引入任何外部组件的前提下,实现:
- 用户维度永久绑定
- 毫秒级延迟
- 秒级回滚/放量
- 单机 5w+ QPS
整个灰度周期只需改动一个变量并 reload,运维成本低、风险可控,适合快速落地新风控系统的灰度发布需求。