在金融支付系统中实施混沌工程:来自企业级 ECS 部署的经验教训
当例行部署导致支付网关瘫痪时
三年前,某大型支付处理商的结算服务在对账高峰期曾中断四小时。根本原因并非硬件故障或 DDoS 攻击,而是在部署过程中进行的一项例行 ECS 任务替换——该操作与对单个 Redis 节点的隐性依赖相结合,导致授权链中出现连锁超时。这次事件使该公司支付了七位数的 SLA 违约金,并耗费了两个月的时间才重新赢回企业客户的信任。
在金融服务领域,对于大多数严肃的混沌工程项目而言,其起点正是这一事件——不是从理论出发,而是通过事后分析揭示出,人们对系统故障模式的理解实际上是多么匮乏。
混沌工程不是什么新鲜事物。Netflix 使其广为人知,亚马逊云科技围绕它构建了故障注入模拟器(FIS),每一份云原生架构指南都会提及它。但那些为无状态 Web 应用程序编写的手册,在应用于运行在 Amazon ECS 上的支付系统时,往往会以发人深省、有时甚至是痛苦的方式失效。本文分享了团队在尝试应用这些方法时所获得的经验,以及从一开始就应采取什么不一样的做法。
为何支付系统打破了标准的混沌模式
标准的混沌实验基于一些假设,而支付系统在设计上却违反了这些假设。
实验可以干净利落地终止
在典型的 Web 服务中,你引入延迟,观察性能下降,然后回滚。而在支付系统中,实验过程中正在处理的交易可能处于以下几种状态之一:已授权但未捕获、已捕获但未结算,或已结算但未对账。终止实验并不会停止这些交易。它们会处于一种模糊状态,既需要人工干预,又可能引发合规异常。
影响范围可以预先定义
大多数混沌测试框架允许你针对一定比例的实例或任务进行测试。支付系统通常存在隐式状态耦合,这使得“10% 的任务”这一表述无法准确地反映出实际的影响范围。即使一个处理批量结算的 ECS 任务仅占服务集群的极小部分,它也可能成为数千笔交易的关键路径。
混沌实验可以在生产环境中自由运行
PCI DSS、SOC 2 以及大多数银行业监管规定都要求,任何会故意降低生产系统性能的操作都必须经过变更管理审批。未经审批流程就运行混沌实验会导致审计问题。许多团队往往在事后才发现这一问题。
这些限制并不意味着混沌工程在金融科技领域是被禁止的。相反,这表明该流程需要以不同的方式构建。
通用工具未能捕捉到的 ECS 特有的故障模式
ECS 引入了一类独特的故障,而那些专为 Kubernetes 或裸机 EC2 设计的混沌分析工具对此覆盖不足。
任务替换行为导致了隐藏的竞争条件
当 ECS 替换任务时(例如在部署过程中、健康检查失败或 Spot 实例中断时),根据你的部署配置,它会在排空旧任务之前启动新任务。“旧任务清空”与“新任务健康”之间的这段时间窗口,正是支付系统容易受到影响的环节。
如果你的授权服务在启动时向服务注册表或负载均衡器进行了注册,而且“最低健康百分比”配置中未考虑预热期,那么流量将到达一个尚未从参数存储加载配置或建立数据库连接池的任务。该任务会处理请求并返回错误,但由于健康检查端点返回 200 状态码,ECS 无法识别该任务已处于降级状态。
控制这一行为的配置位于 ECS 服务和任务定义中:
resource "aws_ecs_service" "payment_auth" {
name = "payment-authorization"
cluster = aws_ecs_cluster.payments.id
task_definition = aws_ecs_task_definition.payment_auth.arn
desired_count = 6
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
health_check_grace_period_seconds = 120
deployment_circuit_breaker {
enable = true
rollback = true
}
}
resource "aws_ecs_task_definition" "payment_auth" {
family = "payment-authorization"
container_definitions = jsonencode([{
name = "payment-auth"
image = var.payment_auth_image
essential = true
stopTimeout = 120
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health/ready || exit 1"]
interval = 10
timeout = 5
retries = 3
startPeriod = 120
}
}])
}复制代码将 deployment_minimum_healthy_percent 设置为 100 可以防止 ECS 在滚动部署期间使任务数量低于目标值。将 health_check_grace_period_seconds 设置为 120 秒,可以让任务在 ECS 将其流量路由至这些任务之前,有足够的时间从参数存储中加载配置并预热数据库连接池。stopTimeout 设置为 120 秒,可以确保在任务清空过程中,正在进行的事务能够完成。这些数值并非默认值;它们是在混沌实验发现默认 30 秒的宽限期对于某项支付授权服务而言不够用后,经过调优得出的——该服务在启动时需要加载加密密钥并建立与多个下游依赖项的连接。
在这里,值得进行的混沌实验并非“终止一个任务”,而是“将启动时的配置加载延迟 15 秒,并观察负载均衡器向该任务发送的内容”。大多数团队直到发生事故暴露这一漏洞后,才进行这项实验。
服务发现的 TTL 超过任务生命周期
ECS 服务发现使用 Route 53 来注册任务。当任务停止运行时,DNS 记录的 TTL 决定了客户端会在多长时间内继续将请求路由到已失效的 IP 地址。在支付系统中,服务之间通过私有 DNS 相互调用,60 秒的 TTL 意味着任务停止后,连接故障将持续 60 秒。你的应用程序能否优雅地处理这种情况,取决于 HTTP 客户端的配置,而非 ECS。
在每秒处理 400 笔交易的系统中,我们将 Route 53 的 TTL 配置为 60 秒,并预期故障转移能在该时间窗口内完成。实际测得的故障转移时间为 93 秒。这一时间差源于两个我们未曾考虑到的缓存层:JVM 的默认 DNS 缓存(其默认 TTL 取决于 JVM 版本和安全管理器配置,通常为 30 秒,但在某些配置下可能无限期保留)以及 VPC 解析器缓存。在每秒 400 次交易 (TPS) 的情况下,这 93 秒的时间窗口导致大约 37000 个请求被发送到已失效的端点。实验结束后,我们将 TTL 缩短至 10 秒,并将 JVM 的 networkaddress.cache.ttl 改为与之匹配的配置:
resource "aws_service_discovery_service" "payment_auth" {
name = "payment-auth"
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.payments.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}复制代码不妨尝试下这个混沌实验:停止一个 ECS 任务,并测量每个依赖服务继续向旧 IP 发送流量的时间长度。将该时间与你配置的 TTL 进行对比。大多数团队会发现,由于存在他们未曾察觉的 DNS 缓存层,实际传播时间比 TTL 更长。
Spot 中断与结算时机的冲突
为了降低成本,许多 ECS 工作负载运行在 Spot 实例上。对于无状态服务而言,两分钟的 Spot 实例中断通知是一个合理的窗口期,足以完成资源释放和重新调度。但对于结算批处理任务,时间计算容不得半点差错。在一个系统中,夜间结算任务在 75 秒内处理了累积的约 5 万笔交易。当模拟的 Spot 实例中断在第 58 秒发生时,导致 1.4 万笔交易状态不明。数据库显示这些交易处于“结算已启动”状态,但下游清算机构却没有收到提交记录。对账系统将这些交易标记为异常,但自动恢复流程默认它们要么是“已完全结算”,要么是“未启动”,而未将“部分提交”纳入考虑范围。处理这 14000 条记录需要人工干预,运维团队为此耗费了六个小时。
这个混沌实验很简单:在执行过程中随机中断运行结算任务的 Spot 实例。后续的问题则更棘手:系统能否检测到部分执行情况?它能否安全地恢复,还是会导致记录重复?在我们的案例中,答案是两者皆非。系统默默失败了,并留下了恢复逻辑无法处理的记录。该实验直接促成了一个架构决策。我们将结算服务从 Spot 实例彻底迁移到了按需计算资源上。当故障模式会导致财务状态不明时,某些工作负载就不值得为了降低成本而进行优化。
构建符合监管要求的混沌工程计划:“审批优先”模式
在受监管的金融环境中,成功实施混沌工程的团队会将实验视为正式的变更请求。这并非官僚主义造成的额外负担,而是一种强制机制——通过遵守临时测试常会跳过的三项纪律来产出更优质的实验。
首先定义稳态
在提交变更请求之前,需记录“正常运行”的状态:授权成功率高于 99.5%,P99 延迟低于 200 毫秒,未解决的交易状态为零。如果没有这一基准,就无法区分真实发现与正常波动。
根据交易风险而非实例数量确定影响范围
根据 ECS 任务在交易生命周期中的角色为其打上标签(即 role=auth-primary、role=auth-secondary 和 role=audit-writer)。首先从 audit-writer 任务开始进行实验,因为该任务出现故障不会导致阻塞。只有在充分理解了比较简单的实验之后,才逐步过渡到 auth-secondary 任务。
要求制定书面回滚条件
每个提案都应回答“在何种可观察的条件下,我们应停止实验并恢复正常运行?”这一问题。对于支付系统而言,这通常是交易失败率的阈值或最久未解决状态的持续时间。在实验进行期间,应该有自动触发机制,而非依赖人工判断。
行之有效的实验推进流程
直接跳到“在生产环境中终止一个 ECS 任务”是一个错误的起点。要能够在不产生不可接受风险的前提下达到学习的目的,其流程包括以下四个阶段。
第一阶段:在模拟生产流量的预发布环境中进行故障注入
在尽可能贴近生产环境的预发布环境中运行混沌实验。在 ECS 环境中,这意味着使用一个独立的 AWS 账户来运行相同的基础设施:相同的 ECS 任务 CPU 和内存分配、相同的 RDS 实例类、相同的 VPC 拓扑(跨可用区,具有相匹配的子网布局),以及相同的服务发现命名空间配置。预发布账户不应该与生产环境共享容量提供方或集群。
在生成流量方面,我们使用 BlazeMeter 模拟了与生产环境相当的负载,每秒事务处理率与生产系统一致。影子环境通过一个包含匿名化生产数据的镜像数据库来处理这些事务。这种方法唯一未能弥补的不足在于不可预测性。生产流量在事务量、时序和负载分布方面存在自然波动,而合成流量无法完全复现这些特征。由于在预发布环境中可以控制事务数量,所以其可预测性更高,但即使在预发布环境中通过测试的实验,在生产环境不规则的负载模式下仍然可能暴露出问题。认识到这一差距是晋升至第二阶段的必备条件之一。
以下是我们运行任何预发布实验前使用的对等性检查清单:
ECS 任务定义使用的 CPU、内存和容器镜像版本与生产环境一致
RDS 实例类、存储类型和参数组与生产环境一致
VPC 在相同数量的可用区中拥有相同数量的子网
服务发现 TTL、健康检查间隔和部署配置完全一致
IAM 角色和安全组规则遵循相同的结构(不同账户,相同策略)
应用程序配置(即连接池大小、超时值、重试策略)从相同的参数存储层次结构中加载
大多数团队会跳过这一阶段,因为他们的预发布环境无法高度复刻生产环境。这种差距本身就是一项值得在运行任何混沌实验之前解决的问题。
第二阶段:生产环境中的非交易路径服务
你的支付系统中包含一些对运营至关重要但不在直接交易路径上的服务,包括仪表盘、报表导出、审计日志写入器和通知发送器。这些都是早期生产实验的安全目标。
在此处进行实验有助于培养开展实验所需的“肌肉记忆”,例如编写稳态定义、提交变更请求、运行实验、衡量结果以及撰写事后分析报告。与技术执行相比,组织流程更难做到位。
第三阶段:低流量时段的备用事务路径服务
授权备用服务、备用路由逻辑和备用数据库副本——这些服务参与事务处理,但拥有备用路径。此处的实验在每周流量最低的时段进行(对于大多数支付处理商而言,通常为工作日的凌晨 3 点至 5 点)。
我们重复了第一阶段的任务终止实验,在凌晨 3 点的时段内停止了一个授权备用 ECS 任务;故障转移行为与预演一致,授权成功率保持在 99.5% 以上。这次运行验证了我们的影子环境已经足够准确地再现了这一故障模式,足以让我们对进入下一阶段充满信心。
关键衡量指标在于备用路径是否能正确激活,以及交易成功率是否保持在稳态阈值范围内。
第四阶段:具备完全自动化回滚能力的核心服务
只有在前三个阶段产生了稳定、可重复的结果后,核心授权和捕获服务才会被纳入实施范围。在这个阶段,你应该已经具备自动化的回滚触发机制,将值班升级流程集成到实验运行器中,并拥有第一至第三阶段的文档记录,证明系统能够平滑降级。
大多数团队需要六到十二个月的时间才能安全地达到这一阶段。
实验究竟揭示了什么
完成上述流程的团队得出了一致的结论。
超时配置几乎总是错误的
通常,支付服务从内部库或框架的默认设置中继承 HTTP 客户端配置。这些默认设置并非针对支付处理器的 P99 延迟特征而设计。最常见的一个发现是,内部服务调用的超时时间短于下游服务的 P99 延迟,这会在正常负载下导致不必要的故障,而在性能下降的情况下则会演变为大范围的故障。
重试逻辑加剧问题多于缓解问题
当下游服务响应缓慢时,在超时后进行重试的支付服务会引发重试风暴。有一项混沌实验针对一个处理 400 TPS 的系统,向数据库引入了 500 毫秒的延迟,采用“三次重试、指数退避和抖动”的重试策略后,数据库连接的持续使用量较基线水平增加了约 2.4 倍。虽然退避和抖动机制避免了简单重试策略会引发的剧烈峰值,但放大效应依然显著。
在这个案例中,HikariCP 连接池已经根据 RDS 实例类和 ECS 任务数量进行了适当调优。因此,该连接池在未达到饱和状态的情况下吸收了负载。而那些未根据基础设施规模合理配置连接池的团队,在观察到重试放大效应之前,就会先遇到连接池耗尽的情况,这反而掩盖了更深层次的问题。
断路器已配置但未经过测试
许多团队的服务网格或应用程序代码中都配置了断路器,但他们从未见过这些断路器被触发。当混沌实验首次触发断路器时,其行为往往出人意料:断路器触发,流量切换到一条未经过维护的备用路由,而该备用路由本身也存在缺乏弹性处理能力的依赖项。
健康检查会“说谎”
ECS 健康检查仅向调度器报告任务是否正在运行,却无法告知任务是否正确地处理了事务。那些引入细微性能退化的混沌实验(如数据库查询变慢、配置加载不完全、特定事务类型的错误率升高),往往在通过健康检查的同时产生了不可接受的结果。
让我们感到意外的是
上述实验得出了预期的各类发现,包括超时配置错误、故障转移路径测试不足以及健康检查不完整。但其中一项实验却出现了一种团队中无人预料到的故障模式。
在模拟可用区故障的过程中,我们原本预计 ECS 会将任务重新分配到健康的可用区并继续处理。然而,ECS 任务却一直试图在受影响的可用区中启动,从而陷入了启动-停止循环。任务会启动,但由于可用区资源质量下降而无法达到健康状态,并在终止后立即被重新调度回同一可用区。由于 ECS 不断地将任务分配到无法存活的区域,所以集群的目标任务数量始终无法满足。
根本原因在于我们的 ECS 服务没有采用支持可用区(AZ)的部署策略。任务在可用区之间随机分布,而且未进行负载均衡。因此,当某个可用区性能下降时,ECS 仍然会继续向该可用区调度替换任务,而没有将资源重新分配到状态正常的可用区。在 ECS 不断尝试在无法维持任务运行的可用区中启动任务时,该服务实际上已经损失了三分之一的运行能力。
解决方案包括两方面。首先,我们启用了 ECS 的 availability_zone_rebalancing 功能。该功能会在检测到不平衡时,促使 ECS 主动将任务重新分配到健康的可用区:
resource "aws_ecs_service" "payment_auth" {
# ... existing configuration ...
availability_zone_rebalancing = "ENABLED"
}复制代码其次,我们配置了部署约束和容量提供策略,以确保当第三个可用区不可用时,服务仍然能在另外两个可用区中满负荷运行。如果仅通过审查代码或架构图,我们根本无法发现这一漏洞。只有在实际模拟故障时,该问题才显现出来。
实践起点:本季度可以开展的三项实验
如果你的团队是从零开始,请在进行其他任何实验之前先进行这三项实验。
实验一:负载下的 ECS 任务替换
在常规部署窗口期间,测量滚动式 ECS 部署之前、期间和之后授权的成功率和延迟。你可能会发现,在任务替换过程中成功率出现下降,而当前的监控系统并未对此发出警报。在进行任何故障注入之前,请先修复该警报机制。
实验二:数据库连接池耗尽
引入延迟,导致支付服务的数据库连接池用尽。测量该服务的响应情况。它会将请求放入队列、明确报错拒绝请求,还是静默超时?大多数团队发现其行为是“静默超时”,这会导致事务状态模糊不清。如果你观察到这种情况,请修改响应路径,使其返回明确的失败信息并指定事务状态,然后再重新测试连接池大小或重试策略。
实验三:服务发现故障转移
停止一个 ECS 任务(不进行数据排空),并测量其下游调用者继续向已失效的 IP 发送请求的时间长度。运行前请设定成功标准:所有调用者应在 10 秒内完成故障转移。如果测得的故障转移时间超过该阈值,请减少 Route 53 的 TTL,调整 JVM 的 networkaddress.cache.ttl 参数,并重新测试客户端的重试行为,直到实验通过为止。
何时不应将混沌工程作为投资
并非每个团队和每个系统都能从混沌工程中获益。过早开始可能会浪费精力或带来风险,却无法获得有价值的发现。
缺乏可观测性
混沌实验通过测量来创造价值。如果你的系统缺乏分布式追踪、结构化日志以及能够近乎实时显示交易成功率、延迟百分位数和错误细分情况的仪表盘,那么即使进行了实验,你也无法了解究竟发生了什么。请先投资于可观测性。看不见的问题无法诊断。
缺乏事件响应流程
混沌实验偶尔会产生意料之外的结果。如果你的团队没有值班轮换制度、运行手册以及经过演练的升级流程,那么一旦实验出错,就会演变成一场没有响应计划的事件。组织层面的先决条件比技术层面的更为重要。
系统的变化速度快于实验速度
处于产品开发初期、每周发布重大功能,或在不同架构之间进行迁移的团队会发现,在还没来得及采取行动之前,混沌实验的结果就已经过时了。混沌工程在架构稳定且渐进式演化的系统中才能产生最大的价值。如果支付服务的核心交易流程本季度正在进行重写,请等到重写工作稳定后再进行混沌实验。
基础工作尚未做好
负载测试、集成测试和部署自动化是混沌工程的先决条件,而非替代方案。如果一个团队通过混沌实验发现其部署流程缺乏回滚机制,那就意味着他们跳过了一个本无需通过故障注入就能发现的问题。
合规成本超过了学习价值
对于在 PCI DSS 或 SOC 2 框架下运营的小型团队而言,开展合规性混沌实验所需要的变更管理(审批流程、文档编制、审计追踪)所消耗的工程时间,可能超过实验本身所产生的发现价值。如果你的系统处理的交易量比较低,而且架构简单、依赖关系较少,那么在权衡成本与收益时,混沌工程可能不如全面的集成测试和实战演练更具优势。
问题不在于混沌工程在抽象层面上是否有价值,而在于团队当前的成熟度、可观测性状况和系统稳定性,是否使其成为当下提升可靠性的方法中效益最高的投资。
小结
这三项入门实验(即负载下的任务替换、连接池耗尽以及服务发现故障转移)都不需要定制化的工具,都只需要提交一份变更请求,而不需要组织层面的额外支持。请在下次部署窗口期间运行第一项实验。如果在任务替换过程中授权成功率出现下降,而监控系统又未触发警报,那么在任何正式的混沌测试计划启动之前,你就已经获得了首个发现并完成了首次修复。
在这个基础上,构建包含稳态定义、影响范围限定以及书面回滚条件的合规性封装。提交变更请求。从审计写入任务开始,逐步向交易路径内部推进。大多数团队能在一个月内达到第 2 阶段,一年内达到第 4 阶段。
AWS 故障注入模拟器(AWS Fault Injection Simulator)和混沌工程社区 Slack(即 chaos.community)都是不错的起点,不需要从零开始构建工具链。
原文链接:https://www.infoq.com/articles/chaos-engineering-ecs-payments/