利用混沌工程在 Prime Video 构建弹性服务
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
大型分布式软件系统由多个独立的子系统(例如内容分发网络、负载均衡器和数据库)及其交互组成。这些交互有时会因不可预见的突发事件(例如网络故障)而产生不可预测的结果。这些事件可能导致系统范围内的故障。
混沌工程是一门通过对分布式系统进行实验来增强系统应对突发事件能力的学科。混沌工程要求采用一些方法,主动识别分布式系统中的交互作用及相关故障,并实施和验证相应的应对措施。混沌工程的关键在于以可控的方式引入故障。
本文介绍了一种在基于Amazon Elastic Compute Cloud (Amazon EC2) 和Amazon Elastic Container Service (Amazon ECS) 的系统中进行故障注入的简单方法,并将其与负载测试套件集成,以验证为防止依赖项和资源耗尽故障而采取的应对措施。一个典型的混沌实验包括:对系统生成基线负载(流量),增加所有对底层数据库的网络调用的延迟,然后验证超时和重试情况。我们将解释如何注入此类故障(增加数据库调用的延迟),为什么在负载下验证应对措施(超时和重试)至关重要,以及如何在基于 Amazon EC2 的系统中执行此实验。
我们将首先简要介绍混沌工程,然后深入探讨如何使用AWS Systems Manager进行故障注入。接下来,我们将介绍我们的开源库AWSSSMChaosRunner。该库的灵感来源于 Adrian Hornsby 的博文“使用 AWS Systems Manager 向 Amazon EC2 注入混沌” 。
最后,我们将提供一个集成示例,并解释Prime Video如何使用该库来防止可能影响客户的服务中断。
混沌工程简介
软件测试通常包括实现和自动化单元测试、集成测试和端到端测试。虽然这些测试至关重要,但它们并不能涵盖分布式系统中可能出现的各种中断(例如,可用区中断、依赖项故障、网络中断等)。
通常情况下,软件系统在这些场景下的行为仍然未知。例如,如果服务集群中的某个 Amazon EC2 实例持续占用大量 CPU 资源会发生什么情况?这种情况可能是由于流量意外激增或代码中循环实现错误造成的。如果不进行压力测试,就很难建立对软件系统的信心。需要考虑的问题:
- 您是否测试过当底层实例持续出现 CPU 使用率峰值时系统的行为?
- 在不同压力下,系统行为是否能够被理解?
- 监控是否到位?
- 警报是否已验证?
- 是否已实施任何应对措施?例如,是否已设置自动扩缩容,其运行是否符合预期?超时和重试机制是否合适?
如前所述,混沌工程需要采用一些方法来主动识别分布式系统中的交互作用及相关故障,并且还需要实施和验证相应的应对措施。这些都可以通过混沌工程实验来实现。
典型的混沌工程实验包括:
- 资源耗尽:例如,CPU、虚拟内存、磁盘空间等资源耗尽。这类故障频繁发生,通常是由部署失败、内存泄漏或意外流量高峰引起的。控制资源耗尽的混沌实验验证了系统具备足够的监控能力来检测此类故障,并采取了适当的应对措施(例如,自动扩缩容、自动重启等),使系统能够自动恢复。
- 网络依赖项故障或响应缓慢:例如,通过网络访问的数据库响应缓慢或故障率高。当网络出现间歇性问题或依赖项处于降级状态时,就会发生这些故障。超时、重试策略和熔断器是应对这些故障的典型措施;然而,这些措施很少得到充分测试,因为单元测试或集成测试通常无法高置信度地验证它们。通过在依赖代码路径中注入延迟或故障来进行混沌实验,可以很好地证明这些措施(超时、重试和熔断器)的有效性。
如需更深入地了解混沌工程,请参阅本文末尾的资源。
AWSSSMChaosRunner:使用 AWS Systems Manager 进行故障注入的库
接下来,让我们回顾一下 AWS Systems Manager 的基本概念:AWS Systems Manager 代理(SSM 代理)、SendCommand API 和AWS Systems Manager文档。
AWS 系统管理器
AWS Systems Manager是一项用于查看来自多个 AWS 服务的运行数据并自动执行跨 AWS 资源的运维任务的服务。用户指南中提供了 Systems Manager 功能的完整列表。
对于 Amazon EC2 实例,AWS Systems Manager 提供SSM 代理来执行实例或服务器内部的操作。此功能通常用于大多数 Amazon EC2 实例的操作系统补丁和管理 SSH 会话。
AWS Systems Manager Agent
SSM Agent 是亚马逊开源软件,采用 Apache License 2.0 许可发布,可安装并配置在 Amazon EC2 实例上。SSM Agent 使 Systems Manager 能够更新、管理和配置这些资源。默认情况下,使用以下 Amazon 系统映像 (AMI) 创建的实例预装了 SSM Agent:2016 年 11 月或之后发布的 Windows Server 2008 至 2012 R2 AMI、Windows Server 2016 和 2019、Amazon Linux、Amazon Linux 2、Ubuntu Server 16.04、Ubuntu Server 18.04 以及 Amazon ECS 优化版。
有关安装和配置说明,请参阅用户指南。
SendCommand API
AWS SSM SendCommand API允许通过 SSM 代理以编程方式在一个或多个实例上运行命令。
示例 :使用 AWS CLI 发送“Hello, World!”命令
- 指定的实例 instanceid=i-1234567890abcdef0 将以 shell 脚本的形式运行“echo Hello, World!”。目标可用于指定单个实例或实例组,方法是使用实例标签(例如,自动伸缩组)。
- SendCommand 执行将在 10 秒后超时。
- 该命令产生的任何日志都将发送到名为 test 的CloudWatch 日志组。
aws ssm send-command \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["echo Hello, World!"]' \
--targets "Key=instanceids,Values=i-1234567890abcdef0" \
--comment "echo Hello, World!"
--timeout-seconds 10
--cloud-watch-output-config "CloudWatchOutputEnabled=true,CloudWatchLogGroupName=test"
SSM 指挥文件
AWS Systems Manager 文档(SSM 文档)可用于以 shell 脚本的形式指定要在单个实例或多个实例上执行的复杂命令。您可以通过 AWS Systems Manager 控制台或 SendCommand API 运行 SSM 文档。
示例:用于 黑洞路由的 SSM 文档,该黑洞路由将 指定 UDP 或 TCP 端口上的所有出站流量路由到指定端口。
- 本文档采用 YAML 格式编写,但也可以使用 JSON 格式编写。
- 命令参数作为变量单独定义。
- action: aws:runShellScript 指定步骤 (mainSteps) 是 shell 脚本的一部分。
---
schemaVersion: '2.2'
description: Blackhole a protocol/port on an instance
parameters:
prtl:
type: String
description: Specify the protocol to blackhole. (Required)
allowedValues:
- tcp
- udp
port:
type: String
description: Specify the port to blackhole. (Required)
duration:
type: String
description: The duration - in seconds - of the blackhole. (Required)
default: "60"
mainSteps:
- action: aws:runShellScript
name: ChaosBlackholeAttack
inputs:
runCommand:
- iptables -A OUTPUT -p {{ prtl }} --dport {{ port }} -j DROP
- sleep {{ duration }}
- iptables -D OUTPUT -p {{ prtl }} --dport {{ port }} -j DROP
AWSSSMChaosRunner
假设 SSM Agent 已安装在 Amazon EC2 实例上并配置了正确的权限,则可以使用 AWS Systems Manager 按如下方式对 Amazon EC2 实例进行故障注入:
- 通过 AWS Systems Manager 控制台或 AWS CLI 创建 SSM 文档。
- SSM 文档中包含的 shell 脚本必须在底层实例上可执行。
2. 通过 AWS Systems Manager 控制台或 AWS CLI 调用 SSM SendCommand API。
- 可以通过为目标参数使用适当的标签来定义 Amazon EC2 集群。
- 必须指定底层 shell 脚本的参数(如上例中的 duration/port/protocol)。
- 必须配置并指定 CloudWatch 日志组,才能在单个位置查看来自整个 Amazon EC2 集群的日志。
如果上述步骤成功,所有指定的 Amazon EC2 主机都将注入故障。例如,EC2 主机将屏蔽发往特定 UDP/TCP 端口的出站流量。然而,此时可能没有任何请求到达您要注入故障的服务;这可能是因为正值低流量期,或者服务器处于开发阶段。在这种情况下,故障注入的影响可能微乎其微,甚至根本无法察觉。因此,验证已采取的应对措施将十分困难。我们需要执行第三步。
3. 使用负载生成器生成服务流量,以模拟系统上的真实高流量。
手动执行上述步骤容易出现配置错误,存在风险,而且耗时。这些步骤可以使用最近发布的AWSSSMChaosRunner 库自动完成,如下图所示。
该库抽象了 SSM 文档的创建和 SSM SendCommand 的调用,并提供了经过验证的 SSM 文档供您进行混沌实验。该库以Apache 2.0 许可证开源,可在GitHub和Maven Central上获取。
失败注入
AWSSSMChaosRunner 库中目前可用的故障注入有:
- NetworkInterfaceLatency :为给定网络接口的所有入站/出站调用添加延迟。
- DependencyLatency :增加对给定外部依赖项的入站/出站调用的延迟。
- DependencyPacketLossAttack:丢弃对给定外部依赖项的入站/出站调用中的数据包。
- MemoryHog:占用舰队上的虚拟内存。
- CPUHog:占用舰队中大量的 CPU 资源。
- DiskHog:占用舰队上大量的磁盘空间。
- AWSServiceLatencyAttack :使用从ip-ranges.amazonaws.com返回的 CIDR 范围向 AWS 服务添加延迟。这对于Amazon Simple Storage Service (Amazon S3) 或Amazon DynamoDB等服务是必要的,因为在混沌实验期间,解析的 IP 地址可能会发生变化。
- AWSServicePacketLossAttack :使用从ip-ranges.amazonaws.com返回的 CIDR 范围丢弃发往 AWS 服务的数据包。这对于 Amazon S3 或 Amazon DynamoDB 等服务是必要的,因为在混沌实验期间,解析出的 IP 地址可能会发生变化。
- MultiIPAddressLatencyAttack:给所有调用指定 IP 地址列表的请求添加延迟。这在路由器→主机之类的架构中可能很有用。
- MultiIPAddressPacketLossAttack:丢弃所有发往指定 IP 地址列表的数据包。这在路由器→主机之类的架构中可能很有用。
混沌测试 EC2 服务
例如,考虑在 Amazon EC2 上运行的服务。(为简化起见,省略了 CDN、负载均衡器和 VPC 等常用组件)。
此服务接收客户端请求,应用业务逻辑,并访问数据库(或任何外部依赖项)。让我们学习如何将 AWSSSMChaosRunner 库应用于此服务。
先决条件
- 熟悉IAM 概念,例如 IAM 策略、角色和用户。
- 该服务的测试用例是用 Java、Kotlin 或 Scala 编写的。AWSSSMChaosRunner 库仅适用于这些语言。
- 必须使用指标或日志来监测服务的健康状况和行为。如果没有监测,就无法观察到故障注入的影响。
- 在执行混沌实验的同时,测试会向服务产生一些基准流量(负载)。产生这些流量有助于验证实验假设。
步骤 1. 设置从 tests 包调用 AWS Systems Manager 的权限。
虽然可以用不同的方式实现这一部分,但这里描述的方法会在每次运行测试时为 AWS Systems Manager 生成临时凭证。
首先,您必须创建一个 IAM 用户及其可以承担的 IAM 角色。以下IAM 策略必须附加到此角色。
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"sts:AssumeRole",
"ssm:CancelCommand",
"ssm:CreateDocument",
"ssm:DeleteDocument",
"ssm:DescribeDocument",
"ssm:DescribeInstanceInformation",
"ssm:DescribeDocumentParameters",
"ssm:DescribeInstanceProperties",
"ssm:GetDocument",
"ssm:ListTagsForResource",
"ssm:ListDocuments",
"ssm:ListDocumentVersions",
"ssm:SendCommand"
],
"Resource": [
"\*"
],
"Effect": "Allow"
},
{
"Action": [
"ec2:DescribeInstances",
"iam:PassRole",
"iam:ListRoles"
],
"Resource": [
"\*"
],
"Effect": "Allow"
},
{
"Action": [
"ssm:StopAutomationExecution",
"ssm:StartAutomationExecution",
"ssm:DescribeAutomationExecutions",
"ssm:GetAutomationExecution"
],
"Resource": [
"\*"
],
"Effect": "Allow"
}
]
}
步骤 2. 初始化 AWS Systems Manager 客户端。
这段代码应该在测试初始化期间调用(即,在创建单例的任何位置)。
//Kotlin
@Bean
open fun awsSecurityTokenService(
credentialsProvider: AWSCredentialsProvider,
awsRegion: String
): AWSSecurityTokenService {
return AWSSecurityTokenServiceClientBuilder.standard()
.withCredentials(credentialsProvider)
.withRegion(awsRegion)
.build()
}
@Bean
open fun awsSimpleSystemsManagement(
securityTokenService: AWSSecurityTokenService,
awsAccountId: String,
chaosRunnerRoleName: String
): AWSSimpleSystemsManagement {
val chaosRunnerRoleArn = "arn:aws:iam::$awsAccountId:role/$chaosRunnerRoleName"
val credentialsProvider = STSAssumeRoleSessionCredentialsProvider
.Builder(chaosRunnerRoleArn, "ChaosRunnerSession")
.withStsClient(securityTokenService).build()
return AWSSimpleSystemsManagementClientBuilder.standard()
.withCredentials(credentialsProvider)
.build()
}
步骤 3. 在开始测试之前启动故障注入攻击,并在测试结束后停止故障注入攻击。
给定的测试会向服务发送流量。
//Kotlin
@Before
override fun initialise(args: Array) {
if (shouldExecuteChaosRunner()) {
ssm = applicationContext.getBean(AWSSimpleSystemsManagement::class.java)
ssmAttack = getAttack(ssm, attackConfiguration)
command = ssmAttack.start()
}
}
@Test
fun `given failure injection generate calls to the service`(int: duration) {
// This test should call an endpoint of the service and keep repeating this for the duration of the test.
// Additional logging can be added or service dashboards can be monitored for an overview.
val startTime = LocalDateTime.now()
while(getElapsedSeconds(startTime) <= duration){
serviceClient.callEndpoint()
}
}
@After
override fun destroy() {
ssmAttack.stop(command)
}
步骤 4. 运行测试。
执行该命令以运行上述测试。
注意:AWSSSMChaosRunner 也可用于基于 EC2+ECS 的服务,只需在上述步骤之前进行一次设置即可。 更多详情请参阅GitHub README文件。
Prime Video 使用 AWSSSMChaosRunner 来防止潜在的服务中断。
2020年3月,Prime Video推出了个人资料功能,使用户能够根据个人资料的活动情况访问不同的推荐内容、剧集进度和观看列表。这种全新的用户体验需要使用Amazon EC2设计和实施新的服务。
这些服务是分布式系统的一部分,它们通过网络调用其他亚马逊内部服务。测试此服务使用的超时、重试和熔断器配置至关重要,原因如下:
- 这些代码路径很难通过单元测试、集成测试和端到端测试进行验证。
- 配置问题通常是在断电期间发现的,这时就需要采取超时、重试和断路器等应对措施。
Prime Video 使用 AWSSSMChaosRunner 的DependencyLatency攻击实现了这一混沌工程实验,通过对服务生成负载,模拟依赖项出现高延迟时的流量。
通过观察服务间调用指标,验证了超时、重试和断路器配置。
现在让我们回顾一下其中一项混沌实验的结果,看看它是如何帮助我们主动发现可能影响客户的问题的。
实验:验证 ElastiCache 超时
混沌实验的设置如下:
- 实验假设:Service → ElastiCache 调用的超时时间设置为 40 毫秒。实验期间将通过观察 Service → ElastiCache 的延迟指标来验证这一假设。
- 故障注入:使用 AWSSSMChaosRunner 向 Service → ElastiCache 调用添加 2 秒延迟。
- 生成服务基准负载:每秒向服务发送 1000 个请求。如前所述,在系统负载下运行混沌工程实验至关重要。
实验结果
上图显示,服务→ElastiCache的延迟超过了配置的40毫秒超时时间。因此,ElastiCache超时配置失败。
根据这些结果,我们修复了超时配置中的一个错误。
为了验证我们的修复方案,我们随后重新运行了相同的实验。
图示表明,Service → ElastiCache 的最大延迟被限制在 40 毫秒,即配置的超时值。即使实验中在该调用路径中额外引入了两秒的延迟,情况依然如此。该结果验证了如果 ElastiCache 响应缓慢或网络路径存在问题,服务会很快超时。
运行这项混沌实验后,我们发现了依赖项降级对策(即 ElastiCache 超时)中的一个漏洞。修复该漏洞避免了可能影响客户的故障发生。
结论
测试服务依赖项的超时、重试和熔断器配置至关重要。本文介绍了一种使用 AWS Systems Manager 在Amazon EC2上进行故障注入的开源方法,并演示了 Prime Video 如何将其与负载测试相结合,以实现更高的弹性。本 Prime Video 案例研究展示了混沌工程如何帮助预防那些难以通过传统测试方法定位的潜在影响客户的问题。
资源
- PrinciplesOfChaos.org
- 混沌工程:故意破坏事物的艺术(Medium博客合集)
- GitHub 上有一个很棒的混沌工程阅读资源合集。
原文发表于 https://aws.amazon.com, 日期为 2020 年 8 月 18 日。
文章来源:https://dev.to/aws/building-resilient-services-at-prime-video-with-chaos-engineering-2hka




