技术实践复盘从告警驱动到预案驱动:一个跨境电商平台的稳定性治理实践
目录
1.3 三十六台机器撑住了流量,但转化率只有百分之一点几... 9
2.4 负载不高却超时:批量操作与连接池的叠加效应... 16
凌晨三四点盯着监控大屏的那种感觉——曲线在爬,告警在响,你不知道下一秒会不会全线崩掉。
这篇文章记录的是我们跨境电商平台上几年来的稳定性治理过程。回头看,早期我们基本是"告警驱动"的:出问题了才去扩容、去救火;后来才慢慢转向"预案驱动":活动前把容量、限流、降级、风控全部算清楚,演练一遍。
这个转变背后,是十几次线上告警、几十次技术决策的积累。我把它们按问题类型整理成了几条线,每一条都跨越了不止一年。之所以会反复,根本原因都一样:第一次只做了止血,没有根治。
下面这些内容,有些是架构层面的判断,有些是很具体的参数配置,都来自真实的生产环境。希望对做高并发系统的同学有些参考价值。
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 确实降了一些。但代价是预约这类业务也一起被屏蔽了,体验折损很明显。这只是临时方案。
第二天同样的问题又出现,我们开始认真定位:
- 用流量分析平台看哪个页面访问量异常
- 用调用链追踪定位到具体是哪些商品(关联了预约活动的新品)
- 拉缓存热点日志确认,热点集中在商品促销这个 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 四步法
- 业务预估:DAU、人均请求数、活动形态、峰值系数
- 链路拆解:CDN → 接入层 → 应用层 → 缓存 → 数据库 → 下游依赖
- 瓶颈识别:单机压测找上限,反推节点数
- 冗余设计:至少 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 次/分钟 | 下单类接口 |

更多推荐




所有评论(0)