目录

写在前面... 5

一、扩容这件事:先学会不添乱... 6

1.1 第一次大促,我们输给了自己... 6

1.2 全线告急:当六个节点同时告警... 8

1.3 三十六台机器撑住了流量,但转化率只有百分之一点几... 9

二、缓存这一层,我们改了四轮... 11

2.1 连接池太小:高并发下的数学必然... 11

2.2 一个"看起来无害"的后台遍历... 12

2.3 会话缓存混用:最意想不到的那个... 14

2.4 负载不高却超时:批量操作与连接池的叠加效应... 16

三、热 key:那个拖了两年的技术债... 18

3.1 单个节点被压到接近满载... 18

3.2 为什么会这样... 19

3.3 三阶段改造... 21

3.4 关于这个技术债... 23

四、防刷:从"封了没用"到釜底抽薪... 23

4.1 上亿次请求的单账号... 23

4.2 为什么接口扛不住... 24

4.3 处置过程:一波三折... 24

4.4 防刷的四层防御... 26

4.5 接口加固的代码参考... 27

五、几条方法论... 28

5.1 分层应对,而不是单点押注... 28

5.2 先止血,再根治... 29

5.3 告警是结果,往上游推两层... 29

5.4 工具链的投入是有复利的... 30

5.5 无效流量是隐性性能问题... 30

六、监控告警:先能看见,才能治理... 31

6.1 指标选型... 31

6.2 采集频率... 32

6.3 告警分级与抑制... 32

七、容量规划:把不确定性算进去... 33

7.1 四步法... 33

7.2 一份容量清单参考... 34

7.3 压测的几个要点... 34

八、写在最后... 35

附:常用参数速查... 36

凌晨三四点盯着监控大屏的那种感觉——曲线在爬,告警在响,你不知道下一秒会不会全线崩掉。
 

这篇文章记录的是我们跨境电商平台上几年来的稳定性治理过程。回头看,早期我们基本是"告警驱动"的:出问题了才去扩容、去救火;后来才慢慢转向"预案驱动":活动前把容量、限流、降级、风控全部算清楚,演练一遍。
 

这个转变背后,是十几次线上告警、几十次技术决策的积累。我把它们按问题类型整理成了几条线,每一条都跨越了不止一年。之所以会反复,根本原因都一样:第一次只做了止血,没有根治
 

下面这些内容,有些是架构层面的判断,有些是很具体的参数配置,都来自真实的生产环境。希望对做高并发系统的同学有些参考价值。
 

1.1 第一次大促,我们输给了自己
 

那是平台第一次真正意义上的流量考验,某国新品首发,周末两天。
 

第一天下午五点开始,告警断断续续地来。看了下监控,各项指标"看着还行",就没太当回事。六点不到告警又来了,联系运维,反馈是"服务端机器运行正常"。等到发现是接入层压力大的时候,我们决定扩容——然后部署失败了
 

是的,在流量最高的时候,我们的扩容部署失败了。没有回滚机制,也没有分批灰度,二十台机器一起上,状态不一致反而让情况更糟。最后一直到晚上八点半流量自然回落,服务才恢复。第一天就这么过去了。
 

第二天更糟。五点流量回升,可用率开始掉。我们申请扩容到 20 台节点,陆续上线之后——可用率不但没涨,反而继续往下掉。六点半收到 Redis 内存告警,但这条告警被淹没在一堆其他告警里,等我们注意到已经是很久之后。六点四十 CPU 负载告警,追加应用服务器。七点部署完成,可用率才开始回升。七点半剔除异常节点又引起一波抖动,重新加回才算稳住。
 

那两天最后的资源是:接入层 20 台、应用层 8 台、Redis 临时扩到 8G。
 

事后复盘,暴露的是三个很基础的问题:
 

  • 扩容流程本身不成熟。部署失败没有回滚,一次上 20 台没有分批,扩容这个动作本身成了新的故障源。
     
  • 告警没有分级。所有告警走同一个通道,真正要命的那条被淹没了。
     
  • 没有 CDN。静态资源全压在接入层,这部分流量本可以完全卸载掉。
     

对应的改进:建立了大促活动报备制度(提前一周提交容量需求);确定了 CDN 方案(按区域配置回源);制定了分批灰度规范(20% → 50% → 100%,每批观察 5 分钟)。
 

关于容量估算,我们后来沉淀了一个简单的公式:
 

