高可用 API 网关限流与微服务过载保护实战:从 Sentinel 动态阈值到 DB 连接池舱壁

线上网关一抖,下游服务往往会一起陪葬。真正有效的限流,不是简单地把请求挡在门外,而是让系统在流量突刺、爬虫刷接口和下游变慢时,仍然能够保住核心链路、削平峰值,并且给数据库留出恢复空间。

本文参考《高可用 API 网关限流与防刷实战:Spring Cloud Gateway + Sentinel 动态阈值与智能封禁》的主线,重新整理成一套更完整的工程方案:网关负责快速准入,服务负责熔断和降级,数据库连接池负责最后一道舱壁。示例以 Spring Cloud Gateway、Spring Cloud Alibaba Sentinel、Nacos、Resilience4j 和 HikariCP 为主,版本号请以项目 BOM 为准。

一、先建立完整的过载保护模型

单点限流经常翻车,原因是它只回答了"放不放行",没有回答下面三个问题:

  • 下游 RT 已经升高时,入口阈值是否应该自动收紧?
  • 非核心请求被拒绝后,能否返回缓存、进入队列或走只读降级?
  • 请求已经通过网关,但数据库连接池快耗尽时,谁负责阻止继续排队?

生产环境更适合采用"采集---决策---执行---反馈"的闭环。网关节点只做短路径判断,规则和封禁状态放到配置中心、Redis 或风控服务中;服务节点按接口重要性选择熔断、隔离、缓存和异步化;数据库侧用连接池上限、超时和并发舱壁限制在途请求。
#mermaid-svg-LAHh5g6XmOBu6LFu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LAHh5g6XmOBu6LFu .error-icon{fill:#552222;}#mermaid-svg-LAHh5g6XmOBu6LFu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LAHh5g6XmOBu6LFu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LAHh5g6XmOBu6LFu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LAHh5g6XmOBu6LFu .marker.cross{stroke:#333333;}#mermaid-svg-LAHh5g6XmOBu6LFu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LAHh5g6XmOBu6LFu p{margin:0;}#mermaid-svg-LAHh5g6XmOBu6LFu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster-label text{fill:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster-label span{color:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster-label span p{background-color:transparent;}#mermaid-svg-LAHh5g6XmOBu6LFu .label text,#mermaid-svg-LAHh5g6XmOBu6LFu span{fill:#333;color:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu .node rect,#mermaid-svg-LAHh5g6XmOBu6LFu .node circle,#mermaid-svg-LAHh5g6XmOBu6LFu .node ellipse,#mermaid-svg-LAHh5g6XmOBu6LFu .node polygon,#mermaid-svg-LAHh5g6XmOBu6LFu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LAHh5g6XmOBu6LFu .rough-node .label text,#mermaid-svg-LAHh5g6XmOBu6LFu .node .label text,#mermaid-svg-LAHh5g6XmOBu6LFu .image-shape .label,#mermaid-svg-LAHh5g6XmOBu6LFu .icon-shape .label{text-anchor:middle;}#mermaid-svg-LAHh5g6XmOBu6LFu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LAHh5g6XmOBu6LFu .rough-node .label,#mermaid-svg-LAHh5g6XmOBu6LFu .node .label,#mermaid-svg-LAHh5g6XmOBu6LFu .image-shape .label,#mermaid-svg-LAHh5g6XmOBu6LFu .icon-shape .label{text-align:center;}#mermaid-svg-LAHh5g6XmOBu6LFu .node.clickable{cursor:pointer;}#mermaid-svg-LAHh5g6XmOBu6LFu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LAHh5g6XmOBu6LFu .arrowheadPath{fill:#333333;}#mermaid-svg-LAHh5g6XmOBu6LFu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LAHh5g6XmOBu6LFu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LAHh5g6XmOBu6LFu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LAHh5g6XmOBu6LFu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LAHh5g6XmOBu6LFu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LAHh5g6XmOBu6LFu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster text{fill:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu .cluster span{color:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LAHh5g6XmOBu6LFu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LAHh5g6XmOBu6LFu rect.text{fill:none;stroke-width:0;}#mermaid-svg-LAHh5g6XmOBu6LFu .icon-shape,#mermaid-svg-LAHh5g6XmOBu6LFu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LAHh5g6XmOBu6LFu .icon-shape p,#mermaid-svg-LAHh5g6XmOBu6LFu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LAHh5g6XmOBu6LFu .icon-shape .label rect,#mermaid-svg-LAHh5g6XmOBu6LFu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LAHh5g6XmOBu6LFu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LAHh5g6XmOBu6LFu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LAHh5g6XmOBu6LFu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
429 + Retry-After
采集 QPS/RT/拦截率
异常来源
核心接口
非核心
客户端/爬虫/活动流量
CDN/WAF
Spring Cloud Gateway 集群
Sentinel Gateway Filter

