
前年做大促保障,我们按惯例做压测,各项指标都好看。有位同事提了一句:压测只测了「量变大」,没测「东西坏了」。于是在大促前两周的一个晚上十点,我们把订单查询服务的一个下游调用延迟从三十毫秒调到两秒,只影响百分之二十的实例。
八分钟后,下单链路大面积超时。原因不是延迟本身,是连接池的等待队列被打满,线程全部堵在那儿,连带把不相关的接口也拖死了。这个问题压测永远测不出来,因为在压测里下游一直是好的。后来我们把延迟调回去,改了三处配置,大促期间再没出现过同类问题。
这就是混沌演练:不是等故障发生,而是主动制造一个可控的故障,看系统怎么反应。听着吓人,做好边界控制之后,风险比不演练小得多。下面讲怎么入门。
演练之前必须能回答一个问题:系统什么样算正常。这个「正常」要能被量化成几个指标,比如接口成功率不低于九成九、订单积压不超过五百单、核心接口响应时间在八百毫秒以内。
指标定不出来,演练就没有终点。故障注入之后,你只能凭感觉判断「好像还行」,这种判断没有价值,出事的时候也不会有告警告诉你。
头一次演练别碰写链路。查询类接口挂了,用户看到报错,影响面可控;下单、支付这类写接口出问题,可能产生脏数据,事后清理的成本远高于演练收益。
我们的顺序是:先内部管理后台,再查询接口,再非核心写接口。每进一级,观察两周,确认监控和预案都跟得上,再往下走。跳级演练的结果通常是发现问题一大堆,但没有能力处置。
管理后台这类系统特别适合起步,因为使用者是内部员工,出问题打个电话就能说明白,不会引发外部投诉。在这个环境里把中止流程、通知机制、记录模板跑熟,再往真实业务上搬。
爆炸半径说的是这次故障影响多少人、多少机器、多长时间。这个范围不能靠口头约定,必须写在配置里,由执行工具强制执行。
我们的默认设置是:灰度环境不限,预发环境百分之二十实例,生产环境不超过百分之五实例且只在业务低峰执行。单次持续时间不超过三分钟,超时自动恢复。这几条写死在模板里,谁要用生产环境都得改模板,改动会被评审看到。
在调用链路上加固定或随机延迟,模拟下游变慢。这是入门首选的故障类型,因为它的影响是渐进的,能观察到系统从正常到劣化的完整过程。
延迟注入要配抖动。固定延迟容易被超时重试机制完美绕过,加了抖动之后,重试的节奏被打乱,连接池压力才会真实暴露出来。
让一定比例的请求直接返回错误码,模拟下游不可用或者逻辑异常。这个故障考验的是重试、熔断、降级三套机制是否真的生效。
要注意错误码的选择。返回五百和返回四〇四,上层处理逻辑完全不同,四〇四一般不会触发重试,五百会。注入之前先确认你想验证的是哪条路径。
直接干掉一部分实例,模拟机器故障或者发布过程中的实例重启。这个故障考验的是负载均衡摘除速度和剩余实例的承载能力。
实例下线是破坏性操作,只能在有副本、有自动伸缩的环境里做。单实例服务做这个演练,等于直接停服,那不叫演练,叫事故。
我们头一次做这个演练,掉了两台实例,负载均衡用了将近九十秒才把流量摘干净,这九十秒里有一批请求打到了已经停掉的机器上,用户看到的是连接被拒。后来把健康检查间隔从三十秒改到五秒,同样的演练,影响时间缩到十秒以内。
把某个依赖的响应时间调到超过调用方的超时阈值,看调用方是快速失败还是死等。这类故障在真实环境里很常见,一个慢查询拖垮整个页面。
依赖超时要和延迟注入区分开:延迟注入是「慢一点」,依赖超时是「慢到必然超时」,前者看劣化曲线,后者看失败处理。
演练不能靠人手敲命令。手敲的问题是不可复现,昨天怎么做的记不清,出了问题也说不清当时的参数。下面这份 JSON 描述的是一次完整的实验,包含目标、稳态校验、故障定义、中止条件和记录方式。
{
"experiment": "order-query-degrade",
"version": 1,
"target": {
"service": "order-query",
"scope": "cluster",
"percent": 20,
"labels": { "env": "staging" }
},
"steady_state": {
"window_seconds": 300,
"checks": [
{ "metric": "api_success_rate", "op": ">=", "value": 0.995 },
{ "metric": "p99_latency_ms", "op": "<=", "value": 800 },
{ "metric": "pending_orders", "op": "<=", "value": 500 }
]
},
"faults": [
{ "type": "latency", "delay_ms": 1200, "jitter_ms": 200, "duration_seconds": 180, "percent": 20 },
{ "type": "error", "code": 503, "rate": 0.1, "duration_seconds": 180, "percent": 20 }
],
"abort": {
"on_steady_state_violation": true,
"consecutive_breaches": 3,
"cooldown_seconds": 60,
"auto_recover": true
},
"notify": {
"channel": "ops-alert",
"before_start_minutes": 10,
"on_abort": true,
"on_finish": true
},
"record": {
"save_trace": true,
"tag": "chaos-order-query-r1"
}
}target 定义打谁、打多少、打哪个环境。steady_state 定义什么算正常,里面的三个指标是自动中止的判断依据。faults 是一个数组,可以一次注入多种故障,也可以分阶段执行。
abort 是整份配置里要紧的一段。连续三次稳态校验不通过就自动中止,中止后冷却一分钟再恢复,避免故障刚撤掉就被流量冲垮。没有这段配置的演练,我不建议在生产环境做。
稳态校验的逻辑是:演练开始前先观察五分钟,确认指标在阈值内;注入过程中每三十秒采一次,连续三次越界就中止。这个「连续」很关键,单次抖动不中止,否则一有毛刺就停,演练跑不完。
阈值怎么定?按历史正常波动定。拿过去两周同一时段的指标分布,取分位数,别直接拍一个整数。定得太紧,天天误中止;定得太松,出事了也不停。
一次实验里多个故障不要同时开。先延迟,观察两分钟,再叠错误注入,这样能看出是哪一种故障触发了劣化。同时开的话,只能知道「坏了」,不知道「为什么坏」。
执行范围按百分之五、百分之二十、百分之五十三档递进。每一档跑完看结果,没问题再放大。这套流程听着慢,但一次跑通之后,后续实验可以直接复用模板。
这套调度我们放在搭贝的定时任务里,到点自动开跑,结束后自动撤掉故障开关。
必须有一个人专门负责中止,这个人不参与排查,只盯着稳态指标和中止按钮。让排查的人同时管中止,出问题时他一定先去看日志,不会记得按按钮。
还要有一个对外沟通的人。演练期间如果客服、业务方来问,由他统一回复,避免一线以为真出故障,触发不必要的升级流程。
盯三类:入口的成功率和延迟、关键业务的积压量、依赖的错误率。入口指标反映用户感受,积压量反映业务影响,依赖错误率用来定位原因。
别盯机器指标。CPU 打到八十不代表有问题,CPU 只有二十也可能因为等锁而全线卡住。机器指标是辅助,业务指标才是判断依据。
演练结束当天出一份记录:注入了什么、系统怎么反应、哪一层先撑不住、预案有没有生效、需要改什么。记录里要有时间线,精确到分钟,事后才能对上日志。
需要改的项要落成工单,写清负责人和期限。没有工单的复盘等于没做,下次演练还会遇到同一个问题。我们头一年做了六次演练,改出来的东西包括连接池上限、超时阈值、熔断窗口,都是压测发现不了的。
问:小团队没有专职运维,能做混沌演练吗?
答:能,从灰度环境开始。不需要复杂工具,写个脚本定时给某个接口加延迟,观察十分钟,也是演练。关键是先把稳态指标定出来,哪怕只有两个指标,也比没有强。等这套流程跑顺了,再考虑引入自动化平台。
问:演练把生产环境搞挂了,责任算谁的?
答:算流程的。如果中止条件写清楚、爆炸半径控制在约定范围内,演练期间的短时劣化属于预期成本,不该追个人责任。反过来,如果没写中止条件、超范围操作,那是流程缺失,要补的是流程,不是罚人。
问:每次都自动中止,是不是阈值定得太严?
答:先看中止时的指标是不是真的越界。如果只是轻微越界,说明阈值贴着正常波动,要放宽;如果确实明显劣化,那是系统真的扛不住,该改系统而不是改阈值。别为了让演练跑完而放松阈值,那是自欺欺人。
问:演练频率多久一次合适?
答:按变更频率定。每季度有大版本发布的,每季度一次;天天发版的,可以每周小范围跑一次。频率太高会消耗团队精力,太低又起不到作用。我们的做法是核心链路每季度一次全流程,日常每周一次单点故障,两种节奏并行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。