1. 压测前夜:如何为秒杀接口准备一场“真实”的流量风暴

很多朋友一提到秒杀系统优化,第一反应就是上缓存、加队列、搞限流。这没错,但在这之前,有一个更关键、也更容易被忽略的步骤:你得先知道你的系统到底能扛多少,以及它会在哪里“跪下”。这就好比你要给一座桥做加固,总得先知道它哪个桥墩最脆弱吧?这个“脆弱点”,就是我们常说的性能塌陷区。今天,我就结合自己踩过的坑,聊聊怎么用压测找到这个点,再用 Sentinel 精准地“扶住”它。

首先,你得模拟出足够真实的压力。很多新手直接用 JMeter 写死几个用户 ID 就开压,这其实很“失真”。真实的秒杀场景,是成千上万个独立、活跃、已登录的用户在瞬间涌入。所以,压测的第一步,不是写脚本,而是造数据。我们需要批量生成海量用户,并为他们获取有效的登录凭证(Token)。原始文章里提到了生成1万个用户和Token,这思路很对,但我们可以做得更细致、更自动化。

我习惯的做法是,写一个专门的数据构造服务,或者一个独立的 Spring Boot 测试工程。核心是分两步走:批量插入用户和批量模拟登录。插入用户时,直接用 JDBC 的 addBatch() 进行批处理,每1000条提交一次,效率远高于一条条插。这里有个小技巧,手机号可以设计成有规律的(比如从13800000000开始递增),这样后续排查问题时也方便定位。生成 Token 时,要模拟真实的登录接口调用。这里我吃过亏:一开始我直接用 RestTemplate 循环调用,速度慢还容易把自家登录接口打挂。后来改用 Apache HttpClient 配合连接池,并且控制好并发节奏,比如每批只处理200个用户,中间短暂休眠一下,既拿到了Token,又避免了对认证服务造成意外压力。

把这些 Token 妥善保存到 tokens.txt 文件里,这就是我们压测大军的“身份证”。接下来,JMeter 的 CSV 数据文件设置器就能派上用场了,让它每次请求随机读取一个 Token 放到请求头里。这样一来,压测流量里的每个请求都带着不同的、有效的用户身份,最大程度还原了真实场景,测出的瓶颈才可信。

2. 揪出元凶:用 JMeter 压测与性能塌陷区分析实战

数据准备好了,真正的“压力测试”才开始。很多人觉得压测就是线程数调大,然后看结果,其实这里面门道很多。我们的目标很明确:找到系统吞吐量的拐点,也就是性能塌陷区。

我会设计一个阶梯式的增压测试场景。在 JMeter 里,我不用固定线程组,而是用 Concurrency Thread Group 或者 Stepping Thread Group。比如,设定在5分钟内,线程数从100逐步增加到5000。同时,配合 吞吐量控制器,确保我们的核心接口——秒杀下单接口,承受主要的压力。监听器方面,聚合报告和 响应时间图 是必看的,但我更推荐用 后端监听器 将数据实时发送到 InfluxDB,再用 Grafana 做可视化大盘。这样,你能看到一个动态的、连续的性能曲线,拐点在哪里一目了然。

回到我们模拟的秒杀场景。假设我们设置商品库存为300件。开始压测后,你会观察到一系列关键指标的变化:

  • 吞吐量 (Throughput):每秒完成的请求数。
  • 平均响应时间 (Average Response Time)。
  • 错误率 (Error Rate)。

当并发用户数(线程数)较小时,比如1000以下,系统游刃有余,吞吐量线性增长,响应时间平稳。但随着线程数突破某个阈值(比如1200),情况开始变化:吞吐量增长变得极其缓慢,甚至停止增长,而平均响应时间却开始指数级上升,从几十毫秒飙升到几秒甚至几十秒,错误率(可能是超时或系统内部错误)也开始抬头。这个 吞吐量停止增长、响应时间陡增的临界区域,就是性能塌陷区。

在我这次测试中,这个拐点出现在大约 1000 QPS 附近。当并发压力超过这个值,系统并没有处理更多的请求(吞吐量卡在1000左右),但每个请求都变得更慢(响应时间飙升),用户体验急剧恶化,系统资源(如数据库连接)被大量占用等待,濒临雪崩。这个“1000 QPS”就是我们系统的最大健康容量。限流的目标,就是要把流量控制在这个容量之内,不让系统跌入塌陷区。

3. 哨兵就位:Sentinel 核心概念与快速上手

找到了塌陷区,我们就要请出今天的“流量哨兵”—— Sentinel。在引入具体配置前,我们先花几分钟搞清楚 Sentinel 是怎么工作的,这比直接复制粘贴配置更重要。