路由限流 + 热点参数限流
微服务集群
客户端退避/排队页
Prometheus + Grafana
规则决策

Nacos 动态阈值 + 风控策略
封禁服务
Redis 黑名单/计数器 TTL
服务熔断/限时/舱壁
MySQL/读写库
缓存/降级数据/消息队列
HikariCP

连接上限 + 获取超时

!\[01-过载保护闭环.png\|网关限流、服务降级与数据库连接池保护闭环]

图中最重要的不是组件数量,而是边界:网关不能把请求无限排队,服务不能把所有异常都重试,数据库不能让每个业务实例随意占满连接。

二、第一道防线:Gateway + Sentinel 做快速准入

1. 依赖和规则数据源

依赖要跟 Spring Cloud Alibaba BOM 对齐,不要单独拼一组互相不兼容的版本。

xml 复制代码
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

网关节点通过 Nacos 订阅规则,gw-flow 是 Gateway 规则类型,不能误写成普通接口资源的 flow

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: ${SENTINEL_DASHBOARD:sentinel-dashboard:8080}
        port: 8719
      datasource:
        gw-flow:
          nacos:
            server-addr: ${NACOS_ADDR:nacos:8848}
            data-id: sentinel-gateway-rules
            group-id: DEFAULT_GROUP
            rule-type: gw-flow
            data-type: json

例如,对订单创建路由按来源 IP 做 1 秒 500 次的基础保护:

json 复制代码
[
  {
    "resource": "api-order-create",
    "count": 500,
    "intervalSec": 1,
    "controlBehavior": 0,
    "burst": 50,
    "paramItem": {
      "parseStrategy": 0,
      "fieldName": null,
      "pattern": null,
      "matchStrategy": 0
    }
  }
]

这里的 GatewayFlowRule 和方法级的 ParamFlowRule 不是一回事。网关规则必须绑定路由资源,否则规则能在配置中心保存,却不会在 Gateway 过滤器上生效。

2. 来源解析要建立信任边界

X-Forwarded-For 只有在请求确实来自可信代理时才能使用。直接信任客户端传来的这个 Header,攻击者可以任意伪造 IP,导致限流和封禁全部失效。生产代码应只读取经过负载均衡器重写的来源,或者使用 RemoteAddress

java 复制代码
@Configuration
public class SentinelGatewayConfig {

    @PostConstruct
    public void init() {
        SentinelGatewayFilter.setRequestOriginParser(request -> {
            // 仅在网关前置代理已清洗 Header 时使用 X-Real-IP。
            String ip = request.getHeaders().getFirst("X-Real-IP");
            if (StringUtils.hasText(ip)) {
                return ip;
            }
            InetSocketAddress remote = request.getRemoteAddress();
            return remote == null ? "unknown" : remote.getAddress().getHostAddress();
        });
    }
}

真正的防刷通常不只看 IP,还会组合 User-Agent、设备指纹、用户 ID 和请求参数熵值。维度越多,越应该把计算拆成轻量的本地判断,复杂画像交给异步风控服务。

3. 被限流后的响应和渐进式封禁

限流处理器要返回明确的 429,并告诉客户端什么时候重试。封禁评估可以异步提交,但不能在 EventLoop 上等待 Redis 或数据库;更不能对所有 429 直接永久封 IP。

java 复制代码
@Component
public class CustomBlockRequestHandler implements BlockRequestHandler {
    private final BanEvaluationService banService;

    public CustomBlockRequestHandler(BanEvaluationService banService) {
        this.banService = banService;
    }

    @Override
    public Mono<ServerResponse> handleRequest(ServerWebExchange exchange,
                                              Throwable ex) {
        String origin = exchange.getRequest().getHeaders().getFirst("X-Real-IP");
        banService.trySubmit(origin, ex.getClass().getSimpleName());
        return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                .contentType(MediaType.APPLICATION_JSON)
                .header("Retry-After", "1")
                .bodyValue(Map.of("code", 42901,
                        "message", "请求过于频繁,请稍后重试"));
    }
}

注册时使用 GatewayCallbackManager.setBlockHandler(...)BlockRequestHandler 返回 Mono<ServerResponse>,由 Sentinel 的 WebExceptionHandler 负责写回响应,不要在业务代码里手动 subscribe();这能避免重复提交和响应未完成的问题。封禁评估可以异步提交,但不能在 EventLoop 上等待 Redis 或数据库;更不能对所有 429 直接永久封 IP。封禁建议分级:首次超限只返回 429,短时间连续超限再写入带 TTL 的 Redis 黑名单,命中多次后才延长封禁;内部系统、CDN 回源和活动白名单必须在网关最前面跳过封禁。

三、第二道防线:服务侧熔断、隔离和降级