代码清单 1|容量估算公式

预估 QPS = (DAU × 人均日请求数 / 86400) × 峰值系数
单机承载 QPS ≈ Tomcat 线程数 × 1000 / 平均响应时间(ms)
所需节点数 = ⌈预估 QPS / 单机承载 QPS × 1.5⌉

 

峰值系数这个东西,电商大促一般取 5~10,具体看活动形态。1.5 是冗余系数,我个人的习惯是不低于这个数,因为你永远算不准。
 

举个例子:预估峰值 8000 QPS,单机 Tomcat 线程 200、平均 RT 50ms,那单机大概能扛 4000 QPS,理论 2 台就够,但算上冗余至少 4 台,实际上我们会给到 6~8 台。
 

1.2 全线告急:当六个节点同时告警
 

还是那个市场,几个月后的一次大型赛事营销活动。
 

这次的告警是这样的:
 

代码清单 2|告警样例:缓存实例六节点内存告警

【缓存实例告警】
node-1(95.1, 95.4, 95.8)
node-2(95.0, 95.3, 95.5)
... 6/6 节点全部超过 95%
告警级别:灾难

 

六个节点内存使用率全部逼近 96%。这意味着 Redis 随时可能开始按 LRU 策略驱逐 key,一旦缓存大面积失效,请求会直接打穿到数据库——那是真正的雪崩。
 

这里有个判断很关键:六个节点同步飙升,说明问题不在某一个 key,而是全局性的使用问题。 要么是连接池设计有问题,要么是某类 key 在全局范围内膨胀。这个判断直接决定了后续的排查方向。
 

当时的连锁反应:大量 502 和连接中断;重启接入层之后详情页依然报错;数据库响应时间明显上涨,整个系统处在崩溃边缘。
 

处置动作分两层:
 

止血层:
- Redis 内存从 8G 扩到 12G
- 应用服务器扩到 12 台
- CDN 配置生效

 

根因层:
- 修复连接池配置(这个下一章细说)
- 清理了后台服务里一个多余的遍历操作(下一章细说)

 

另外,这次排查顺带发现了一个隐患:商品属性缓存的单个 key 接近 10MB,地址缓存也有类似量级。当时把它们记进了待优化清单。后来的事实证明,这个"待优化"拖了很久,代价很大。
 

1.3 三十六台机器撑住了流量,但转化率只有百分之一点几
 

年底大促,三天,流量分三波持续拉升。
 

这次的流量形态和之前不一样。之前是脉冲式的——冲上来一个尖峰,几分钟就回落;这次是持续式的,流量上来之后在高位徘徊了几个小时,可用率一路阴跌到 87% 左右就横住了。
 

持续式流量比脉冲式难处理得多。脉冲式只要扛过那几分钟就行,持续式要求整条链路——CDN、接入层、应用层、缓存、数据库——连续几个小时都保持稳定,任何一个环节先扛不住,前面做的都白费。
 

因为提前做了预判,我们在限流平台上配好了开关。第一波来了,从 12 台加到 24 台;第三波又突破,继续加到 36 台,同时开了降级兜底。最终扛住了。
 

但活动结束看转化数据的时候,挺尴尬的:详情页点击转化率只有百分之一点几。三十几台机器扛下来的流量里,绝大部分是刷屏点击和机器人,不是真实购买意图。
 

这件事给我们的触动很大。以前我们一直把刷量和黄牛当成安全问题,交给风控去管。但那一刻意识到——它们消耗的是真实的 CPU、真实的缓存连接、真实的数据库 IO。无效流量本质上是性能问题,是性能预算的一部分。
 

这个认知转变,直接推动了后面的防刷专项。
 

2.1 连接池太小:高并发下的数学必然
 

上面那次全线告警里,除了内存问题,还有一个:连接池耗尽。
 

报错很经典:
 

代码清单 3|异常日志:连接池耗尽(Could not get a resource)

JedisConnectionException: Could not get a resource from the pool
 

原因是连接池的 maxTotal 用的是一个偏小的默认值。高并发场景下,所有连接都被占着,新请求拿不到连接,一直等到超时。
 

修复方式很直接——把 maxTotal 调大。但调大多少不是拍脑袋的,后来我们整理了一个算法:
 

代码清单 4|连接池 maxTotal 估算公式

maxTotal ≈ (Tomcat 最大线程数 × 缓存操作占比) / 实例数 × 冗余系数
 

