我参与的某零售巨头的促销活动系统,上线后 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 ArmorLoad 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)

ProsCons
所有服务统一配置,不用改代码很难做”VIP 50 req/s、普通 10 req/s”这种业务限流
Java、Go、Python 用同一套配置无法实现三层降级策略
不侵入业务代码每个请求多走一次 Sidecar(1-3ms)
自带 Metrics、链路追踪、日志要翻业务容器和 Sidecar 两份日志
得维护 Istio 控制平面

应用层实现

ProsCons
可以按用户等级、会员状态差异化限流每个服务独立维护配置
灵活写复杂降级逻辑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 搞不定。

渐进改造思路

  1. 保留应用层限流和降级(核心业务逻辑)
  2. 加上 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 做下游熔断 + 应用层做用户限流和业务降级。