网关放行并不代表服务一定有能力处理。下游平均 RT 从 50 ms 变成 500 ms 时,即使入口 QPS 不变,在途请求也会按 Little's Law 近似放大十倍。服务侧必须在自己的边界内再次限制并发和等待时间。

1. Resilience4j 的保守配置

下面是一组适合读接口的起始值,不是通用答案。生产上应使用压测和历史指标重新校准:

yaml 复制代码
resilience4j:
  circuitbreaker:
    instances:
      inventory:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 100
        minimumNumberOfCalls: 40
        failureRateThreshold: 50
        slowCallDurationThreshold: 500ms
        slowCallRateThreshold: 60
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
  timelimiter:
    instances:
      inventory:
        timeoutDuration: 800ms
  bulkhead:
    instances:
      inventory:
        maxConcurrentCalls: 200
        maxWaitDuration: 0

核心原则是:超时要短于上游整体超时,maxWaitDuration 尽量为 0,避免请求在舱壁里排队;重试只允许幂等读请求,并且最多一次,不能把故障流量放大。

2. Reactive 调用示例

java 复制代码
public Mono<InventoryView> query(String sku) {
    return inventoryClient.query(sku)
            .timeout(Duration.ofMillis(300))
            .transformDeferred(CircuitBreakerOperator.of(inventoryBreaker))
            .transformDeferred(BulkheadOperator.of(inventoryBulkhead))
            .onErrorResume(this::isOverloaded,
                    ex -> cache.get("inventory:" + sku)
                            .map(InventoryView::stale)
                            .switchIfEmpty(Mono.just(InventoryView.unknown())));
}

private boolean isOverloaded(Throwable ex) {
    return ex instanceof TimeoutException
            || ex instanceof CallNotPermittedException
            || ex instanceof BulkheadFullException;
}

降级不是把所有异常都吞掉。库存、支付、扣款等核心写请求宁可快速失败并让客户端按业务协议重试;商品详情、推荐、排行榜等非核心读请求可以使用带时间标记的缓存;耗时计算可以返回 202 Accepted 和任务 ID,但前提是请求已经可靠写入队列,不能拿 202 掩盖实际没有接收成功。

3. 动态阈值要有回滚开关

Sentinel 的 SystemRule 可以结合 CPU、Load、平均 RT 和并发线程数收紧入口,但自动调参必须带上下限、白名单和版本号。配置中心故障时,节点应继续使用最近一次成功规则和一套很保守的 fallback,而不是把所有请求放开。规则发布建议走 Git 版本库或配置中心版本历史,出现核心指标断崖时支持自动回滚 30%~50% 的阈值。

四、第三道防线:DB 连接池如何保护

数据库通常不是第一个报警的组件,却是最容易把整个集群拖死的组件。连接池一旦长时间 pending,应用线程、请求上下文和响应缓冲区都会继续占用资源,最后表现为网关 P99 飙升、服务线程池耗尽和数据库连接数打满。

1. 先算总预算,再填 Hikari 参数

连接池不能按"机器越多,连接越多"配置。一个简单的预算公式是:

text 复制代码
单实例最大连接数 <= floor((DB max_connections - 预留连接) / 应用实例数)

例如 MySQL max_connections=300,为运维和复制预留 60 个连接,4 个服务实例平均分配,理论上限为 60;考虑突发和其他作业,应用可以先设为 48,再通过压测调整。若数据库平均查询耗时 20 ms,48 条连接的理论数据库并发大约是 48,不能因为网关能接收几万 QPS 就把池子改成 500。

2. HikariCP 起始配置

yaml 复制代码
spring:
  datasource:
    hikari:
      maximum-pool-size: 48
      minimum-idle: 8
      connection-timeout: 150ms
      validation-timeout: 1s
      idle-timeout: 10m
      max-lifetime: 25m
      keepalive-time: 5m
      initialization-fail-timeout: 1
      register-mbeans: true

connection-timeout 是获取连接最多等多久,不是 SQL 执行超时。两者都要设置:连接获取超时用于快速失败,事务/查询超时用于释放已经借出的连接。leak-detection-threshold 适合预发排查连接泄漏,生产开启要评估日志开销。

3. JDBC 阻塞调用要隔离,并提前设置业务舱壁

如果服务仍使用 JDBC,不能把查询直接放到 WebFlux EventLoop。下面的 Semaphore 是应用层的最后一道并发闸门,数量应小于连接池上限,给健康检查和其他轻量 SQL 留出余量:

java 复制代码
private final Semaphore dbSlots = new Semaphore(40);

public Mono<Order> findOrder(long id) {
    return Mono.defer(() -> {
        if (!dbSlots.tryAcquire()) {
            return Mono.error(new ServiceBusyException("db bulkhead is full"));
        }
        return Mono.fromCallable(() -> orderRepository.findById(id))
                .subscribeOn(Schedulers.boundedElastic())
                .timeout(Duration.ofMillis(200))
                .doFinally(signal -> dbSlots.release());
    });
}