几个经验值:
 

参数

建议值

说明

maxTotal

100~200

单实例,不建议再高

maxIdle

50

最大空闲连接

minIdle

10

避免频繁建连

maxWaitMillis

1000~3000

批量操作场景取上限

testOnBorrow

false

开启会增加约 5% 延迟

为什么不建议把连接池开得特别大?因为 Redis 是单线程处理命令的,连接数上去之后,服务端 select/epoll 的系统调用开销会明显增加。一般经验是:单实例 maxTotal 不超过 200,所有应用实例的连接总数控制在 10000 以内。
 

2.2 一个"看起来无害"的后台遍历
 

还是那次活动,我们在消息消费服务里发现了一段定时执行的遍历缓存 key 的代码,用于清理商品列表页缓存。
 

这个操作看起来人畜无害——不就是清个缓存吗?而且用的还是游标式遍历,单次调用不会长时间阻塞。
 

问题在于:Redis 处理命令是单线程串行的。 游标遍历确实把一次大操作拆成了很多次小操作,但当并发量上去之后,这些小操作会挤在同一条处理队列里,和业务请求互相排队。我们后来实测到,这类操作的并发能到几百,直接把实例拖到响应极慢。
 

更要命的是——这个操作根本没必要存在。商品列表页缓存晚几分钟清除,用户完全感知不到。
 

所以第一次的处理很简单:删掉多余的遍历调用。
 

但过了几个月,又一次推送活动,实例又超时了。查下来发现还有另一条触发路径:降级开关打开的时候,会触发后台批量清缓存的遍历逻辑。
 

这说明第一次根本没处理干净。我们加了兜底:降级期间不执行任何缓存清理。降级场景下,这类操作本就可以跳过。
 

关于游标遍历的几个原则,我们后来固定下来了:
 

  • 高频清理 → 用 EXPIRE 自动过期,不要用遍历
     
  • 低频维护 → 可以用遍历,但单实例并发控制在 5 以内
     
  • 紧急删除 → 用 UNLINK(非阻塞删除),不要用 DEL
     
  • 大 key 排查 → --bigkeys / MEMORY USAGE / HLEN,别用 KEYS
     

2.3 会话缓存混用:最意想不到的那个
 

遍历的问题处理完之后,超时告警还是没消。
 

我们仔细分析了一下超时的 key 分布,发现超时的几乎全是会话相关的 key:
 

代码清单 5|会话缓存 key 样例(spring:session)

spring:session:sessions:*
spring:session:expirations:*

 

原因清楚了:会话缓存和业务缓存共用了同一个集群。
 

这里要理解 Spring Session 的一个行为——它默认会在每次请求时滑动更新会话的过期时间,也就是说,每一次用户请求都会触发一次同步的写操作。流量高峰的时候:
 

  • 大量用户请求 → 大量会话读写
     
  • 业务接口也在大量读写缓存
     
  • 两者共享同一个连接池,互相抢占
     
  • 会话的写操作吃掉了本该给业务用的连接和内存
     

这就解释了一个当时让我们很困惑的现象:遍历关掉了、连接池加大了,但接口还是在超时。 因为请求根本没到 Redis 那里,在应用本地的连接池排队阶段就已经超时了。
 

处置:
 

  • 物理隔离。会话缓存和业务缓存拆成独立集群,彻底断开耦合。这是最关键的一步。
     
  • 本地缓存过期时间延长,减少回源压力。
     
  • 限流配置上线。
     
  • 非必要请求不主动创建会话,减慢 key 膨胀速度。
     

隔离做完之后,key 数量恢复正常,接口趋于平稳。
 


补充一点:会话集群独立之后,它的连接池可以单独配小一些(30~50 就够),因为会话写频率远低于业务读频率。共用的时候不敢这么配,分开之后就可以精细控制了。

 

2.4 负载不高却超时:批量操作与连接池的叠加效应
 

再往后,线上开始偶发读取超时。频率不高,持续存在,找不到规律。
 

代码清单 6|异常日志:读取超时(Read timed out)

JedisConnectionException: Read timed out
 

注意这不是建连失败,是发出请求之后等响应超时
 

看监控:正常基线大概 500 ops/s,告警时峰值 2500 ops/s。内存、CPU 都不高,不是典型的资源瓶颈表现。
 

很奇怪——负载这么轻,为什么会超时?
 

