传统混沌工程常隐含三个前提:实验可以随时干净停止、爆炸半径能够提前算清、生产环境适合直接验证。金融支付系统往往同时打破这三个前提。一次看似简单的 ECS 任务终止,可能经过 DNS 缓存、客户端重试、数据库连接池和跨可用区调度,演变成持续时间更长、范围更大的连锁反应。
企业 ECS 部署中的几个数字很能说明问题:60 秒 DNS TTL 最终对应了 93 秒故障转移;重试逻辑让数据库负载放大到 2.4 倍;可用区重新平衡还可能形成通用混沌工具观察不到的循环。对支付平台来说,实验设计的核心因此不是“能不能杀掉一个容器”,而是“能不能证明资金链路在故障扩散时仍然保持正确”。
DNS TTL 不是故障恢复倒计时
将服务发现记录的 TTL 设置为 60 秒,并不意味着所有调用方都会在第 60 秒切换到健康实例。一次完整的故障转移还可能受到以下环节影响:
- DNS 解析器、运行时或代理各自维护缓存;
- 已建立的 HTTP、gRPC 或数据库连接不会因 DNS 记录变化立即关闭;
- 健康检查需要先确认故障,再更新注册记录;
- 客户端可能仍在对旧地址执行退避重试;
- 新 ECS 任务还要经过启动、注册和预热。
因此,93 秒故障转移并不是“60 秒 TTL 配错了”这么简单。更合理的测量方式,是分别记录故障发生、健康检查失败、DNS 记录更新、客户端首次解析新地址以及业务成功率恢复的时间点。
支付接口尤其需要观察业务层恢复,而不能只看 ECS 的 runningCount。容器处于 RUNNING 状态,不代表它已经加载密钥、建立数据库连接并能够安全处理扣款请求。
重试可能把局部故障放大成数据库事故
当上游请求超时,最直觉的补救是重试。但如果多个 ECS 任务同时失败,所有调用方采用相同间隔重试,就会形成同步重试风暴。来源案例中的数据库负载达到正常水平的 2.4 倍,说明实验影响已经越过被注入故障的服务,进入共享数据层。
支付链路中的重试至少应具备四个约束:
- 写操作必须携带幂等键。 网络超时无法证明交易没有成功,不能把第二次扣款当成新交易。
- 限制最大尝试次数和总时间预算。 不要让三层服务各重试三次,最终把一次请求放大成数十次内部调用。
- 使用指数退避和随机抖动。 避免所有客户端在同一毫秒重新冲击数据库。
- 设置重试预算。 当系统整体错误率升高时,应减少而不是增加重试流量。
实验看板不能只放 HTTP 错误率,还应同时显示数据库 QPS、活跃连接、锁等待、重试次数和幂等冲突数。否则,前端成功率短暂恢复可能掩盖后端正在被重试耗尽。
ECS 的跨可用区反馈循环
通用故障注入工具通常能看到“某个任务被终止”,却不一定理解 ECS 调度器、负载均衡器和可用区容量之间的反馈关系。如果任务被持续放到容量不足或健康检查不稳定的可用区,服务可能不断经历启动、摘除、替换和再次平衡。
这类循环需要从控制面观察,而不是只盯应用指标:
- 每个可用区的运行任务数;
- ECS deployment 数量和 rollout 状态;
- 任务启动、停止及停止原因;
- 负载均衡目标注册与摘除频率;
- 容量提供者的可用资源和扩缩容活动;
- 同一服务在可用区之间的迁移次数。
实验终止条件也不能只是“注入时间到”。如果控制面已经进入重新平衡循环,停止故障注入不会自动恢复稳定状态。真正的结束条件应是任务分布重新稳定、目标组健康、积压清空,并且支付成功率和重复交易指标回到基线。
可以这样实践:带硬保护的单任务实验
下面是一个可改造的 Bash 示例。它默认只允许在 staging 环境运行,要求服务至少有两个任务、当前没有滚动部署、关键 CloudWatch 告警均为 OK。除非显式设置 RUN_CHAOS=true,脚本只会执行 dry run。
运行前需要安装并配置 AWS CLI 和 jq。请将集群、服务、区域及告警名称替换为自己的非生产资源:
#!/usr/bin/env bash
set -euo pipefail
: "${CLUSTER:?Set CLUSTER}"
: "${SERVICE:?Set SERVICE}"
: "${ALARM_NAMES:?Set comma-separated ALARM_NAMES}"
AWS_REGION="${AWS_REGION:-us-east-1}"
ENVIRONMENT="${ENVIRONMENT:-unset}"
RUN_CHAOS="${RUN_CHAOS:-false}"
if [[ "$ENVIRONMENT" != "staging" ]]; then
echo "Refusing to run outside staging" >&2
exit 1
fi
service_json=$(aws ecs describe-services \
--region "$AWS_REGION" \
--cluster "$CLUSTER" \
--services "$SERVICE" \
--output json)
desired=$(jq -r '.services[0].desiredCount' <<<"$service_json")
running=$(jq -r '.services[0].runningCount' <<<"$service_json")
deployments=$(jq -r '.services[0].deployments | length' <<<"$service_json")
rollout=$(jq -r '.services[0].deployments[0].rolloutState // "UNKNOWN"' <<<"$service_json")
if (( desired < 2 )) || (( running != desired )); then
echo "Service lacks safe headroom: desired=$desired running=$running" >&2
exit 1
fi
if (( deployments != 1 )) || [[ "$rollout" != "COMPLETED" ]]; then
echo "Service is not in a stable deployment state" >&2
exit 1
fi
IFS=',' read -ra alarms <<<"$ALARM_NAMES"
for alarm in "${alarms[@]}"; do
state=$(aws cloudwatch describe-alarms \
--region "$AWS_REGION" \
--alarm-names "$alarm" \
--query 'MetricAlarms[0].StateValue' \
--output text)
if [[ "$state" != "OK" ]]; then
echo "Alarm $alarm is $state; experiment blocked" >&2
exit 1
fi
done
task=$(aws ecs list-tasks \
--region "$AWS_REGION" \
--cluster "$CLUSTER" \
--service-name "$SERVICE" \
--desired-status RUNNING \
--query 'taskArns[0]' \
--output text)
if [[ -z "$task" || "$task" == "None" ]]; then
echo "No running task found" >&2
exit 1
fi
echo "Selected task: $task"
if [[ "$RUN_CHAOS" != "true" ]]; then
echo "Dry run only. Set RUN_CHAOS=true to stop this staging task."
exit 0
fi
aws ecs stop-task \
--region "$AWS_REGION" \
--cluster "$CLUSTER" \
--task "$task" \
--reason 'bounded staging chaos experiment' \
>/dev/null
echo "Task stopped. Watch business SLOs, retry load, DNS timing, and AZ distribution."
例如先执行 dry run:
CLUSTER=payments-staging \
SERVICE=payment-api \
AWS_REGION=ap-southeast-1 \
ENVIRONMENT=staging \
ALARM_NAMES=PaymentErrorRate,DatabaseConnections \
./ecs-chaos.sh
确认选中的任务和所有保护条件后,才添加 RUN_CHAOS=true。这个脚本只是最小示例:在真实平台中,还应把“停止测试流量”做成独立且经过验证的紧急开关,并通过自动化系统持续检查告警,而不是依赖工程师手动盯屏。
上生产前,先回答这些问题
支付系统采用混沌工程时,可以用下面的清单审查实验:
- 故障是否先在压测、回放或隔离的 staging 环境验证?
- 爆炸半径是否按任务数、交易量、商户范围和可用区同时限制?
- 写请求是否具备经过验证的幂等语义?
- 是否测量实际 DNS 切换时间,而不是直接采用 TTL 数值?
- 是否为重试设置总预算,并监控数据库负载放大倍数?
- 停止注入后,如何确认 ECS 调度和可用区分布真正收敛?
- 告警触发时,能否立即停止测试流量,而不仅是停止故障动作?
- 对账、重复扣款检测和人工处置流程是否参与演练?
成熟的支付混沌工程不是更大胆地破坏生产,而是更精确地定义边界、观测传播路径并验证资金正确性。ECS 任务恢复只是开始;只有 DNS、重试、数据库、可用区调度和交易账务全部回到可证明的稳定状态,实验才算真正结束。