去年参加了 AWS re:Invent 2016, 最喜欢也最需要的新产品莫过于 AWS Batch. 回来几个月了发现中国区依旧没有上线, 然后就手痒痒"山寨"了一个, 跑着还很稳定. 记录一下"山寨"过程中的思考.
0x00 AWS Batch 拆解
什么是 AWS Batch? 官方介绍:
AWS BatchAWS Batch 让开发人员、科学家和工程师能够轻松高效地在 AWS 上运行成千上万项批处理计算任务。AWS Batch 可根据提交的批处理任务的数量和特定资源要求,动态预置计算资源 (CPU 或 内存优化型实例) 的最佳数量和类型。借助 AWS Batch,您无需安装和管理运行您的任务所使用的批处理计算软件或服务器群集,从而使您能够专注于分析结果和解决问题。AWS Batch 可以跨多种 AWS 计算服务和功能 (如 Amazon EC2 和竞价型实例) 计划、安排和执行您的批处理计算工作负载。
简单来说就是一个 Producer-Consumer 的异步任务执行系统. 拆解下来有如下几个部分组成:
- 任务队列, 用于存储异步消息. AWS 上有成熟的 SQS, 开源也有很多实现, 比如 Celery
- 任务执行程序. 任务执行需要对应的 binary, 为了方便部署, AWS Batch 选择的是 Docker image, 直接指定 docker image 的 name 便可以从 registry pull 下来.
- 任务调度 engine. 为了提高资源利用率, AWS Batch 支持指定执行程序所需 cpu memory 等, engine 根据需求充分调度, 提高系统利用率
- Producer 端任务定义. 也就是后续任务执行程序的输入. 如果做一个通用的任务执行系统的话, Docker image name 也可以作为任务定义, 传入消费端.
- 权限等其他配置. AWS Batch 我猜是基于 AWS ECS 做的, 因此支持指定 Docker image 使用与宿主主机不同的 IAM Role, 更细化控制权限(有关 IAM, 可以参见之前文章 AWS IAM 入门)
拆解完成后, 来看看我们的场景
0x01 业务场景
有一个业务, 每天凌晨需要执行多个批处理任务(任务耗时从几分钟到几个小时不等), 白天基本上没有任务执行. 遇到的问题就是: 想让任务尽快执行完成, 又想节省成本.
之前的办法是, 把任务挤在一个大 instance 上, 调整好并发(最多并发四个执行), 白天空闲.
那么, 如何改造呢?
0x02 可选方案
第一个想法: 上 Yarn 等资源调度
公司现有的资源调度框架, Yarn 是一个选择, 但想想要写 Yarn application 我瞬间就放弃了, 不值当.
第二个想法: autoscaling + ec2
当前在 AWS 中国区, 最简单的方式就是直接使用 ec2 作为独立执行单元调度了(没有 ECS), 既然是耗时耗资源(不是简单的函数执行, 需要几个 GB 内存的 batch 任务), 直接使用 ec2 对应的 instance type, 也不算浪费. 那么参照 AWS Batch 的拆解, 对应的模块如何实现?
- 任务队列: 可以使用 SQS, 但为了简化系统, master 节点已经有了 MySQL, 直接使用 MySQL 作为任务队列更简单
- 任务执行程序: 既然有 master 节点, ec2 worker 节点启动时, 直接从 master 拉取最新的执行程序即可.
- 任务调度 engine: worker 节点启动一个 agent, 使用 polling 的方式从 master 节点获取任务即可.由于任务之间相互独立, 调度的 engine 就简单的下发任务即可
- Producer 端任务定义: 系统仅仅执行一种任务, 下发的任务就是简单的配置, 作为参数输入到任务执行程序.
- 权限等其他配置: 整个集群使用同一个 IAM role 和相同的 security group
系统就变成了这个样子:
系统结构
0x03 如何"无损" scale in?
由于任务是定时开始的(凌晨), 扩容使用 scheduled action; 但如何在没有任务执行的时候关闭 ec2 节点而不影响正在执行的任务?
一种方式是根据 SQS 队列中的任务数量, 可以参见 AWS 官方文档: Scaling Based on Amazon SQS. 但是我的任务是比较大的批处理任务, 队列中没有任务, 很有可能任务正在执行中, 如果直接关闭 ec2 instance 进行 scale in 的话, 会导致我的任务执行到一半就失败.
那么有没有其他办法呢? 翻了翻 AWS autoscaling 文档, 发现 AWS 的新功能: Instance Protection for Auto Scaling, 试了一下, 发现 autoscaling 会一直重试 scale-in 的操作, 直到你的 ec2 instance 把 scale-in 保护关闭. 正合我意!
0x04 最终实现
事情想清楚了, 行动就显得简单了.
首先, 给 worker 节点的 IAM 要加上对应的 autoscaling 权限
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:SetInstanceProtection"
],
"Resource": "*"
}
]
}
其次, worker 代码中的关键在于:
- 每次从 master 获取任务前, 必须先打开 scale-in 保护
- 强烈建议累计 N 次获取不到任务时, 才关闭 scale-in 保护, 避免任务队列中依然有任务, 但 worker 节点在上一个任务执行完毕到获取到下一个任务的间隙, 被 autoscaling 关闭
# 记录连续没有获取到 task 的次数
task_poll_miss_count = 0
while True:
# 打开 scale-in 保护
protect_from_scalein()
try:
# 从 master 节点获取任务
task = get_task_from_master()
if task is not None:
# 如果任务不为空, 则执行任务
task_poll_miss_count = 0
execute_task(task)
else:
task_poll_miss_count += 1
finally:
if task_poll_miss_count >= 10:
# 连续 10 次没有获取到任务, 应该是系统空闲, 直接关闭 scale-in 保护, 留足够时间给 autoscaling 关闭该节点
LOG.info("task poll miss count > 10, unlock scale-in protection")
no_protect_from_scalein()
task_poll_miss_count = 0
time.sleep(random.randint(50, 100))
由于任务执行的周期性, 直接使用 autoscaling 的 schedued action 提前在任务执行前启动 worker, 预估一个执行时间后直接将整个 autoscaling group 的节点数量设置成 0.
scale in 效果- 从下向上看, 前三个都是成功被系统 scale in, 关闭节点
- 后续节点由于有任务执行, scale-in 会失败, 但系统会持续重试, 直到 scale-in 保护开关关闭为止.
0x05 总结
使用 autoscaling + scale-in 保护, 轻松实现山寨了一个 "AWS Batch". 虽然简陋, 但很使用, 优点就是大大提高了批处理任务并行度的同时, 还降低了成本. 但缺点也很明显:
- 需要自己处理任务执行失败后, 失败的任务需要重新投递问题
- 需要固定的执行时间, 以便定时扩容
- 需要在 worker 节点初始化的时候处理执行程序分发问题, 或者获取到任务后根据任务中的参数在执行逻辑中添加 docker pull 逻辑
- 资源利用率以单个 instance 为单位, 不支持一个 worker 并发执行多个任务
总之, 作为一个"山寨的 AWS Batch", 够用就好. 如果 AWS Batch 中国区再不发布, 我就要考虑一下, 把这个山寨货更完善一下, 毕竟批处理任务执行系统是刚需.
-- EOF --
网友评论