我们翻了调用日志,发现超时集中在商品批量查询的方法上,这个方法用批量管道一次读取多个规格的促销数据。
 

定位到根因之后,链路是这样:
 

代码清单 7|超时链路推导:批量管道占用连接

高并发瞬间,大量用户同时查商品列表
  → 每个请求用批量管道读多个 key
  → 批量操作占用连接的时长,远大于单次操作(要等所有响应回来)
  → 连接池瞬间被占满
  → 新请求排队等待
  → 等待时间设得太短
  → 超时

 

这完美解释了"偶发、集中在某几个时间点"的现象——平时并发低没事,小高峰的几秒钟内连接池瞬间打满。
 

这里要理解批量管道的本质:它把多个命令打包发送,节省的是网络往返次数,但服务端仍然是串行处理的,它并不节省 CPU 时间。 而且因为要等所有响应返回才释放连接,它占用连接的时长反而比单条命令更长。这是一个很容易被忽略的点。
 

修复(按当前线程数和缓存操作占比重新计算):
 

代码清单 8|连接池配置示例(properties)

# 单机 Tomcat 线程 200,缓存操作占比约六成
xxx.cache.depend.common.poolConfig.maxTotal=200
# 批量操作等待时间需要更宽松
xxx.cache.depend.common.poolConfig.maxWaitMillis=3000
xxx.cache.depend.common.poolConfig.minIdle=10
xxx.cache.depend.common.poolConfig.maxIdle=50

 

再往后,我们给促销价格这类热点数据加了进程内缓存(几秒级过期),热点规格的高频读请求在本地就命中了,不再每次都走缓存集群。
 

3.1 单个节点被压到接近满载
 

某次推送活动,告警来了:
 

代码清单 9|告警样例:单节点 CPU 打满(热 key)

【缓存实例告警】
node-1(99.97)      ← 单个节点 CPU 打满
告警级别:灾难
异常比例:16.7% (1/6)

 

六个节点,只有一个 CPU 到了 99.97%,其他五个都很正常。
 

这是热 key 的典型特征。 如果是全局流量问题,六个节点会一起涨;只有一个节点爆掉,说明大量请求集中打在了同一个 key 上。
 

先用降级开关临时止血——非核心模块不展示,那个节点的 CPU 确实降了一些。但代价是预约这类业务也一起被屏蔽了,体验折损很明显。这只是临时方案。
 

第二天同样的问题又出现,我们开始认真定位:
 

  1. 用流量分析平台看哪个页面访问量异常
     
  2. 用调用链追踪定位到具体是哪些商品(关联了预约活动的新品)
     
  3. 拉缓存热点日志确认,热点集中在商品促销这个 key 系列上
     

确认之后发现:这个 key 里的字段数量达到了数百个。
 

3.2 为什么会这样
 

商品活动缓存当初是这么设计的——把一个商品下所有规格的促销信息,全部塞进同一个哈希结构里:
 

代码清单 10|数据结构:商品促销大 Hash

商品促销:{商品ID}
  字段: 规格ID_8280  → 促销数据
  字段: 规格ID_8281  → 促销数据
  ...(数百个字段)

 

查询的时候用全量拉取的方式,一次把这个结构整个读出来。也就是说,哪怕你只需要其中一个规格的数据,也得把几百个字段全拉一遍
 

热点商品(比如正在做活动的新品)被大量用户同时访问,所有请求都打向存储这个结构的那一个节点。
 

这里要理解集群模式下热 key 为什么无法通过扩容解决:
 

集群把 key 空间分成 16384 个槽,每个节点负责其中一段。路由方式是:
 

代码清单 11|集群槽位路由算法(CRC16)

key → CRC16(key) % 16384 → 槽位节点
 

同一个 key 永远落在同一个节点上。 你加再多的节点,这个 key 还在原来那个节点——除非手动做数据迁移。所以扩容对单个热 key 是无效的。
 

另外补充一点:哈希结构在小字段量的时候是有内存优势的(内部用紧凑编码),但字段数超过阈值之后会转成普通哈希表,这个优势就没了。几百个字段早就超出了它的优化区间。
 

3.3 三阶段改造
 

第一阶段:改数据结构
 

把"一个商品 → 一个大结构"改成"每个规格 → 独立 key":
 

代码清单 12|数据结构改造前后对比

改造前:全量拉取  商品促销:{商品ID}          # 读几百个字段
改造后:按需批量取  商品促销:{规格ID_1}
                  商品促销:{规格ID_2}
                  ...                     # 用多少取多少

 

