多云分流的两条链路:ESA 边缘分流 vs 小程序网关分流
同一个业务,同一批后端,我们前后落地了两套多云分流方案。它们解决的是同一个问题——把请求按用户和接口调度到不同的云上——但一套把决策做在云外的边缘节点,另一套把决策压进了云内的网关。 这篇文章讲清楚两套方案的技术栈、各自为什么长成这样,以及那几处真正拉开差距的地方。
1. 背景:为什么需要多云分流
单一云厂商的依赖风险是实打实的。我们要的不是「再买一朵云做冷备」——冷备切换太慢,而且平时不接流量,真出事时没人敢切。我们要的是热备:两朵云同时承载生产流量,随时可以按比例调整。
这带来三个硬需求:
- 流量可调度:按 userid 和接口维度配置各云比例,粒度和灰度都要能控。
- 故障可快切:单云故障时,能在秒级把流量全量拨到健康的云。
- 数据要一致:跨云之后,数据库主从、缓存一致性都得有说法。
围绕这三条,我们前后做了两套分流链路。它们的分流策略几乎一模一样,但分流的位置完全不同——而位置决定了一切。
2. 两条链路总览
2.1 ESA 边缘分流
请求在还没有进入任何一朵云的 VPC 之前,就在 ESA 的边缘节点上被判定归属,然后直接回源到目标云的负载均衡。
2.2 小程序网关分流
请求先进入腾讯云,经过网关、WAF、CLB,最后在 APISIX 上完成分流决策——而 APISIX 就部署在腾讯云的 UI 机器里,判定给腾讯云的请求走本机环回,连 VPC 都不用出。
两张图摆在一起,差异一眼可见:决策点一个在云外,一个在云内。这一个位置差,往下衍生出了协议、网络路径、承载能力、配置下发方式的全部区别。
3. 方案一:ESA 边缘分流
3.1 技术栈
| 层 | 选型 | 说明 |
|---|---|---|
| 边缘运行时 | ESA 边缘函数(JavaScript) | Web Worker 风格的 fetch(request) 入口 |
| 决策载体 | 单文件 JS,编译期常量 | 无外部依赖,无网络调用 |
| 配置来源 | 控制台配置 + 代码内常量表 | 改分流比例需要更新边缘函数 |
| 回源目标 | 阿里云 ALB / 腾讯云 CLB | 公网回源 |
| 可观测 | 边缘日志 + X-Backend-Server / X-Match-Rule 响应头 |
每个响应都能看出走了哪朵云 |
选择 ESA 的理由很直接:它是成熟的边缘产品,节点多、故障率低,分流逻辑写成一个纯函数就够了。决策不需要查任何外部状态,所有规则都编译进代码,运行期是一次内存查找。
3.2 分流因子与优先级链
分流输入只有两个:请求路径和userid。userid 从 Cookie 里的 AT= 解析出来(一段 % 分隔的载荷,取倒数第二段纯数字字段)。两者喂进同一条固定顺序的判定链:
1. userMapping —— userid 精确白名单,命中即定
2. modConfig —— userid 取模,按余数区间分桶(灰度进度的主要旋钮)
3. force —— 强制指定后端(应急开关)
4. weighted —— 按各后端权重随机前三级都是「确定性」的——同一个用户永远落同一朵云,这是灰度可控的前提。只有最后一级是随机权重,用于无差别地按比例放量。
路径匹配走三个索引,顺序是 exact → prefix → suffix:
exact:Map直查,O(1)prefix:按长度降序排序后线性扫描,保证最长前缀优先suffix:取路径最后一段做精确匹配(注意是最后一段完全相等,不是子串包含)
三个索引在建表时一次性构建好,运行期只有查表,没有正则、没有遍历字符串。
3.3 分流规则长什么样
一份典型的规则配置:
const domainConfig = {
aliyun: { weight: 100, userids: [] },
tencent: { weight: 0, userids: ['10000001', '10000002'] } // 白名单用户
};
// 取模分流:userid 末位对 10 取余,余数 3 走腾讯云,其余走阿里云
const controllerMod = {
goodsDetailMod: {
mod: 10,
mapping: { '3': 'tencent', '4-9': 'aliyun', '0-2': 'aliyun' },
},
};
const pathConfig = {
// 后缀匹配:只按路径最后一段比
'getFeedHome': { matchType: 'suffix', tencent: 100 },
'getGoodsList': { matchType: 'suffix', tencent: 100 },
// 带用户维度的复合规则
'getGoodsDetail': {
matchType: 'suffix',
userMapping: userMappingTencent, // 白名单优先
modConfig: controllerMod.goodsDetailMod, // 其次取模
},
};getGoodsDetail 这条规则值得单独说:它同时挂了 userMapping 和 modConfig。判定链会先查白名单,白名单没命中再走取模——这就是「VIP 用户固定走一朵云,其余用户按比例灰度」的实现方式。
3.4 决策与回源
边缘函数的骨架:
export default {
async fetch(request) {
const url = new URL(request.url);
const pathname = url.pathname.toLowerCase();
const userId = parseUserIdFromCookie(request.headers.get('cookie'));
// 三级索引查找
let matched = exactPathIndex.get(pathname)
|| matchPrefix(pathname)
|| suffixExactIndex.get(lastSegment(pathname));
// 命中规则 → 本地判定;未命中 → 全局兜底
const backend = matched
? resolveBackend(matched, userId)
: resolveGlobalBackend(domainConfig, userId);
// 通过请求头把决策结果传给回源
const newRequest = new Request(request.url, { /* ... */ });
newRequest.headers.set('x-backend-server', backend);
const response = await fetch(newRequest);
response.headers.set('X-Backend-Server', backend);
response.headers.set('X-Match-Rule', rule);
return response;
}
};注意这里的形态:整个分流器是一个无状态的纯函数,输入 (path, userid),输出 backend。没有缓存、没有外部存储、没有状态机。这是边缘计算最舒服的形态,也是它能在毫秒级完成决策的原因。
4. 方案二:小程序网关分流
小程序链路是完全不同的接入面:
小程序 → 小程序网关 → WAF → CLB → APISIX → 阿里云 / 腾讯云4.1 技术栈
| 层 | 选型 | 说明 |
|---|---|---|
| 接入层 | 小程序网关 | 承载小程序私有请求协议 |
| 安全层 | WAF | 应用层防护 |
| 负载均衡 | CLB | 腾讯云负载均衡 |
| 分流层 | APISIX(serverless-pre-function) |
分流主逻辑,Lua 编写 |
| 部署形态 | 与腾讯云 UI 机器同机部署 | 不单独占机器,复用 UI 的算力 |
| 配置源 | Redis(每 60s 拉取) | 配置热更新,无需 reload |
| 兜底 | 本地文件 → 内置常量 | 三级降级,永不空转 |
| 回源 | 127.0.0.1 环回 / 公网 到阿里云 |
命中腾讯云不出机器 |
4.2 为什么 CLB 之后还要加一层 APISIX
这是整条链路最容易被问到的问题:CLB 本身就能把流量分摊到多个后端,为什么后面还要挂一层 APISIX?
因为 CLB 能转发,但不能决策。
云负载均衡的能力边界很清楚:按域名、路径前缀、权重做转发——这些都是静态规则。而我们要的分流长这样:
- 同一个接口,
userid末位是 3 的走腾讯云,其余走阿里云; - 一批白名单用户固定走某一朵云,其余用户按比例灰度;
- 不同接口配不同比例,随时可调;
- 出故障时一键把某朵云全量摘掉。
这些是决策:要读配置、解析 Cookie、做取模运算、查索引表。CLB 的规则语言里没有「逻辑」这回事——它配不出「userid 末位取模等于 3」这种条件。改一次比例还得走控制台或 API 并等待生效,也不具备「往配置中心写一份、几十秒后自动生效」的能力。
对比 ESA 那条链路,这件事一下就清楚了:
ESA 的边缘函数本来就是可编程的——它能在边缘跑一段 JS,读 cookie、算取模、查索引。所以 ESA 那条链路天然就能做细分流,不需要额外的可编程层。
而 CLB 只是个转发器。要在它后面做细分流,就必须补一层可编程的网关——这就是 APISIX 的位置:跑 Lua、连 Redis、执行任意判定逻辑。
一句话:
ESA 链路不缺可编程层,因为 ESA 本身就是;小程序链路缺,所以补了一个 APISIX。
4.3 分流逻辑:一次请求的判定路径
APISIX 的 run() 函数做的事,和 ESA 那个 fetch() 逐条对应:
function _M.run(conf, ctx)
-- 1. 只读一次内存快照,整个请求周期不再碰 Redis / 磁盘
local st = get_state()
local pathname = string.lower(ctx.var.uri or "/")
local userId = getUserIdFromCookie(ctx.var.http_cookie or "")
-- 2. 三级索引查找,顺序与 ESA 完全一致:exact → prefix → suffix
local matched = st.exactPathIndex[pathname]
or match_prefix(st.prefixPathIndex, pathname)
or st.suffixExactIndex[last_segment(pathname)]
-- 3. 同一套判定链:userMapping → modConfig → force → weighted
local usedKey = matched and resolveBackend(matched, userId)
-- 4. 全局兜底:domainConfig 按权重选,选不出来走 defaultBackend
if not usedKey then
usedKey = resolveGlobalBackend(st.domainConfig, userId, st.defaultBackend)
end
-- 5. 应急开关:routePriority == 0 时全部压到 defaultBackend
if st.routePriority == 0 then
usedKey = st.defaultBackend
end
-- 6. 设置动态上游(关键:必须用 upstream.set,不能改 ctx.var.upstream_host)
local target = st.upstreamUrlMap[usedKey]
local nodeHost = resolve_to_ip(target.host) -- balancer 只认 IP,不认域名
upstream.set(ctx, "flowProxy#" .. usedKey, ctx.conf_version, {
type = "roundrobin",
scheme = target.scheme,
nodes = { { host = nodeHost, port = target.port, weight = 1 } },
pass_host = "rewrite",
upstream_host = ctx.var.host, -- 保留原始 Host
})
end函数名、判定顺序、匹配类型、兜底策略——和 ESA 那版 JS 是同一套设计。换个语言重写一遍,这不是巧合,而是刻意的:两套链路的分流语义必须一致,否则同一个用户在两条链路上会落到不同的云,灰度比例就没法对齐了。
4.4 配置形态:从「编译期」到「运行时」
这是两套方案最本质的工程差异。
ESA 的配置是编译进代码的常量。改一个分流比例 = 改代码 = 重新发布边缘函数。
APISIX 的配置是运行时从 Redis 下发的 JSON:
{
"schemaVersion": 1,
"version": "2026-10-06T12:00:00Z",
"routePriority": 2,
"defaultBackend": "aliyun",
"upstreamUrlMap": {
"aliyun": { "scheme": "http", "host": "alb-xxxxxxxx.cn-beijing.alb.aliyuncsslb.com", "port": 80 },
"tencent": { "scheme": "http", "host": "lb-xxxxxxxx.clb.bj-tencentclb.com", "port": 80 }
},
"domainConfig": {
"aliyun": { "weight": 100, "userids": [] },
"tencent": { "weight": 0, "userids": ["10000001", "10000002"] }
},
"userMappings": {
"vipTencent": { "10000001": "tencent", "10000002": "tencent" }
},
"modConfigs": {
"goodsDetailMod": { "mod": 10, "mapping": { "3": "tencent", "4-9": "aliyun", "0-2": "aliyun" } }
},
"pathConfig": {
"/api/feed/home": { "matchType": "exact", "tencent": 100 },
"/api/goods/list": { "matchType": "prefix", "aliyun": 90, "tencent": 10 },
"getGoodsDetail": { "matchType": "suffix", "userMapping": "vipTencent", "modConfig": "goodsDetailMod" }
}
}运营改分流比例,是往 Redis 里写一个 key,60 秒内自动生效——不动一行 Lua 代码,不需要 reload,更不需要发版。
这是 Redis 带来的核心价值:分流策略变成了数据,而不是代码。灰度放量、临时把某个接口切到另一朵云、给一批用户开白名单,这些本来要排期改代码的事,全都变成了一次配置写入。
注意 routePriority 这个字段,它是个三级应急开关:
| 值 | 行为 |
|---|---|
2 |
全功能:精确/前缀/后缀匹配 + 全局分流 |
1 |
关闭路径匹配,只保留全局分流 |
0 |
全部压到 defaultBackend——一键把流量收回单云 |
routePriority = 0 是我们为「单云故障」预留的核按钮。它不改变任何规则,只是让所有请求无条件落到默认后端,一个同步周期内就翻转完成。
4.5 本地文件兜底:不强制依赖 Redis
Redis 解决了「配置改得动」,但也顺手引入了一个新的依赖——Redis 自己挂了怎么办?
如果分流器每次都去读 Redis,Redis 就会变成和业务服务同级的关键路径:它抖一下,全站路由跟着抖;它崩了,连「切回单云」这个应急动作都做不了。用一个可能故障的组件去支撑容灾能力,是本末倒置。
所以配置在本地也留一份,读取时按优先级取:
Redis(每 60s 覆盖 —— 保证「改得动」)
└─ 本地文件(worker 冷启动时加载 —— 保证「Redis 崩了也活得下去」)
└─ 内置常量(两者都不可用时的最后防线)三层各自解决一个不同的问题:
| 层级 | 解决的问题 | 失效后的表现 |
|---|---|---|
| Redis | 配置能不能改 | 改不动新配置,但旧配置继续生效,路由不受影响 |
| 本地文件 | 配置能不能活 | 退回内置常量,退化成单后端 |
| 内置常量 | 服务能不能起 | — |
关键在于:Redis 故障只影响「变更能力」,不影响「服务能力」。 运维改配置会失败,但线上路由一切照旧——这正好是容灾最需要保证的性质。
这条链路还有几处刻意的设计,都是踩过坑之后定下来的:
① 热路径绝不碰 Redis 和磁盘。 每个请求只读一次内存快照 get_state()。Redis 拉取、JSON 解码、规则校验全部在 60 秒一次的定时器里完成。请求路径上没有任何 I/O。
② 校验必须在写文件之前。 顺序是「拉取 → 解码 → 校验 → 写文件 → 发布快照」。如果先写文件后校验,一次误推就会污染持久兜底文件,之后每次 worker 重启都会把坏配置复活——正好是兜底机制的反面。
③ 写文件是原子的,且带 pid 后缀。 -s reload 期间新旧两代 worker 会并存,如果 tmp 文件名共用,两个进程会互相截断,把半截字节 rename 成正式文件。所以 tmp 名带上 ngx.worker.pid()。
④ 只有 worker 0 写文件。 避免多 worker 并发写同一个文件。
⑤ 内置兜底里刻意不放业务数据。 最后那层 FALLBACK 常量只有后端地址表,没有那批白名单 userid。原因很直白:那 200 多个 ID 是业务配置,属于 Redis 和本地文件,不属于代码。真出现 Redis 和文件同时不可用的双重故障,宁可退回单后端裸跑,也不要让代码里埋一份会过期的分流表。
4.6 热路径的安全绳:同步链与看门狗
定时同步这一段,是整个 Lua 实现里最讲究的部分。
问题:APISIX 用的这个 OpenResty 版本里,ngx.timer.at / ngx.timer.every 只返回数字 1,拿不到可取消的句柄。这意味着如果同步链意外死掉,没法取消旧的,只能再加一个——于是两个循环同时打 Redis,看门狗每触发一次就再叠一个。
解法:改成自续期 + 代次号。
local SYNC = { generation = 0 }
local function sync_loop(gen, delay)
ngx.timer.at(delay or 60, function(premature)
if premature then return end
-- 代次已被看门狗更新:这条链就此终结,不再续期
if gen ~= SYNC.generation then return end
timer_cb(false)
if gen == SYNC.generation then
sync_loop(gen) -- 只有代次还对得上,才续下一轮
end
end)
end看门狗发现「超过 3 个周期没跑」,只做一件事:SYNC.generation = SYNC.generation + 1,然后重启装载。老的链在走到自己下一步时会发现代次不符,自行终止。全程不需要 cancel——因为这个版本给不了 cancel。
与之配套的还有几个细节:
- 短连接,不用连接池:60 秒建一次连不心疼。而池化连接会被 Redis 自己的 timeout 或中间设备断掉,陈旧 socket 报的错会被误判成「Redis 挂了」,进而导致本地文件永远不再更新。
- 只对比「上次写成功的字节」:如果对比「上次读到的」,一次写失败(磁盘满、权限错)之后就再也不会重试了,因为下一轮看到内容没变就跳过了。
- 日志去重:同一个失败原因只打一条,换原因才再打,成功即清空。否则定时器每 60 秒刷一次,error.log 就没法看了。
4.7 配置校验:把「静默误路由」挡在门外
分流配置是运营可以改的。而配置写错最可怕的后果不是报错——是静默生效成另一种语义。所以校验器几乎是逐条对着「代码里会静默出错的位置」来写的:
| 校验项 | 拦住的静默故障 |
|---|---|
matchType 必须显式且为三值之一 |
"Suffix" 拼错会把 suffix 表静默变成 prefix 表,一批规则一起错 |
| 键转小写后不得重名 | 索引会互相覆盖,赢家取决于 pairs 顺序(未定义) |
suffix 键不得含 / |
suffix 只和路径最后一段比,带斜杠永远匹配不上 |
exact / prefix 键必须有前导 / |
路径恒以 / 开头,缺斜杠规则永远匹配不上 |
权重必须是 > 0 的整数 |
权重 0 会被 value > 0 丢掉,字符串权重会被类型检查丢掉,两者都不报错 |
userids 不得出现在 pathConfig |
它是 domainConfig 的字段,放错位置会被静默忽略 |
| 死条目检测 | 既无权重、又无映射/取模/force 的规则,每个请求都静默落全局默认 |
mapping 余数区间不得重叠 |
重叠时路由结果取决于 pairs 遍历顺序 |
mapping 端点不得 >= mod |
余数恒 < mod,越界部分是死配置 |
顶层禁止出现 JSON null |
cjson 把 null 解成 userdata,== nil 判不出来但 pairs 会枚举到 |
| 空规则集拒绝覆盖 | 挡住截断或误推 {} 把可用配置抹掉 |
每一条后面都对应一个真实会踩的坑。这套校验的定位不是「防止崩溃」——大部分错误本来就不会崩——而是把语义漂移变成显式拒绝。
5. 核心差异对比
5.1 一张表
| 维度 | ESA 边缘分流 | 小程序网关分流 |
|---|---|---|
| 决策位置 | 云外边缘节点 | 云内 APISIX |
| 运行环境 | ESA 边缘函数(JS) | APISIX serverless-pre-function(Lua) |
| 可编程能力 | 平台自带(边缘函数) | CLB 不具备,靠 APISIX 补齐 |
| 入口协议 | 标准 HTTP/HTTPS | 小程序私有协议 |
| 配置来源 | 编译期常量 + 控制台 | Redis 下发(60s)+ 本地文件 + 内置兜底 |
| 配置生效 | 改代码/改控制台,重新发布 | 写 Redis,秒级~60s 自动生效 |
| 部署形态 | 云外边缘节点(托管) | 与腾讯云 UI 机器同机部署 |
| 回源网络 | 公网回源到 ALB / CLB | 腾讯云 127.0.0.1 环回,阿里云走公网 |
| 覆盖端 | H5 / APP / Web | 小程序(可扩展至 APP / H5) |
| 判定链 | userMapping → mod → force → weighted | 完全相同 |
| 索引结构 | exact / prefix / suffix | 完全相同 |
| 容灾粒度 | 改分流配置 | routePriority = 0 一键收口 |
| 可观测 | 响应头 X-Backend-Server / X-Match-Rule |
同左 |
5.2 差异一:决策位置——云外 vs 云内
这是根源性的差异,其余几条都从它推出来。
ESA 在流量进入任何一朵云之前做决策。好处是分流彻底、对后端零侵入:后端完全不知道自己是被调度的一方,两朵云看到的就是普通请求。代价是决策只能依据请求本身携带的信息——路径、Cookie、Header。边缘节点看不到 VPC 内的服务健康状态,也拿不到网关解出来的身份。
APISIX 在流量已经进入 VPC 之后做决策。此时请求已经过网关和 WAF,能拿到的东西更多,而且——这是关键——决策点和它要转发到的业务服务在同一台机器上(详见 5.5)。
5.3 差异二:协议——HTTP vs 小程序私有协议
这是小程序链路最实质的优势,也是它必须单独走一条链路的原因。
小程序网关承载的是小程序自己的请求协议,不是标准 HTTP。对小程序来说,请求根本不以裸 HTTP 的形态暴露在公网上。
这带来几个直接收益:
- 抓包难度陡增:攻击者拿到的不是标准 HTTP 报文,常规的抓包重放工具箱直接失效。
- 爬虫门槛抬高:想模拟请求,得先过协议层,而不是拼一个 URL 就行。对于有接口级灰度分流的业务,这一点尤其重要——分流规则本身会暴露「哪个接口可以打」。
- 可以做协议内建的安全能力:签名、时效、防重放这类机制,放在协议层比放在应用层更难绕过。
- 与 WAF 形成两层防护:私有协议层挡掉一批,WAF 再挡一层。
这条协议层的价值,ESA 那条链路给不了。 ESA 的边缘函数工作在标准 HTTP 语义上——它拿到的就是一个 Request 对象。它能在边缘做拦截和分流,但请求本身仍然是公网上的标准 HTTP。
需要说明的是:两条链路不是替代关系。ESA 适合直接在公网侧接入标准 HTTP 的端(H5、Web、APP),小程序链路的入口是网关,天然在云内。这是两个并行的接入面,各适配各的端。
5.4 差异三:配置下发——编译期 vs 运行时
ESA 的分流规则是代码里的常量表。这有明确的优点:极快,规则是编译期常量,Set 和 Map 在模块加载时构建好,运行期只有 O(1) 查表,没有任何外部依赖。缺点是改配置要走发布流程。
APISIX 的规则从 Redis 下发。多出来的复杂度全在降级路径上——60 秒拉一次,失败要保住上一次的好数据,本地文件要能持久兜底,写文件要原子,定时器死了要有看门狗捞回来。4.5 和 4.6 两节讲的就是这些。
这个复杂度的投入换来的是:运营改分流比例不用找研发发版。在多云灰度这种场景下,这个差别是决定性的——灰度放量本来就是要频繁调的操作。
5.5 差异四:分流层放在哪——云外边缘 vs 云内业务机器
先分清两件事:APISIX 的引入动机是「CLB 不可编程」(见 4.2)——这个理由和网络没有半点关系。
但 APISIX 放在哪里,是完全独立的一个设计选择。而正是「和 UI 机器同机」这个选择,让分流层顺带把网络和算力的账也算了一遍。这一段讲的是后者:不是为什么要有 APISIX,而是为什么它长在这个位置。
ESA 链路:边缘节点判定归属后,回源到阿里云 ALB 或腾讯云 CLB,这一段走公网。请求的每一跳都是一次公网连接。
小程序链路:APISIX 不是独立的网关集群,而是直接跑在腾讯云的 UI 机器里。于是路径变成:
- 命中腾讯云 → APISIX 把请求交给本机的 UI Nginx,走
127.0.0.1环回,连 VPC 网络栈都不经过; - 命中阿里云 → 这才是唯一需要出机器的流量,走公网 转发到阿里云。
换句话说,在这条链路上,只有判定给阿里云的请求才真正「走出去」。
这个部署形态能成立,靠的是两个前提:
① 腾讯云的机器便宜。 腾讯云的实例单价大约是阿里云的近一半。这意味着腾讯云侧的 UI 机器本来就该多留些余量——把这些余量拿出来跑 APISIX,等于用低单价算力干分流这件活。
② APISIX 的占用足够低。 它基于 OpenResty 的事件驱动模型,常驻内存和 CPU 开销都很小,和 UI 挤在同一台机器上不会抢业务资源。反过来,如果分流器是个吃资源的重量级组件,同机部署就不成立——省下的流量钱还不够赔 UI 的性能。
两者叠加,结果是腾讯云 UI 机器的利用率被抬起来了:原本只跑 UI 的机器,顺手把全站的分流决策也扛了。
那为什么不把分流节点放到边缘?
这里有个自然的疑问:ESA 那条链路把分流做在边缘,小程序这条为什么不照做?
因为流量已经回来了。WAF 这一跳的作用就是把请求收回机房——请求走到 WAF 时,人已经在腾讯云的机房里了。此时再把它送出去到边缘节点做分流,等于让请求白跑一趟公网:出去、判定、再回来,凭空多出一段往返延迟和一段网络流量。
所以分流节点的位置,应该跟着「流量当前在哪」走:
- ESA 链路里,ESA 本身就是入口,流量天然在边缘,分流就在边缘做;
- 小程序链路里,WAF 已经把流量带回机房,分流就该就近在机房里做。
分流只该落在请求的必经之路上,不该制造额外的网络往返。 这是两条链路共同的原则,只是各自的「必经之路」不在同一个位置。
这个部署形态落到拓扑上,就是这样——分流层长在机器内部,两条出口分别是环回和 VPC:
同机这个位置带来的三层收益(都是顺带的,不是引入 APISIX 的理由):
第一层,网络路径缩到最短。 命中腾讯云的请求走的是 127.0.0.1——所有网络路径里最短的一条。它比同 VPC 内跳还短:不经过虚拟网络,不出网卡,延迟和抖动趋近于零。只有判定给阿里云的流量才会离开这台机器。
第二层,算力成本被复用掉了。 APISIX 没有独立的机器开销,它借住的是腾讯云那些本来就便宜的 UI 机器。分流的算力成本接近于零,同时把腾讯云侧的资源利用率拉高了。
第三层,LB 层的配置被大幅简化。 引入 APISIX 之前,负载均衡需要为每个后端维护转发规则。引入之后,CLB 只需要把流量转给 APISIX——分流规则全都收在 APISIX 的 Redis 配置里。LB 层的配置面从「一堆规则」变成「一跳转发」,变更风险和运维成本都降下来了。
这里还有一个容易忽略的点:APISIX 本身不是吞吐瓶颈。它继承了 OpenResty 的事件驱动模型,单节点就能承接远超业务峰值的并发,并且可以水平扩展成集群。把分流收敛到 APISIX,在吞吐上留了充足的余量——这正是敢把它和业务塞进同一台机器的底气。反过来,如果分流层是个性能瓶颈,它根本没资格和 UI 抢资源,前面那些顺带收益也就无从谈起。
但代价也要说清楚。 同机部署省下了机器,同时也把 APISIX 的可用性和 UI 机器的可用性绑在了一起:UI 机器崩,分流器跟着崩。分流器一旦不可用,链路内部那些「切流开关」也就没人来执行了——请求根本到不了它。这个问题怎么解,见 7.1。
5.6 差异五:分流逻辑却是同构的
讲完五条差异,说一个反直觉的结论:两套分流器的逻辑其实一模一样。
看这段并排:
// ESA 边缘函数(JavaScript)
function resolveBackend(config, userId) {
if (config.userMapping?.[userId]) return config.userMapping[userId]; // 1
if (config.modConfig) { /* 取模 */ } // 2
if (config.force) return config.force; // 3
/* 权重随机 */ // 4
}-- APISIX 分流器(Lua)
local function resolveBackend(config, userId)
if config.userMapping and config.userMapping[userId] then -- 1
return config.userMapping[userId]
end
if config.modConfig then /* 取模 */ end -- 2
if config.force then return config.force end -- 3
-- 4 权重随机
end判定顺序相同,索引结构相同,取模算法相同,兜底策略相同,连响应头都相同(X-Backend-Server / X-Match-Rule)。取 userid 的那段 AT Cookie 解析逻辑,JS 和 Lua 是逐行对应的——包括那个「从后往前找第二段连续数字字段」的细节,和 % 分隔时不能用正则、必须用字面量查找的坑。
这不是偷懒,是刻意的同构。原因很简单:两条链路面向的是同一批用户。如果两边语义不一致,同一个用户在 H5 上落阿里云、在小程序里落腾讯云,跨云的缓存一致性和灰度比例就全乱了。
所以真正的设计主张是:
分流语义统一,分流位置按接入面各就各位。
语义是业务契约,必须一致;位置是工程选择,按每个接入面的特点来定。
6. 小程序链路的延伸价值
6.1 协议层安全
前面 5.3 讲过,这里补充一点:小程序私有协议不是终态,是可复用的接入能力。
6.2 可复用到 APP 和 H5
这是当初做这条链路时埋的一个伏笔。
小程序网关承载的是「端到网关」这一跳的协议。而这一跳是端无关的——只要端侧能跑起来对应的接入 SDK,理论上就能接进来:
| 端 | 接入方式 | 收益 |
|---|---|---|
| 小程序 | 现已接入 | 私有协议 + 环回分流 |
| APP | 内置 SDK | 复用同一套协议层与分流策略 |
| H5 | JS SDK | 同上;H5 侧的协议强度取决于运行环境 |
一旦 APP 和 H5 也接进来,收益是复合的:
- 一套分流策略覆盖全端——APISIX 里的那份 Redis 配置,对所有接入端同时生效。改一次,全端灰度同步推进。
- 协议层安全能力向全端铺开——签名、时效、防重放这些,不用再各端各做一遍。
- 环回收益成倍放大——目前只有小程序流量里命中腾讯云的那部分不出机器;全端接入后,全站流量都按同一个比例收敛到本机环回。
6.3 环回收益的规模效应
APISIX 的引入理由是「CLB 不可编程」(4.2),环回是顺带的好处。但这个顺带的好处有个很好的性质:它随分流比例一起放大。
因为分流决策的位置决定了流量的网络路径。决策在云外,每一次回源都是一次公网连接;决策在云内、而且和业务同机,命中本云的部分连 VPC 都不出,直接环回。
分流比例越高,收益越大。当腾讯云的承接比例拉到 100% 时,公网出向流量趋近于零——而多云分流的终局状态,恰恰就是「日常按比例分散,故障时全量切到某一朵」。切到单云的那一刻,也正是环回收益最大的那一刻。
7. 容灾与切流
两套链路都支撑同一套容灾目标,但操作方式不同:
| 场景 | ESA 链路 | 小程序链路 |
|---|---|---|
| 调整灰度比例 | 改分流配置 | 写 Redis key |
| 生效时间 | 改配置后秒级~15 秒 | 一个同步周期(≤60s) |
| 全量切到单云 | 一键切流开关 | routePriority = 0 |
| 决策依据 | 健康检查(规划中) | 同左 |
一次真实故障:腾讯云侧 RedisProxy 崩溃导致 APP 瘫痪,运维通过 ESA 快速开关把腾讯云全部接口流量切到阿里云,从改配置到生产恢复约 15 秒。
这次故障也直接影响了小程序链路上的设计——分流配置本身就成了一条关键路径,所以它必须比自己调度的那些服务更稳。4.5 的三级降级和 4.6 的看门狗,都是冲着这个目标去的:Redis 挂了不影响路由,文件坏了还能跑,监控进程死了能自愈。
7.1 两级回滚:从小手术刀到总闸
回滚能力是分级的,越靠前的干预越轻:
| 级别 | 动作 | 作用范围 | 代价 |
|---|---|---|---|
| 一级 | APISIX 里 routePriority = 0 |
新链路内部收口,全部压到默认后端 | 最小,不改变链路结构 |
| 二级 | 全部切回 ESA | 整条链路收回 ESA 统一承载 | 总闸,新链路整体退出 |
一级是关掉精细分流。 分流规则还是那套,只是让所有请求无条件落到默认后端,一个同步周期内生效。它解决的是「分流配置本身出了问题」这一类事——配置推错了、灰度比例调歪了、某个后端压根不该接量。
二级是总闸。 如果有问题的不是配置,而是 APISIX、小程序网关,或者腾讯云这一串里的任何一环——那切 routePriority = 0 也没用,因为请求根本到不了 APISIX,这个开关也就无从生效。最典型的就是 5.5 里说的那件事:UI 机器崩了。
这时候的动作是做减法:把整条链路收回 ESA,让分流重新由那条已经验证过的成熟链路承担,同时把流量快速切到还活着的云节点上去。
为什么总闸是 ESA? 因为它的失效模式和 APISIX 不一样。
APISIX 和 UI 同机部署(见 5.5),省了机器,代价是分流器的可用性和业务机器的可用性绑在了一起——UI 机器崩,分流器跟着崩。而分流器一崩,上面那一级 routePriority = 0 也就成了空话:请求到不了 APISIX,那个开关没人执行。
ESA 正好补上这个位置。它是边缘化部署的——大量边缘节点分布在不同位置,全部同时故障的概率几乎为零。业务机器会崩,但边缘节点不会一起崩。
于是这两级回滚的分工就清楚了:
- 一级(
routePriority = 0)解决「分流配置错了」,前提是 APISIX 还活着; - 二级(切回 ESA)解决「APISIX 或者它所在的机器没了」,靠的是一个失效模式完全不同、且独立于业务机器的承载面。
总闸的价值不在于它有多快,而在于它保证新链路无论怎么崩,都有一条已知能工作的旧路径可以退回去。新链路是增量,ESA 是那个「出事了总能回去」的地方。
一句话概括这套设计:
一个和业务同生共死的分流器,必须配一个不同生共死的兜底。
8. 演进方向
8.1 理想态:小程序网关直连 EO,分流回到边缘
先说终局,因为它反过来解释了现在的很多选择。
目前「分流在机房内」,是被现状约束出来的结果——它的前提是「流量已经回到机房了」。而理想状态是让分流重新回到边缘:
小程序网关直接对接腾讯云 EdgeOne(EO),由 EO 在边缘完成分流判定。
为什么这是理想态?因为判定发生在请求的必经之路上,请求不必先进机房、再决定去哪:
| 场景 | 现在的路径 | 理想态 |
|---|---|---|
| 命中腾讯云 | 进机房 → 就地环回 | 边缘判定后进机房,路径不变 |
| 命中阿里云 | 先进腾讯云机房,再折返回阿里云 | 边缘判定后直接去阿里云,根本不进腾讯云机房 |
关键在第二行。现在命中阿里云的请求,实际上是绕了一圈——先进了腾讯云机房,才被判定要走阿里云,然后再折返出去。EO 接上之后,这一圈就省掉了。
要注意,理想态并不牺牲环回收益:命中腾讯云的请求照样进机房、照样走本机环回。EO 省掉的是命中阿里云那部分「白跑一趟机房」的往返。
更重要的是,这条演进不违反 5.5 的原则,恰恰是它的延伸:
分流节点应该落在流量本来就经过的位置,而不是制造额外的往返。
现在 APISIX 放在机房内,理由是「流量已经到机房了」;将来 EO 立在了请求的必经之路上,分流就该回到边缘。位置跟着流量路径走,原则始终没变。
至于现在这层 APISIX,届时会退居两种角色之一:作为精细分流的兜底层保留,或者直接去掉、由 EO 统一承接。
8.2 其他方向
- 自动切流:接入健康检查探针,自动探测各云核心服务可用性,故障时自动调用 API 翻转分流配置——把恢复时间从人工的十几秒压缩到秒级。
- 读流量本地化:把跨云的读请求逐步迁到本云中间件,降低跨云依赖。
- 主备 → 双活:从「一主一备」向真正双活演进,提升资源利用率。
- 统一控制面:把两条链路的分流配置收敛到一个可视化平台——流量分布、健康状态、改配操作都在这一个面板上完成。目前 ESA 走控制台、小程序走 Redis,是两套操作入口。
- 全端接入:APP 和 H5 复用小程序网关的协议层与分流策略,让环回收益覆盖全站流量。
9. 小结
一句话概括两套方案的关系:
分流语义是同构的,分流位置是不同的。
ESA 把决策做在云外的边缘节点——决策快、对后端零侵入,用标准 HTTP 语义服务 H5 / APP / Web,配置编译期固化。
小程序链路把决策做在云内的 APISIX——请求先过小程序网关(私有协议,更安全),再进腾讯云 UI 机器上的 APISIX。命中腾讯云的请求走
127.0.0.1环回、连 VPC 都不出,只有命中阿里云的才真正离开机器;配置从 Redis 下发、运营可自助调整,并且这套协议层与分流策略可以复用到 APP 和 H5。
选哪套,取决于接入面:请求是标准 HTTP 从公网进来的,用 ESA;请求是自家协议从端上进来的,用网关 + APISIX。而两条链路共用的那套判定链——白名单、取模、权重、exact/prefix/suffix——是同一个业务契约,必须保持一致。
把时间拉长看,两条链路最终还会收敛:如果小程序网关能和 EO 打通,分流会重新回到边缘,两条链路的分流位置也就重新一致了。到那时留下的,只有那套从头到尾没变过的判定链。
评论(0)
还没有评论,来抢沙发吧 ✦