在凯士比数字工厂(KiPlant)项目中,我主导搭建了支撑万级 QPS 的微服务架构底座。这篇文章记录的不是最终的架构图,而是那个数字是怎么测出来的、瓶颈是怎么找到的、以及每一步优化背后的思考过程。
一、背景与目标
1.1 业务场景
凯士比数字工厂是一个面向制造业的 SaaS 平台,核心功能包括设备全生命周期管理、生产排程优化、能耗分析、数字孪生等。平台采用多租户架构,需要同时服务多家工厂客户。
数据来源有两大类:一是物理设备数据,PLC、传感器等以固定频率(最快每秒一次)上报设备状态、振动、温度等指标;二是业务操作数据,工厂调度员的排产操作、设备巡检人员的点检记录、管理层的报表查询等。
1.2 为什么定 1W QPS
这不是拍脑袋定的数字,而是根据业务规模推算出来的。
单个工厂按 200 台核心设备计算,每台设备每秒上报 1-2 条数据,就是 200-400 QPS 的设备数据写入。加上业务操作、报表查询、大屏刷新等请求,单个工厂高峰期预估 500-800 QPS。平台规划服务 20+ 家工厂,叠加安全冗余系数 1.5 倍,总量级落在 1W QPS 左右。
这个数字的含义是:整个平台在所有租户同时满载运行时,各微服务的聚合 QPS 需要稳定支撑 1W,核心读接口(大屏、报表)的 P99 延迟要控制在 500ms 以内,核心写接口(设备数据采集)不丢数据。
二、压测方案设计
2.1 压测工具选型
我选择了 JMeter + Grafana + Prometheus 的组合。JMeter 负责模拟并发请求和生成压力,Prometheus 采集 JVM、中间件、数据库的运行指标,Grafana 做实时可视化。
考虑过 wrk 和 Gatling,但 JMeter 的优势在于支持复杂的业务场景编排(比如先登录获取 token、再按比例混合不同类型的请求),团队成员也更熟悉。
2.2 压测环境
为了避免压测影响生产环境,我们搭建了一套独立的压测环境,硬件配置和生产环境保持一致:
2.3 压测场景设计
没有用单一接口的极限压测,而是设计了混合场景模拟真实流量分布:
JMeter 使用 Stepping Thread Group 逐步加压:从 100 并发开始,每 30 秒增加 100 并发,直到系统出现明显拐点(RT 急剧上升或错误率超过 1%)。
三、四轮压测与瓶颈突破
3.1 第一轮:网关成为第一个瓶颈(~3000 QPS)
现象: 压到 3000 QPS 时,网关的 CPU 使用率飙到 90%+,下游服务还很轻松。大量请求堆积在网关,RT 从 50ms 飙升到 2 秒。
根因: Spring Cloud Gateway 默认的日志级别是 DEBUG,每个请求都会打印完整的路由匹配和过滤器链执行日志。加上我们自定义的全局过滤器(鉴权、限流、日志记录)中存在同步阻塞调用——鉴权过滤器里用了 RestTemplate 同步调用用户服务校验 token。
优化措施:
第一,将日志级别调到 WARN,减少 IO 开销。第二,鉴权方式从同步 RPC 调用改为本地 JWT 解析。原来每个请求都要调用用户服务验证 token,改为网关本地持有公钥直接解析 JWT,省掉了一次网络往返。第三,将网关的 Netty 工作线程数从默认值调整为 CPU 核心数 × 2。
# Gateway 性能相关配置
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000
response-timeout: 5000
pool:
max-connections: 500
acquire-timeout: 5000
# Netty 线程配置
reactor:
netty:
ioWorkerCount: 8 # 4核 × 2效果: 网关吞吐量从 3000 提升到 7000+ QPS,CPU 使用率降到 50% 左右。
3.2 第二轮:数据库连接池告急(~6000 QPS)
现象: 压到 6000 QPS 时,设备数据上报接口和报表查询接口的 RT 同时飙升。Druid 监控面板显示活跃连接数已经到了上限,等待获取连接的线程堆积。
根因: 多个微服务共用同一个 MySQL 实例,而每个服务的 Druid 连接池默认配置只有 10 个最大连接。设备数据上报是高频写入,报表查询涉及聚合统计(扫描大量数据),两类请求竞争有限的数据库连接,慢查询持有连接时间长,快速写入请求拿不到连接。
优化措施:
第一,按服务拆分数据库职责。设备数据的高频写入走时序数据库 TDEngine,不再写 MySQL。MySQL 只承载业务数据(工单、巡检、用户等)和报表的聚合查询。
第二,调整连接池参数。根据每个服务的实际并发需求差异化配置:
# 设备服务(高频写入 TDEngine 后,MySQL 压力大幅降低)
spring:
datasource:
druid:
initial-size: 5
min-idle: 5
max-active: 20
# 报表服务(聚合查询耗时较长,需要更多连接)
spring:
datasource:
druid:
initial-size: 10
min-idle: 10
max-active: 40
max-wait: 3000第三,对报表服务的慢 SQL 进行优化(加索引、改写子查询为 JOIN、预计算热点统计数据到汇总表)。
效果: MySQL 连接等待归零,报表查询 P99 从 3 秒降到 400ms,整体 QPS 突破 8000。
3.3 第三轮:Redis 热点 Key 问题(~8500 QPS)
现象: 压到 8500 QPS 时,设备状态查询接口偶发超时。Redis 集群中某个节点 CPU 明显高于其他节点。
根因: 大屏展示需要查询"全厂设备状态概览",这个接口会读取一个 factory:{factoryId}:device_overview 的缓存 Key。所有租户的大屏请求都集中到这类 Key 上,而 Redis Cluster 是按 Key 的 hash slot 分配节点的,同一个 Key 的所有请求只会落在一个节点上,形成了热点。
优化措施:
采用本地缓存 + Redis 二级缓存策略。对于设备状态概览这类更新频率固定(每 5 秒刷新一次)、读远大于写的数据,在应用层加了一层 Caffeine 本地缓存,TTL 设为 3 秒:
@Cacheable(cacheNames = "deviceOverview",
key = "#factoryId",
cacheManager = "caffeineCacheManager") // 本地缓存,3秒过期
public DeviceOverview getDeviceOverview(Long factoryId) {
// 本地缓存未命中时,才查 Redis
String key = "factory:" + factoryId + ":device_overview";
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JsonUtils.parse(cached, DeviceOverview.class);
}
// Redis 也未命中,查库并回填
DeviceOverview overview = deviceService.buildOverview(factoryId);
redisTemplate.opsForValue().set(key, JsonUtils.toJson(overview), 5, TimeUnit.SECONDS);
return overview;
}本地缓存拦截掉了约 80% 的请求,到达 Redis 的实际 QPS 大幅下降,热点节点的 CPU 恢复正常。
效果: 设备状态查询接口 P99 从 500ms+ 降到 30ms,整体 QPS 突破 1W。
3.4 第四轮:验证稳定性(1W QPS 持续压测)
最后一轮不再加压,而是在 1W QPS 下持续压测 2 小时,观察系统的稳定性指标:
2 小时压测期间,各项指标平稳,没有出现内存泄漏(堆使用率呈锯齿状波动,没有持续上升),没有连接泄漏,没有消息堆积。
四、架构全景
经过四轮优化后,整体架构如下:
┌──────────────┐
│ Prometheus │
│ + Grafana │
└──────┬───────┘
│ 监控
▼
客户端 ──→ Nginx(LB) ──→ Spring Cloud Gateway(JWT本地鉴权, Sentinel限流)
│
┌────────────┼────────────┐
▼ ▼ ▼
设备服务 排程服务 报表服务 ... (其他服务)
│ │ │ │
▼ ▼ ▼ ▼
TDEngine Redis MySQL MySQL(读库)
(时序) (缓存) (业务) (报表聚合)
│
▼
RocketMQ
(异步解耦/削峰)五、经验总结
压测不是上线前走过场,而是架构优化的驱动力。 四轮压测暴露了四个不同层次的瓶颈(网关→数据库→缓存→稳定性),每一个都不是坐在办公室里能预见到的。
瓶颈永远出现在你最不关注的地方。 第一轮压测前我以为瓶颈会在数据库,结果先倒在了网关的 DEBUG 日志和同步鉴权上。
混合场景压测比单接口压测有价值得多。 单接口压测能测出接口的理论极限,但生产环境的瓶颈往往出在不同类型请求的资源竞争上(比如慢查询和快速写入竞争数据库连接)。
优化顺序很重要:先排除低效配置,再做架构改造。 调日志级别、调连接池参数这些"小事"的投入产出比极高,不要一上来就搞大的架构改动。
如果这篇文章对你有帮助,欢迎访问我的博客 robinzhu.top 获取更多实战分享。