在渐进式部署期间,每个请求根据指定的百分比随机路由到任一版本。这意味着同一用户每次请求可能收到来自不同版本的内容,可能导致**版本偏差(version skew)**问题。
版本亲和性通过基于稳定标识符确定性地将用户分配到某个版本来解决此问题,因此在渐进式部署期间,用户在页面加载和子请求中始终命中同一版本。
在发往 Worker 的入站请求上设置 Cloudflare-Workers-Version-Key 头:
curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: foo'对于给定的部署,所有版本键设置为 foo 的请求将由 Worker 的同一版本处理。平台对键进行哈希,并将结果与配置的百分比结合,确定性地分配版本——你无法选择某个键映射到哪个版本。
随着渐进式部署的推进(例如,从 10% 到 20% 再到 50%),已分配到新版本的用户的键将保持在新版本上。旧版本上的用户将随着百分比增加逐步迁移到新版本,但除非你回滚,否则不会切换回去。
你可以在从 Internet 向 Worker 发起外部请求时设置 Cloudflare-Workers-Version-Key 头,也可以在使用 service binding 从一个 Worker 向另一个 Worker 发起子请求时设置。
当你的 Worker 提供带有内容哈希文件名的静态资源(如 index-a1b2c3d4.js)时,版本亲和性尤为重要,这是大多数现代构建工具和框架的默认行为。
在渐进式发布期间,应用程序的不同版本将具有不同的资源文件名:
- 版本 A 的 HTML 引用
assets/index-a1b2c3d4.js - 版本 B 的 HTML 引用
assets/index-m3n4o5p6.js
没有版本亲和性时,用户可能收到来自版本 A 的 HTML,但当浏览器请求 index-a1b2c3d4.js 时,该请求可能被路由到版本 B——该版本没有此文件——导致 404 错误和页面损坏。
使用选择版本键中的任一方法配置版本亲和性,可确保来自同一用户的所有请求都路由到同一版本,从而完全避免此问题。
合适的版本键取决于应用程序可用的稳定标识符。你可以使用 zone 上的转换规则(Transform Rule)设置头,从请求中提取值而无需修改应用程序代码。
如果应用程序在 cookie 或头中有用户标识符,这是最佳选择。每个用户确定性地分配到一个版本,并在会话、设备和重新加载之间保持在该版本上。
Expression Editor(表达式编辑器) 中的文本:
http.cookie contains "user_id"Modify request header(修改请求标头) 下选择的操作:Set dynamic
Header name(标头名称):Cloudflare-Workers-Version-Key
Value(值):http.request.cookies["user_id"][0]
如果应用程序设置了会话 cookie,使用会话标识符。这会在会话期间提供一致的路由。如果会话过期并创建新会话,用户可能被分配到不同版本。
Expression Editor(表达式编辑器) 中的文本:
http.cookie contains "session_id"Modify request header(修改请求标头) 下选择的操作:Set dynamic
Header name(标头名称):Cloudflare-Workers-Version-Key
Value(值):http.request.cookies["session_id"][0]
如果应用程序在请求中没有任何稳定标识符,你有两个选项:
选项 1:使用客户端 IP 地址。 这是最简单的方法,无需更改应用程序。同一 NAT 或 VPN 后的用户将被分组在一起,切换网络的移动用户可能改变版本,但对于大多数应用程序,与每次请求随机路由相比,这显著减少了版本来回切换。
Expression Editor(表达式编辑器) 中的文本:
trueModify request header(修改请求标头) 下选择的操作:Set dynamic
Header name(标头名称):Cloudflare-Workers-Version-Key
Value(值):ip.src
选项 2:从 Worker 设置长期 cookie。 在第一次请求(将随机分配)时,Worker 生成稳定标识符并将其设置为 cookie。所有后续请求使用该 cookie 作为版本键。这为匿名用户提供最佳一致性,但需要少量应用程序代码。
export default {
async fetch(request, env) {
const response = await handleRequest(request, env);
// Set a long-lived cookie to use as a version affinity key.
const COOKIE_NAME = "version-key"; // can be any name
const cookieHeader = request.headers.get("Cookie") ?? "";
const hasAffinityCookie = new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=`).test(
cookieHeader,
);
if (!hasAffinityCookie) {
const id = crypto.randomUUID();
response.headers.append(
"Set-Cookie",
`${COOKIE_NAME}=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`,
);
}
return response;
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await handleRequest(request, env);
// Set a long-lived cookie to use as a version affinity key.
const COOKIE_NAME = "version-key"; // can be any name
const cookieHeader = request.headers.get("Cookie") ?? "";
const hasAffinityCookie = new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=`).test(cookieHeader);
if (!hasAffinityCookie) {
const id = crypto.randomUUID();
response.headers.append(
"Set-Cookie",
`${COOKIE_NAME}=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`,
);
}
return response;
},
};然后创建转换规则,使用此 cookie 作为版本键:
Expression Editor(表达式编辑器) 中的文本:
http.cookie contains "version-key"Modify request header(修改请求标头) 下选择的操作:Set dynamic
Header name(标头名称):Cloudflare-Workers-Version-Key
Value(值):http.request.cookies["version-key"][0]
你可以通过发送多个具有相同版本键的请求并确认它们由同一版本处理来验证版本亲和性是否正常工作:
# Both requests should return responses from the same version
curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: test-user-123'
curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: test-user-123'使用版本元数据绑定(version metadata binding)在测试期间在 Worker 的响应中包含版本 ID。
在渐进式发布期间,监控 Worker 的分析数据,关注 404 响应率的增加,尤其是资源文件(.js、.css、.png)。使用 Analytics Engine 或 Logpush 跟踪这些指标,及早发现版本偏差问题。如果发现问题,可以回滚到上一版本。
- 渐进式部署 - 基于百分比的流量拆分如何工作
- 版本覆盖 - 按 ID 将请求发送到特定版本(用于冒烟测试和调试,而非最终用户路由)
- 版本元数据绑定(version metadata binding) - 从 Worker 内部访问版本 ID 和标签