你可以把 Sentinel 想象成高速公路上的智能匝道控制系统。当主路(你的核心服务)车流量过大即将瘫痪时,这个系统会自动在入口匝道进行限流,控制进入主路的车辆数,保证主路还能以可接受的速度通行,不至于彻底堵死。Sentinel 的核心概念就三个:

  1. 资源 (Resource):你要保护的任何东西,可以是一个URL、一个方法、甚至一段代码。在我们的场景里,就是那个 seckillVoucher 方法。
  2. 规则 (Rule):保护资源的具体策略。比如“每秒最多放行1000个请求”(流控规则),或者“最近5秒内错误率超过50%就熔断”(降级规则)。
  3. 控制台 (Dashboard):一个可视化界面,让你能动态地查看资源监控、管理各种规则,不用改代码就能生效。

安装 Sentinel 控制台超级简单。去 GitHub Release 页面下载一个独立的 sentinel-dashboard-*.jar 包,然后用一条命令启动:

java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -jar sentinel-dashboard-*.jar

默认账号密码都是 sentinel。启动后,访问 http://localhost:8090 就能看到界面。这里注意,控制台本身只是一个监控和管理端,规则数据默认存在内存中,重启会丢失。生产环境需要配合 Nacos、ZooKeeper 或 Apollo 等配置中心做规则持久化,这个我们后面可以再深入。

在项目中引入 Sentinel 依赖,现在通常推荐直接使用 Spring Cloud Alibaba 的 starter,它集成了更多便捷功能:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId> <!-- 如需持久化到Nacos -->
</dependency>

在 application.yml 里简单配置一下控制台地址:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
      eager: true # 取消懒加载,项目启动即连接控制台

做完这些,你的应用就已经和 Sentinel 控制台连接上了。但此时还没有任何规则,哨兵处于“观察模式”。

4. 精准布防:在秒杀接口上配置 Sentinel 流控规则

现在进入最关键的实操环节:给我们的秒杀接口配上“紧箍咒”。根据压测结果,我们知道系统的健康线是 1000 QPS。那么,限流规则就围绕这个值来设置。

首先,我们需要在代码中定义“资源点”。最简单的方式是使用 @SentinelResource 注解。我们改造一下秒杀控制器的方法:

@PostMapping("/seckill/{id}")
@SentinelResource(
    value = "seckill", // 资源名,唯一标识
    blockHandler = "seckillBlockHandler", // 流控降级处理函数
    fallback = "seckillFallback" // 业务异常处理函数
)
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
    // 原有的业务逻辑
    return voucherOrderService.seckillVoucher(voucherId);
}

// 流控或降级处理函数(参数和返回类型需与原方法匹配,最后多一个 BlockException 参数)
public Result seckillBlockHandler(Long voucherId, BlockException ex) {
    log.warn("秒杀接口被限流或降级,voucherId: {}", voucherId);
    return Result.fail("活动太火爆了,请稍后再试!");
}

// 业务异常降级处理函数(Throwable 参数)
public Result seckillFallback(Long voucherId, Throwable th) {
    log.error("秒杀接口业务异常", th);
    return Result.fail("秒杀服务暂时不可用");
}

这里 blockHandler 专门处理流量控制、熔断降级等 Sentinel 规则触发的异常,而 fallback 处理的是业务代码本身抛出的异常。分开处理能让日志更清晰。

代码改好后,启动应用并手动访问一次秒杀接口。然后刷新 Sentinel 控制台,你应该能在左侧看到名为 seckill 的资源。点击它,进入“流控规则”页面,点击“新增流控规则”。

关键配置来了,我通常会这样设置:

  • 资源名: seckill (与注解 value 一致)
  • 流控模式: “QPS”(我们根据压测的QPS拐点来限)
  • 单机阈值: 1000 (这就是我们测出的临界点)
  • 流控效果: “快速失败”

“快速失败”就是直接抛 BlockException,走我们上面写的 blockHandler 方法,响应最快。另外两种模式,“Warm Up”(预热)适合系统冷启动后慢慢放量,“排队等待”则适合处理脉冲流量,让请求匀速通过,但会增加平均响应时间。对于秒杀这种瞬时高峰,我一般首选“快速失败”。

点击“新增”后,规则立刻生效。现在,任何一秒内,对这个 seckill 接口的访问超过1000次,第1001次及之后的请求,都会立刻收到我们自定义的“活动太火爆了,请稍后再试!”的提示,而不会进入真正的业务逻辑。这就好比在系统即将跌入塌陷区的前一刻,设置了一道坚固的闸门。