同时把序列化方式也换成了更高效的。这一步做完,CPU 就下来了,响应时间不再有突刺。
 

第二阶段:从源头减少请求量
 

改造前,用户一进商详页就把这个商品所有规格的促销数据全请求一遍。改完之后,进页面只返回默认规格的数据,用户切换规格时才按需查当前规格
 

这一步的收益其实比第一步还大——它直接把商详页的缓存调用量降了一个数量级。
 

第三阶段:进程内热点缓存
 

接入热点缓存组件,在应用进程内维护一层短过期时间的本地缓存。热点商品的促销数据在本地就命中了,完全不需要走到缓存集群。
 

组件

特点

适用场景

Caffeine

性能好,API 干净

单 JVM 场景

JetCache

支持多级缓存、注解式

Spring 技术栈

J2Cache

支持集群间失效广播

对一致性要求高

上线效果:
 

  • 移动端超时告警:消除
     
  • 熔断告警:消除
     
  • 缓存响应时间:稳定,无突刺
     
  • 商品接口响应时间:大幅下降
     

3.4 关于这个技术债
 

这个大 key 其实在两年前那次全线告警的时候就发现了。当时的处理是——记进待优化清单,标注"优先级排后"。
 

两年后,它以 CPU 打满的灾难级告警形式,逼着我们必须处理。
 

几乎每一个"先欠着"的设计缺陷,最后都会在最忙、最紧张、最不希望出问题的时候还账。 区别只是早还和晚还,利息差很多。
 

4.1 上亿次请求的单账号
 

某次活动,日志告警突然涌现,某个市场的商城被刷了。
 

随手搜了一个账号的请求日志——请求量上亿次。而这还只是其中一个账号。
 

CPU 频繁告警,缓存、数据库、账号系统的请求量全线大幅上升。
 

攻击目标是下单确认接口。这个接口正常每分钟大概几百次请求,被刷的时候是上万次。攻击者绕过了前端的商品校验,直接调后端接口反复提交。
 

4.2 为什么接口扛不住
 

关键在这里:下单确认接口缺少对商品下架和无库存的前置校验。
 

所以每一次刷量请求,都会走完整的下单链路:
 

代码清单 13|下单确认链路(改造前)

查商品查库存查用户查账单地址库存不足失败
 

即便最终因为库存不足失败了,前面这些数据库和缓存查询已经全部执行了一遍。无效请求的单次成本,被拉到了和有效请求一样的量级。
 

4.3 处置过程:一波三折
 

第一波:WAF 封禁 —— 没用
 

发现被刷之后立刻联系配 WAF 封禁规则,封了一批 IP,但请求量没有下降
 

排查下来,问题出在规则配置上:WAF 读的是请求头里的客户端 IP 链,但规则里的匹配路径写错了,根本没命中目标接口。修正之后重新配置,封了十几个 IP 段,流量才下来一些。
 


教训:WAF 规则上线前必须在预发环境走完整链路验证。等到活动当天才发现规则没生效,是最糟糕的时机。

 

第二波:换账号维度 —— 有效但低效
 

IP 封禁很快就见顶了。攻击者换 IP 的成本极低,秒拨 IP 几毛钱就能换一批。而且这次攻击没有特别集中的固定 IP,更多是账号维度的异常。
 

改成人工拉黑高频账号——有效,但黄牛账号数量太多,人工拉黑本质是打地鼠。
 

第三波:接口加固 —— 真正解决问题
 

真正起作用的还是改代码:在确认接口入口处补充商品下架和无库存的前置校验。
 

代码清单 14|接口加固前后对比

改造前:请求进来走完整下单链路库存不足失败
改造后:请求进来检查商品状态下架/无库存立刻返回

 

改造之后,无效请求在入口处就被挡掉了,整个链路十几毫秒内结束。提交类请求量下降了五倍。
 

顺带还做了一个优化:账单地址查询改成后置——先通过所有业务校验,最后才查账单地址,进一步减少无效的数据库查询。
 

4.4 防刷的四层防御
 

后来我们把防刷整理成了一个分层模型:
 

层级

手段

实施成本

拦截效果

网络层

WAF、IP 黑名单、机房 IP 识别

20%~30%

应用层

账号限流、验证码、滑动验证

30%~40%

业务层

接口加固、状态前置校验

50%~70%

