我参与的某零售巨头的促销活动系统,上线后 2 小时,系统崩了。这是我第一次参与后端项目开发,因为时间赶,很多细节没做
日活 200 万用户,疯狂刷新页面抢优惠券。API 每秒接到 8000 多个请求,响应时间从 200ms 飙到 30 秒。数据库连接池耗尽,Redis 过载,下游的库存服务也被打垮了。促销系统变成了 503 页面展示系统。
三个致命问题
前端请求太猛:产品要求”每次回到主页都刷新数据”,用户每点一次返回键,就是一次 API 调用。高峰期,70% 的请求是重复的。
自己扛不住:单个 Node.js 实例没有限流,GCP Load Balancer 后面的 4 个实例全部 CPU 100%,新请求排队等死。
压垮下游:服务疯狂调用库存服务的 API,把对方的数据库连接池打满。他们崩了,请求全部 timeout,雪崩式崩溃。
修复方案有两个方向:限流 Rate Limiting 和熔断+降级 Circuit Breaker。
三层防护架构:
graph TB User((用户请求)) User --> CloudArmor(Cloud Armor<br/>IP 限流) CloudArmor --> LB(Load Balancer) LB --> AppServer(应用服务器<br/>用户限流) AppServer --> Downstream(下游服务<br/>熔断保护) style User fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style CloudArmor fill:#d3f9d8,stroke:#2f9e44,stroke-width:2px style LB fill:#e7f5ff,stroke:#1971c2,stroke-width:2px style AppServer fill:#ffe8cc,stroke:#d9480f,stroke-width:2px style Downstream fill:#e5dbff,stroke:#5f3dc4,stroke-width:2px
限流方案对比
限流可以在多个层面实现:
| 方案 | 实现位置 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Gateway 限流 | API Gateway (Kong/Nginx) | 统一管理,无需改代码 | 配置复杂,难以自定义逻辑 | 简单的全局限流 |
| Cloud Armor | Load Balancer 层 | 最外层防护,挡 DDoS | 只能基于 IP/地理位置 | 防御恶意攻击 |
| 应用层限流 | 业务代码内 | 灵活,可按用户/API 定制 | 需要维护代码和状态 | 细粒度业务限流 |
| 数据库限流 | 数据库连接池 | 保护数据库 | 限流太晚,上游已积压 | 最后防线 |
我们的选择:Cloud Armor(外层防护) + 应用层限流(业务逻辑)
为什么不用 Gateway?
- Kong/Nginx 的限流配置不够灵活,无法按用户等级、API 类型做差异化限流
- 促销场景需要动态调整限流阈值,Gateway 配置变更需要重启
- 应用层可以直接访问 Redis 用户数据,实现更精准的限流策略
应用层限流(express-rate-limit)
限流逻辑:每秒只处理 N 个请求,其余直接拒绝。
我们用 express-rate-limit 库实现两级限流:
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
// 全局限流:每个 IP 每分钟 60 个请求
const globalLimiter = rateLimit({
windowMs: 60000,
max: 60,
store: new RedisStore({ client: redisClient }) // Redis 共享计数
})
app.use(globalLimiter)为什么需要 Redis
4 个实例各自计数,用户请求分散到不同实例,限流失效,用户本应被限制为 100 个/秒,实际可以发送 400 个/秒(4×100)。
Redis 解决方案:所有实例连接同一个 Redis,在 Redis 里记录全局计数。无论请求打到哪个实例,都查同一个计数器。
// 用户限流:每个用户每秒 10 个请求
const apiLimiter = rateLimit({
windowMs: 1000,
max: 10,
keyGenerator: (req) => req.user.id,
store: new RedisStore({ client: redisClient }) // Redis 共享计数
})
app.use('/api', apiLimiter)熔断器:opossum
下游服务挂了,每个请求等 5 秒 timeout 才返回错误,线程被占用,新请求排队,系统假死。
熔断器逻辑:下游持续失败,直接拒绝请求,不要傻等。
三种状态:
- Closed(闭合):正常状态,请求通过
- Open(断开):失败率过高,直接拒绝
- Half-Open(半开):等待一段时间后,尝试少量请求测试恢复
我们用 opossum 库实现:
import CircuitBreaker from 'opossum'
// 创建熔断器
const stockBreaker = new CircuitBreaker(callStockService, {
timeout: 3000, // 超时 3 秒
errorThresholdPercentage: 50, // 错误率超过 50% 触发熔断
resetTimeout: 30000 // 30 秒后尝试恢复
})
// 设置降级函数
stockBreaker.fallback(() => ({ message: '库存服务暂时不可用' }))
// 使用
async function getStock(productId) {
try {
return await stockBreaker.fire(productId)
} catch {
return await getFallbackStock(productId) // 降级
}
}连续失败 5 次后进入 Open 状态,直接返回错误。等待 30 秒后进入 Half-Open 尝试恢复。响应时间从 5 秒降到 10ms。
熔断后返回什么:降级策略
熔断器只是快速失败,真正的降级要返回有意义的数据。库存服务挂了。核心原则:保证核心功能可用,次要功能降级,可选功能关闭。
库存查询的三层降级
库存服务挂了,先返回 Redis 缓存(5 分钟前的数据),没缓存就返回基于历史平均的预估库存,还不行就隐藏库存信息允许下单。
用户体验:
- 有缓存时,显示”数据可能有延迟,建议尽快下单”
- 无缓存但历史库存充足,显示”库存充足”按钮
- 完全降级时,只显示”立即购买”,下单时再检查真实库存
库存服务挂了,用户仍能正常浏览和下单。少部分用户可能下单后发现无库存,但比整个页面卡死好得多。
async function getStock(productId: string) {
try {
return await stockBreaker.fire(productId)
} catch {
return await getFallbackStock(productId)
}
}
async function getFallbackStock(productId: string) {
// 第一层降级:返回缓存
const cached = await getCachedStock(productId)
if (cached) return cached
// 第二层降级:返回预估库存
const estimated = await getEstimatedStock(productId)
if (estimated) return estimated
// 最终降级:允许下单
return getDefaultStock()
}
async function getCachedStock(productId: string) {
const cached = await redis.get(`stock:${productId}`)
if (!cached) return null
return {
...JSON.parse(cached),
warning: '数据可能有延迟'
}
}
async function getEstimatedStock(productId: string) {
const avgStock = await db.getAvgStock(productId)
if (avgStock <= 50) return null
return {
available: true,
message: '库存充足',
realtime: false
}
}
function getDefaultStock() {
return {
available: true,
message: '立即购买',
note: '下单时验证库存'
}
}非核心功能也要降级
| 功能 | 降级策略 | 用户感知 |
|---|---|---|
| 用户推荐 | 个性化推荐 → 热门商品 | 推荐商品变成通用的,不影响浏览 |
| 商品评论 | 实时评论 → 热门评论缓存 → 隐藏评论区 | 评论区消失或显示缓存 |
| 优惠券 | 实时查询 → 直接隐藏 | ”暂无可用优惠券” |
即使多个非核心服务挂了,用户仍能浏览商品、看价格、加购物车、下单。只是少了一些增强体验的功能。
完整代码示例
把限流、熔断、降级组合起来:
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
import CircuitBreaker from 'opossum'
// 限流器
const globalLimiter = ... //
const apiLimiter = ...
// 熔断器
const stockBreaker = ...
// 业务逻辑
async function getProductWithFallback(productId: string) {
const product = await productBreaker.fire(productId) // 只熔断不降级
const stock = await getStockWithFallback(productId) // 降级处理
const recommendations = await getRecommendationsWithFallback(productId) // 降级处理
return { product, stock, recommendations }
}
async function getStockWithFallback(productId: string) {
try {
return await stockBreaker.fire(productId)
} catch {
return await getFallbackStock(productId) // 三层降级
}
}
async function getRecommendationsWithFallback(productId: string) {
try {
return await recommendBreaker.fire(productId)
} catch {
return await getPopularProducts() // 降级到热门商品
}
}
// 应用限流器
app.use(globalLimiter)
app.use('/api', apiLimiter)
// API 路由
app.get('/api/product/:id', async (req, res) => {
const data = await getProductWithFallback(req.params.id)
res.json(data)
})请求处理流程:globalLimiter(IP)→ apiLimiter(用户)→ 业务逻辑(熔断+降级)→ 返回数据
最外层防护:Cloud Armor
Google Cloud 的 Web 应用防火墙(WAF),部署在 Load Balancer 前面,拦截恶意请求、防 DDoS、做 IP 级别限流。
(我们用Pulumi管理 Cloud Armor 配置)
// 创建安全策略
const securityPolicy = new gcp.compute.SecurityPolicy("rate-limit-policy", {
description: "Rate limiting policy for production",
// IP 限流规则
rules: [{
action: "rate_based_ban",
priority: 1000,
match: {
versionedExpr: "SRC_IPS_V1",
config: {
srcIpRanges: ["*"], // 对所有 IP 生效
},
},
rateLimitOptions: {
conformAction: "allow",
exceedAction: "deny(429)",
enforceOnKey: "IP",
rateLimitThreshold: {
count: 100,
intervalSec: 60,
},
banThreshold: {
count: 1000, // 1 分钟超过 1000 次,封禁
intervalSec: 60,
},
banDurationSec: 600, // 封禁 10 分钟
},
}],
});
// 绑定到后端服务
const backendService = new gcp.compute.BackendService("api-backend", {
loadBalancingScheme: "EXTERNAL",
securityPolicy: securityPolicy.id,
// ... 其他配置
});
效果:超过限制的请求直接返回 429 错误,不会到达你的应用服务器。Cloud Armor 是最外层防护,配合应用层限流和熔断器,形成完整的三层防护体系(见前文架构图)。
最终效果
请求量从 8000 降到 2000 QPS,响应时间 P99 从 30 秒降到 800ms,错误率从 80% 降到 3%。用户刷新太快会看到”请求过于频繁”,库存不可用时显示”暂时无法查询”,但页面不会卡死。
限流不是银弹
限流保护了系统,但也拒绝了真实用户,转化率会下降。更好的方案是前端缓存减少请求、库存查询改成消息队列、提前压测知道极限。限流和降级是最后防线,在极端情况下优雅降级而不是彻底崩溃。
前后端共同守护稳定
前端:70% 的请求是重复的。改进后用防抖、本地缓存、懒加载、请求合并。
后端:没有限流、没有熔断、没有降级。改进后加上限流保护自己、熔断保护下游、降级保证核心功能可用。
如果用了 Kubernetes:当时我们手动管理 4 个实例,公司还没引入 K8S。如果用 K8S HPA,流量激增时自动扩到 30 个实例,活动结束又自动缩回来。限流能防住瞬时流量,扩容能在几十秒内把容量顶上去,两个配合起来效果最好。
K8S 环境:该用 Sidecar 还是应用层?
上面的方案是在应用代码里写限流和熔断。如果用 Kubernetes + Service Mesh(Istio/Linkerd),能不能把这些逻辑挪到 Sidecar 里?
Sidecar vs 应用层
Sidecar (Service Mesh)
| Pros | Cons |
|---|---|
| 所有服务统一配置,不用改代码 | 很难做”VIP 50 req/s、普通 10 req/s”这种业务限流 |
| Java、Go、Python 用同一套配置 | 无法实现三层降级策略 |
| 不侵入业务代码 | 每个请求多走一次 Sidecar(1-3ms) |
| 自带 Metrics、链路追踪、日志 | 要翻业务容器和 Sidecar 两份日志 |
| 得维护 Istio 控制平面 |
应用层实现
| Pros | Cons |
|---|---|
| 可以按用户等级、会员状态差异化限流 | 每个服务独立维护配置 |
| 灵活写复杂降级逻辑 | Node.js 用 opossum、Java 用 Resilience4j |
| 没有额外网络跳转 | 每个服务都要引入库 |
| 所有逻辑在业务代码,日志清晰 | 需要自己集成监控 |
| 只需熟悉业务代码 |
什么时候用哪个?
用 Sidecar:微服务团队,多语言,需要统一管理
如果有 10+ 个服务,用 Java、Go、Node.js 混合开发,Service Mesh 统一管理更省事:
# Istio 熔断配置(所有服务共享)
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: stock-service-cb
spec:
host: stock-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s用应用层:需要按业务逻辑限流(我们的场景)
促销系统需要:
- 普通用户 10 req/s,VIP 用户 50 req/s
- 库存服务挂了返回缓存
- 推荐服务降级到热门商品
Sidecar 搞不定这些业务逻辑,得在应用层写。
混合方案:Sidecar 做粗粒度 + 应用层做细粒度(推荐)
请求流向:
Cloud Armor(IP 限流)
↓
Istio Sidecar(服务级限流/熔断)
↓
应用代码(用户级限流 + 业务降级)
分工:
- Istio:保护下游服务,每个服务最多 1000 req/s,超了就熔断
- 应用层:处理业务逻辑(VIP/普通用户差异化、三层降级策略)
// 应用层只关心业务逻辑
const vipLimiter = rateLimit({
max: req => req.user.isVip ? 50 : 10, // Istio 做不到
keyGenerator: req => req.user.id
})
// Istio 负责保护下游(在 YAML 配置)K8S 环境完整架构
graph TB User((用户)) User --> Ingress(Ingress<br/>SSL + 路由) Ingress --> Gateway(Istio Gateway<br/>服务级限流) Gateway --> Pod(Pod) subgraph Pod App(应用容器<br/>用户限流 + 降级) Sidecar(Envoy Sidecar<br/>熔断保护) App --> Sidecar end Sidecar --> Downstream(下游服务) style User fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style Ingress fill:#d3f9d8,stroke:#2f9e44,stroke-width:2px style Gateway fill:#e7f5ff,stroke:#1971c2,stroke-width:2px style App fill:#ffe8cc,stroke:#d9480f,stroke-width:2px style Sidecar fill:#e5dbff,stroke:#5f3dc4,stroke-width:2px
渐进式改造
如果已经在应用层写了限流和降级,不建议全部迁移到 Sidecar,因为降级逻辑太复杂(三层降级、缓存、预估库存),Sidecar 搞不定。
渐进改造思路:
- 保留应用层限流和降级(核心业务逻辑)
- 加上 Istio 做下游保护(防止你的服务压垮别人)
# 用 Istio 保护调用的库存服务
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: stock-service
spec:
host: stock-service.prod.svc.cluster.local
trafficPolicy:
outlierDetection:
consecutiveErrors: 5
interval: 10s
baseEjectionTime: 30s这样 opossum 熔断器可以简化一些,让 Istio 处理网络层的熔断。
| 维度 | Sidecar(Istio) | 应用层 | 推荐 |
|---|---|---|---|
| 简单全局限流 | ✅ | ❌ | Sidecar |
| 业务逻辑限流 | ❌ | ✅ | 应用层 |
| 多语言统一 | ✅ | ❌ | Sidecar |
| 复杂降级 | ❌ | ✅ | 应用层 |
| 下游保护 | ✅ | ⚠️ | Sidecar |
| 调试难度 | ❌ | ✅ | 应用层 |
促销系统最佳方案:混合——Istio 做下游熔断 + 应用层做用户限流和业务降级。