5. 效果验证与调优:限流前后的压测对比与规则进阶

布防完成,是骡子是马,拉出来再遛遛。我们再次启动 JMeter,用同样的压测脚本(比如3000线程,循环5次)对系统发起冲击。

这次,你会看到截然不同的结果。在聚合报告里,虽然还是会有一部分请求失败(被限流),但成功的请求(即进入业务的那些)的响应时间会保持在一个相对稳定、可接受的范围,比如几十到一百毫秒。系统整体的 CPU、内存、数据库连接数等资源,也不会像第一次压测那样被拖垮。整个系统从“可能被冲垮”变成了“有损但稳定”的服务。

我们来看看 Sentinel 控制台提供的监控数据,它比 JMeter 报告更直观:

  • 实时监控图:你会看到一条代表 seckill 资源 QPS 的曲线,它会被牢牢地压制在 1000 这条线附近波动,证明限流规则在起作用。
  • 簇点链路:可以清晰地看到每个资源的通过 QPS、拒绝 QPS、响应时间等。
压测轮次场景描述平均响应时间吞吐量 (QPS)系统状态
第一次无限流,3000线程压测> 3000ms (持续恶化)先升后卡在~1000CPU飙高,DB连接耗尽,濒临崩溃
第二次限流1000 QPS,3000线程压测~150ms (稳定)稳定在~1000资源使用平稳,被限流的请求快速失败

这个对比非常鲜明。限流不是提高系统处理能力,而是在能力不足时,选择保护系统,牺牲掉一部分非核心的请求,保障整体和大部分用户的可用性。

基于这个基础,我们可以做更精细化的调优。例如:

  • 关联流控:如果“查询库存”接口和“扣减库存”接口有关联,可以设置“查询”的流量大了,就限制“扣减”的调用。
  • 热点参数限流:针对某些特别热门的商品ID(比如 voucherId=100),单独设置更低的限流阈值,避免一个热点商品拖累整个秒杀服务。
  • 系统自适应保护:设置全局的防护规则,比如当系统 CPU 使用率超过 80%,或平均 RT 超过 500ms,就触发全局限流,这是一个最后的兜底策略。

6. 避坑指南:Sentinel 在生产环境部署的注意事项

最后,分享几个我把 Sentinel 用到生产环境时踩过的坑,希望能帮你少走弯路。

第一个坑:规则丢失。 默认情况下,在控制台配置的规则只存在客户端内存和服务器内存中,应用重启或控制台重启,规则就没了。生产环境必须配置规则持久化。我最常用的是推模式持久化到 Nacos。在 application.yml 中配置 Nacos 数据源,这样规则会在 Nacos 中保存,客户端启动时自动拉取,控制台修改规则也会同步推送到 Nacos 和所有客户端。

spring:
  cloud:
    sentinel:
      datasource:
        ds:
          nacos:
            server-addr: localhost:8848
            dataId: ${spring.application.name}-sentinel-flow
            groupId: DEFAULT_GROUP
            rule-type: flow

第二个坑:懒加载导致监控空白。 有时候应用启动后,Sentinel 控制台看不到任何资源。这是因为 Sentinel 默认是“懒加载”的,只有资源被访问过一次后才会被监控。我们之前配置的 eager: true 就是为了解决这个问题。如果还不行,检查一下客户端与控制台的网络连通性。

第三个坑:BlockException 处理函数不生效。 这可能是最常见的问题。请务必检查:

  1. 函数必须是 public。
  2. 返回类型必须与原方法一致。
  3. 参数列表必须与原方法一致,并在最后额外加一个 BlockException 类型的参数。
  4. 方法需要写在同一个类里。如果不想污染 Controller,可以使用 blockHandlerClass 属性指定外部类,但外部类里的处理方法必须是 static 的。

第四个坑:忽略某些流量。 像健康检查接口 /actuator/health、内部心跳等,不应该被限流。可以在配置中设置例外:

spring:
  cloud:
    sentinel:
      filter:
        enabled: false # 关闭对所有URL的自动埋点
      web-context-unify: false

然后通过自定义 UrlCleaner 接口或 RequestOriginParser 接口,来精细控制哪些请求需要被 Sentinel 管理。

把这几件事做好,Sentinel 才能真正成为你线上系统稳定的守护神。限流从来不是“一劳永逸”的配置,它需要结合持续的监控和压测,动态调整。当你对系统的性能边界了如指掌,并能用工具稳稳地守住这条边界时,面对任何流量洪峰,你心里都会更有底。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