风控层

行为指纹、设备聚类、模型

极高

80%+

必须多层同时生效。 我们这次踩的坑就是最好的证明——WAF 那一层因为配置问题完全失效了,如果当时只有这一层,后果会很严重。
 

从投入产出比看,业务层的接口加固是 ROI 最高的。网络层和应用层成本虽低,但攻击者绕过成本也很低;风控层效果最好,但需要长期投入特征库和模型迭代。
 

4.5 接口加固的代码参考
 

代码清单 15|下单确认接口加固示例(Java)

@PostMapping("/api/order/confirm")
public Result confirm(@RequestBody OrderRequest request) {
    // 1. 账号级限流,单账号每分钟 5
    String userId = getCurrentUserId();
    if (!rateLimiter.tryAcquire(userId, 5, 1, TimeUnit.MINUTES)) {
        return Result.fail("RATE_LIMIT", "操作过于频繁");
    }

    // 2. 商品状态前置校验,下架直接返回
    ProductDTO product = productService.getById(request.getProductId());
    if (product == null || product.isOffShelf()) {
        return Result.fail("PRODUCT_OFF_SHELF", "商品已下架");
    }

    // 3. 库存前置校验
    if (!stockService.hasStock(request.getSkuId(), request.getQuantity())) {
        return Result.fail("OUT_OF_STOCK", "库存不足");
    }

    // 4. 核心前置校验全部通过后,才走后续业务
    return orderService.create(request);
}

 

核心思路就一句话:把最便宜的判断放在最前面,把最贵的资源留给真正有效的请求。
 

5.1 分层应对,而不是单点押注
 

扩容、限流、降级、WAF、接口加固,是五个不同层次的手段。每一层都可能单独失效:
 

手段

常见失效模式

扩容

部署失败、节点状态不一致

限流

阈值拍脑袋、活动当天才配置

降级

屏蔽了正常业务,体验折损

WAF

路径配错、被轻松绕过

接口加固

覆盖不全,有漏网之鱼

真正靠得住的不是某一层做到极致,而是多层同时生效形成纵深。而且要在活动前逐层检查确认,活动中的临时补救代价太高了。
 

5.2 先止血,再根治
 

降级和扩容是争取时间的手段,不是解决问题的手段。
 

每次用完临时方案之后,必须逼自己去找根因。 会话缓存隔离那次,降级开关打开只争取了几个小时的喘息,关键是同期把混用问题彻底查清楚了,才有了后面的根治。
 

不去找根因,下次同样的流量来了,必然以同样的方式崩。
 

5.3 告警是结果,往上游推两层
 

不要停在告警的直接现象上。
 

现象

上推一层

再推一层(根因)

缓存内存满

大 key

会话与业务混用

接口超时

连接池打满

批量操作 + 连接数配置不当

CPU 打爆

热 key

大结构全量拉取,集群无法分散

每次多问一个"为什么",才能走到真正值得修的地方。
 

5.4 工具链的投入是有复利的
 

没有热点日志和调用链追踪,热 key 问题靠猜可能要排查一周;有了工具,一天内就能定位到具体商品和 key 结构。
 

监控告警、热点分析、调用链追踪这些基础设施,每提升一个层次,下次排障就能省下大量时间。这是少数几件"投入一定有回报"的事情。
 

5.5 无效流量是隐性性能问题
 

三十几台机器扛住了峰值流量,但转化率只有百分之一点几——大量资源在为无效请求服务。
 

防刷本质上也是性能预算的一部分:把有限的系统资源优先留给真实用户,才是高并发治理的最终目标。这件事不能只丢给安全团队。
 

前面所有案例都依赖一个前提——你得先看见问题。这一章补充我们在监控体系上的一些实践。
 

6.1 指标选型
 

业界常用的两套框架:
 

RED(服务视角)
- Rate:每秒请求数
- Errors:失败率
- Duration:响应时间(平均 / P95 / P99)

 

USE(资源视角)
- Utilization:资源使用率
- Saturation:饱和度(队列长度、线程池活跃数)
- Errors:错误计数

 

我个人的体会是:平均响应时间基本没有参考价值,一定要看 P99。 平均 50ms 的接口,P99 可能是 800ms,而用户投诉的永远是后者。
 

6.2 采集频率
 

指标类型

建议频率

存储周期

业务 QPS / 响应时间

10s

30 天

缓存 / 数据库性能

15s

90 天