更理想的方案是迁移到 R2DBC,并用响应式连接池;但无论 JDBC 还是 R2DBC,都要保留连接获取超时、SQL 超时和并发上限。只换客户端而没有边界,仍然会把数据库打满。

4. 必须监控的连接池指标

  • active:正在使用的连接数;
  • idle:空闲连接数;
  • pending:等待连接的请求数;
  • 获取连接耗时、SQL 执行耗时和超时次数;
  • 数据库端 Threads_running、慢查询、锁等待和拒绝连接数。

pending > 0 持续 30 秒,或连接获取 P99 超过 100 ms,应先降低入口和非核心接口并发,再排查慢 SQL;不要第一反应就是继续增大连接池。

五、智能封禁和误封治理

限流是防守,封禁是风控动作。黑名单状态可以放入 Redis 并设置 TTL:

java 复制代码
public Mono<Boolean> ban(String origin, Duration ttl) {
    String key = "risk:gateway:ban:" + origin;
    return redis.opsForValue().set(key, "1", ttl);
}

封禁策略建议分三档:

行为 处理 说明
单次超限 429 + Retry-After 给客户端退避机会
10 分钟内多次超限 临时黑名单 15~120 分钟 Redis TTL,支持人工缩短
明确恶意特征 WAF/风控联动封禁 不让业务网关承担复杂画像

对疑似爬虫可以采用"影子封禁":请求仍然透传,但返回缓存数据或低价值数据,同时打上风险标签。这样可以降低误杀真实用户的概率,也能观察策略是否有效。内部系统、回源 IP、压测账号和活动白名单要在策略最前面生效。

六、监控、告警与压测验收

Prometheus 看板至少覆盖:

  • Gateway 路由 QPS、P95/P99、429/5xx、规则同步状态;
  • Sentinel passQpsblockQps、RT、热点参数 TopN;
  • 熔断器状态、慢调用率、Bulkhead 拒绝数和降级命中率;
  • Hikari active/idle/pending、连接获取耗时、慢 SQL 和锁等待;
  • EventLoop pending task、CPU、堆/直接内存、GC、网络带宽。

告警要组合条件,避免"拦截率高就狼来了":例如拦截率连续 2 分钟超过 60%,且下游 P99 同时升高,才升级为服务级告警;单节点 Nacos 断连属于规则级告警;单 IP 突增属于风控事件,不应直接把运维电话打爆。

压测至少做三轮:

  1. 单实例基线:固定报文大小、路由比例和下游延迟,找出 P99 开始恶化的安全 QPS。
  2. 端到端容量:加入真实数据库、缓存、队列和故障响应,验证扩容是否接近线性。
  3. 故障与耐久:模拟下游变慢、Nacos 断连、Redis 延迟、单实例退出和长时间运行,观察规则 fallback、连接池 pending 与内存是否恢复。

结语:把"保护"做成层层递进的边界

一套可靠的流量治理不是 Sentinel 的单点能力,而是四层边界共同作用:CDN/WAF 过滤明显恶意流量,Gateway 快速限流和封禁,服务侧熔断/隔离/降级,数据库连接池限制最后的并发预算。

最容易犯的错误有三个:把固定阈值当成永久答案,把所有异常都重试,把数据库连接池当成吞吐开关。正确做法是用真实压测得到安全容量,用动态规则跟随系统负载,用明确的降级协议保护核心链路,再用连接池 pending 和慢 SQL 指标决定是否继续放行。

原文参考:高可用 API 网关限流与防刷实战:Spring Cloud Gateway + Sentinel 动态阈值与智能封禁

相关推荐
whcyhhh1 小时前
头歌实践教学平台:数据科学与大数据技术导论(七下)
大数据·数据库·python
迪康coolmu1 小时前
企业IT运维闭环——工单管理与远程协助一体化实践
java·大数据·运维·网络·数据库·人工智能·安全
星期一研究室1 小时前
用《颜色》管理项目,混乱现场快速变清晰
微服务·产品·设计
cmes_love1 小时前
期权分钟行情数据下载:商品/股指/ETF期权全覆盖
数据库·区块链
骇客野人1 小时前
MySQL / PostgreSQL 单表及表数据备份恢复实施方案
数据库·mysql·postgresql
东方护航数据恢复(深圳)1 小时前
勒索病毒零赎金解密:加密样本逆向分析与可恢复性评估方法论
大数据·网络·数据库
心平气和量大福大2 小时前
android-实例2-数据库sqlite(查询)
android·数据库·sqlite
海兰2 小时前
【数据库】告别数据库性能噩梦,拥抱 UUID v7
数据库
linweidong2 小时前
字节AI Agent面试题大全及参考答案(下)
数据库·oracle