一、跨国大促,网关是第一道生死关
在国内做双11,流量洪峰你可以随时加机器、加带宽。但如果你面对的是"黑五"、"斋月"或者"海外圣诞季"的流量洪峰,跨国 API 网关面临的局面极其复杂:
-
跨链路延迟高:海外用户的请求 RTT(往返时延)可能高达 300ms 以上。
-
云成本不可控:在 AWS 或 GCP 上一旦触发自动扩容,账单可能瞬间失控。
-
漏斗效应:一个电商前端页面可能并发请求 20 个不同的微服务 API。一旦网关扛不住,前端全线崩溃。

在这个场景下,网关不能做简单的"转发器",它必须成为限流与熔断的智能防线。
二、分布式限流:Redis + Lua 的滑动窗口
传统的令牌桶(Token Bucket)算法在单机内存中很好用,但在跨国多活架构下,多个网关实例部署在不同的可用区,必须做分布式全局限流。
最常见的工程实践是使用 Redis + Lua 脚本。
2.1 为什么必须是滑动窗口?
很多系统采用固定窗口(Fixed Window)限流(比如限制每分钟 100 次)。但固定窗口存在"临界问题":在第 1 分钟的最后 1 秒进了 100 次,第 2 分钟的第 1 秒又进了 100 次。瞬间 200 次流量涌入,系统被打爆。
滑动窗口(Sliding Window) 解决了这个问题,它将时间切分成更细的粒度,通过计算当前时间前推 1 分钟内的请求总量来判断是否限流。
lua
-- Redis + Lua 实现滑动窗口限流
-- KEYS[1]: 用户的限流 Key (如: limit:api:user_1001)
-- ARGV[1]: 当前时间戳(毫秒)
-- ARGV[2]: 限流窗口大小(毫秒)
-- ARGV[3]: 最大允许请求数
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max_req = tonumber(ARGV[3])
-- 1. 移除窗口外的旧请求记录
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)
-- 2. 获取当前窗口内的请求数量
local current_count = redis.call('ZCARD', KEYS[1])
-- 3. 判断是否超过阈值
if current_count >= max_req then
return 0 -- 拒绝请求
else
-- 加入当前请求(用时间戳+随机数作为 member,确保不覆盖)
redis.call('ZADD', KEYS[1], now, now .. '_' .. math.random(10000))
-- 设置过期时间,防止内存泄漏
redis.call('PEXPIRE', KEYS[1], window)
return 1 -- 允许请求
end
跨国部署的注意点 :Redis 必须是全局共享(Global Datastore) 的,比如 AWS ElastiCache Global Datastore。如果各个可用区各自维护本地 Redis,就无法实现全局限流,流量依然可能击穿下游。
三、熔断:面对"超时重试"引发的雪崩
跨国调用的高延迟,极易引发重试风暴(Retry Storm) 。
假设一个下游服务因为海外机房网络抖动变慢,响应时间从 100ms 飙升到 2s。前端用户等不及,手动刷新;或者客户端 SDK 触发了自动重试。大量重试请求瞬间堆满网关的线程池,导致网关崩溃,进而拖垮整个系统。
3.1 熔断器状态机设计
针对跨国调用的复杂性,熔断器必须基于异常比例 和慢调用比例双重触发。
java
public class CircuitBreaker {
private State state = State.CLOSED;
private int failureCount = 0;
private int slowCallCount = 0;
private long windowStartTime = System.currentTimeMillis();
// 熔断阈值:在 10 秒窗口内,失败/慢调用超过 50%
private static final int MIN_REQUEST_THRESHOLD = 20;
private static final double FAILURE_RATE_THRESHOLD = 0.5;
public boolean allowRequest() {
if (state == State.OPEN) {
// 熔断时间已过,尝试半开
if (System.currentTimeMillis() - windowStartTime > 10000) {
state = State.HALF_OPEN;
return true; // 放行一个探测请求
}
return false;
}
return true;
}
public void recordResult(boolean isSuccess, long durationMs) {
if (state == State.HALF_OPEN) {
// 半开状态:探测成功则恢复,失败则继续熔断
state = isSuccess ? State.CLOSED : State.OPEN;
resetMetrics();
return;
}
// 收集指标
if (!isSuccess) failureCount++;
if (durationMs > 1000) slowCallCount++; // 慢调用(超过1秒)
// 检查是否达到熔断条件
int totalRequests = failureCount + slowCallCount; // 简化示例
if (totalRequests > MIN_REQUEST_THRESHOLD &&
(double) (failureCount + slowCallCount) / totalRequests > FAILURE_RATE_THRESHOLD) {
state = State.OPEN;
windowStartTime = System.currentTimeMillis();
}
}
}
3.2 跨国特有规则:按"地域"配置不同阈值
-
东南亚(如新加坡机房):链路较好,延迟低,熔断阈值可以设为"错误率 > 30%,响应时间 > 1s"。
-
拉美或非洲(如巴西、南非) :跨境海底光缆极不稳定。如果你把熔断阈值设为 1s,可能一天要熔断几十次。这里的策略应该是:放宽延迟阈值(比如 3s),但收紧错误率阈值(如 > 20% 立即熔断),让调用尽快失败,而不是死等。
四、隔离舱(舱壁模式):防止单一接口拖垮全局
如果网关部署了 1000 个线程。A 业务(订单查询)很慢,占用了 800 个线程。此时 B 业务(支付回调)进来,发现线程池满了,直接被拒绝。这就叫"故障蔓延"。
舱壁模式(Bulkhead Pattern) 要求为不同的 API 划分独立的资源池。比如:
-
Order_Service划分配 400 个线程。 -
Payment_Service划分配 200 个线程。 -
Tracking_Service划分配 200 个线程。
这样即使 Order_Service 因为海外DB卡顿导致线程耗尽,Payment_Service 依然有可用的线程池对外提供服务。
五、避坑指南
坑1:重试没有限制重试次数和退避策略。 网关层严禁自动对写操作(如创建订单)进行无限制的重试。对于重试,必须引入指数退避(Exponential Backoff)和抖动(Jitter),避免大量客户端在同一秒发起重试。
坑2:限流规则写死在代码里。 大促峰值和日常的限流值完全不同。必须将限流阈值、熔断规则抽取到配置中心(如 Nacos / Apollo),支持运营人员在控制台动态调整。
坑3:忽视跨云同步延迟。 如果你用 GSLB 把流量切换到了备用云,但主备云之间的全局限流数据(Redis)没有同步,会导致备用云被削峰失败的流量打穿。
六、总结
跨国网关的高可用,本质上是"削峰"和"降级"的艺术。通过 Redis + Lua 实现全局分布式滑动窗口限流,用状态机建立基于错误率和慢请求的熔断机制,再通过舱壁模式隔离不同业务线。结合跨国链路的特性设定差异化阈值,你才能在海外大促的流量洪峰中,保住系统的主干。
下一篇预告:网关防住了外部流量,但如果你要在三维空间里把货装进集装箱,如何防止偏载侧翻?明天聊三维装箱与重心平衡算法。
> 本文作者:深圳+Eyf•半字节科技▪技术产品团队
> 物流、快递、快运作业管理系统,让每一票,都清晰可控。从任务分配到签收回传,一站式管理每个作业环节。