主机 CPU / 内存

30s

30 天

应用 GC / 线程池

60s

30 天

关键业务事件

实时

长期

采集频率不是越高越好。频率太高,存储和查询成本会快速上升;太低又会漏掉瞬时尖峰。业务核心指标 10 秒是个比较平衡的选择。
 

6.3 告警分级与抑制
 

代码清单 16|告警分级标准(P0 / P1 / P2)

P0 紧急:核心接口可用率低于 99% 持续 1 分钟
         通知:电话 + 短信 + @所有人
         响应:5 分钟内

P1 严重:缓存内存超 90%、数据库慢查询超阈值
         通知:短信 + @值班
         响应:15 分钟内

P2 警告:GC 频率异常、磁盘使用超 80%
         通知:群消息
         响应:1 小时内

 

告警抑制是必须做的。 否则一个集群宕机会引发几十条关联告警,真正重要的那条反而被淹没了——我们早期就吃过这个亏。
 

代码清单 17|告警抑制规则示例(Prometheus)

inhibit_rules:
  - source_match:
      alertname: CacheClusterDown
    target_match:
      alertname: CacheHighMemory
    equal: ['cluster']

 

上面这条规则的意思是:集群整体宕机时,抑制单节点的内存告警。
 

7.1 四步法
 

  1. 业务预估:DAU、人均请求数、活动形态、峰值系数
     
  2. 链路拆解:CDN → 接入层 → 应用层 → 缓存 → 数据库 → 下游依赖
     
  3. 瓶颈识别:单机压测找上限,反推节点数
     
  4. 冗余设计:至少 1.5 倍冗余,再留 30% 缓冲
     

7.2 一份容量清单参考
 

代码清单 18|大促容量规划清单

活动:某区域市场大促
预期:DAU 200 万,峰值 QPS 30000

接入层:12 台(单机 3000 QPS
应用层:24 台(单机 1500 QPS,线程 200
缓存:  6 节点 16G(业务与会话分离)
数据库:读写分离,1 4
消息队列:12 节点,单主题 24 分区
CDN   按区域回源,预留带宽峰值

 

这份清单的重点不是数字本身,而是每一层都要算。很多事故不是因为某一层算错了,而是某一层根本没算。
 

7.3 压测的几个要点
 

  • 必须全链路压测。单接口压测没有意义,真实瓶颈往往出现在链路的交界处。
     
  • 流量要染色。通过请求头区分压测流量,避免污染生产数据。
     
  • 逐步加压。1 倍 → 2 倍 → 3 倍预估峰值,找到系统的极限点在哪。
     
  • 故障注入。压测过程中随机关掉节点,验证容灾和降级是不是真的生效。
     

最后一条尤其重要。很多降级开关配好之后从来没真正演练过,等活动当天才发现它压根没生效。 我们这次 WAF 路径配错,本质上就是缺少验证环节。
 

做稳定性治理这几年,有几个认知是慢慢建立起来的。
 

技术债会在最脆弱的时候爆发。 那个接近 10MB 的大 key,当年被标注为"待优化",两年后以 CPU 打满的灾难告警逼着我们处理。欠下的债总是要还的,只是早还和晚还的利息差别很大。
 

降级开关是双刃剑。 打开它很容易,但打开的那一刻,你也同时关掉了一部分正常业务。降级是借来的时间,不是答案——打开之后要立刻去找根因,而不是松一口气。
 

工具链值得提前投入。 它不像业务功能那样有直接的产出,但每次排障省下的时间,都是在给未来的自己减负。
 

这几年的成长不只是系统的。从最早"完全没有预案,告警了才知道要扩容",到后来能通过热点日志精确定位到哪个商品的哪个 key 结构有问题,并给出分阶段的改造方案——这中间,每一个经历过那些夜晚的人,排障的直觉和对系统的理解都在悄悄变化。
 

那些盯着告警看到天亮的夜晚,没有白费。
 

参数

建议值

说明

缓存连接池 maxTotal

100~200

单实例

缓存连接池 maxWaitMillis

1000~3000

批量操作取上限

Tomcat 线程数

200

单机参考值

游标遍历并发

不超过 5

单实例

大 key 阈值

String 超 1MB、哈希超 5000 字段

需拆分

会话集群连接池

30~50

独立部署后

本地热点缓存过期

5~10s

热点数据

账号限流阈值

5~10 次/分钟

下单类接口

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