去年參加了 AWS re:Invent 2016, 最喜歡也最需要的新產(chǎn)品莫過于 AWS Batch. 回來幾個月了發(fā)現(xiàn)中國區(qū)依舊沒有上線, 然后就手癢癢"山寨"了一個, 跑著還很穩(wěn)定. 記錄一下"山寨"過程中的思考.
0x00 AWS Batch 拆解
什么是 AWS Batch? 官方介紹:
AWS Batch 讓開發(fā)人員、科學(xué)家和工程師能夠輕松高效地在 AWS 上運行成千上萬項批處理計算任務(wù)佳遂。AWS Batch 可根據(jù)提交的批處理任務(wù)的數(shù)量和特定資源要求奔浅,動態(tài)預(yù)置計算資源 (CPU 或 內(nèi)存優(yōu)化型實例) 的最佳數(shù)量和類型崎逃。借助 AWS Batch,您無需安裝和管理運行您的任務(wù)所使用的批處理計算軟件或服務(wù)器群集邪驮,從而使您能夠?qū)W⒂诜治鼋Y(jié)果和解決問題妥箕。AWS Batch 可以跨多種 AWS 計算服務(wù)和功能 (如 Amazon EC2 和競價型實例) 計劃、安排和執(zhí)行您的批處理計算工作負載剩岳。
簡單來說就是一個 Producer-Consumer 的異步任務(wù)執(zhí)行系統(tǒng). 拆解下來有如下幾個部分組成:
- 任務(wù)隊列, 用于存儲異步消息. AWS 上有成熟的 SQS, 開源也有很多實現(xiàn), 比如 Celery
- 任務(wù)執(zhí)行程序. 任務(wù)執(zhí)行需要對應(yīng)的 binary, 為了方便部署, AWS Batch 選擇的是 Docker image, 直接指定 docker image 的 name 便可以從 registry pull 下來.
- 任務(wù)調(diào)度 engine. 為了提高資源利用率, AWS Batch 支持指定執(zhí)行程序所需 cpu memory 等, engine 根據(jù)需求充分調(diào)度, 提高系統(tǒng)利用率
- Producer 端任務(wù)定義. 也就是后續(xù)任務(wù)執(zhí)行程序的輸入. 如果做一個通用的任務(wù)執(zhí)行系統(tǒng)的話, Docker image name 也可以作為任務(wù)定義, 傳入消費端.
- 權(quán)限等其他配置. AWS Batch 我猜是基于 AWS ECS 做的, 因此支持指定 Docker image 使用與宿主主機不同的 IAM Role, 更細化控制權(quán)限(有關(guān) IAM, 可以參見之前文章 AWS IAM 入門)
拆解完成后, 來看看我們的場景
0x01 業(yè)務(wù)場景
有一個業(yè)務(wù), 每天凌晨需要執(zhí)行多個批處理任務(wù)(任務(wù)耗時從幾分鐘到幾個小時不等), 白天基本上沒有任務(wù)執(zhí)行. 遇到的問題就是: 想讓任務(wù)盡快執(zhí)行完成, 又想節(jié)省成本.
之前的辦法是, 把任務(wù)擠在一個大 instance 上, 調(diào)整好并發(fā)(最多并發(fā)四個執(zhí)行), 白天空閑.
那么, 如何改造呢?
0x02 可選方案
第一個想法: 上 Yarn 等資源調(diào)度
公司現(xiàn)有的資源調(diào)度框架, Yarn 是一個選擇, 但想想要寫 Yarn application 我瞬間就放棄了, 不值當(dāng).
第二個想法: autoscaling + ec2
當(dāng)前在 AWS 中國區(qū), 最簡單的方式就是直接使用 ec2 作為獨立執(zhí)行單元調(diào)度了(沒有 ECS), 既然是耗時耗資源(不是簡單的函數(shù)執(zhí)行, 需要幾個 GB 內(nèi)存的 batch 任務(wù)), 直接使用 ec2 對應(yīng)的 instance type, 也不算浪費. 那么參照 AWS Batch 的拆解, 對應(yīng)的模塊如何實現(xiàn)?
- 任務(wù)隊列: 可以使用 SQS, 但為了簡化系統(tǒng), master 節(jié)點已經(jīng)有了 MySQL, 直接使用 MySQL 作為任務(wù)隊列更簡單
- 任務(wù)執(zhí)行程序: 既然有 master 節(jié)點, ec2 worker 節(jié)點啟動時, 直接從 master 拉取最新的執(zhí)行程序即可.
- 任務(wù)調(diào)度 engine: worker 節(jié)點啟動一個 agent, 使用 polling 的方式從 master 節(jié)點獲取任務(wù)即可.由于任務(wù)之間相互獨立, 調(diào)度的 engine 就簡單的下發(fā)任務(wù)即可
- Producer 端任務(wù)定義: 系統(tǒng)僅僅執(zhí)行一種任務(wù), 下發(fā)的任務(wù)就是簡單的配置, 作為參數(shù)輸入到任務(wù)執(zhí)行程序.
- 權(quán)限等其他配置: 整個集群使用同一個 IAM role 和相同的 security group
系統(tǒng)就變成了這個樣子:
0x03 如何"無損" scale in?
由于任務(wù)是定時開始的(凌晨), 擴容使用 scheduled action; 但如何在沒有任務(wù)執(zhí)行的時候關(guān)閉 ec2 節(jié)點而不影響正在執(zhí)行的任務(wù)?
一種方式是根據(jù) SQS 隊列中的任務(wù)數(shù)量, 可以參見 AWS 官方文檔: Scaling Based on Amazon SQS. 但是我的任務(wù)是比較大的批處理任務(wù), 隊列中沒有任務(wù), 很有可能任務(wù)正在執(zhí)行中, 如果直接關(guān)閉 ec2 instance 進行 scale in 的話, 會導(dǎo)致我的任務(wù)執(zhí)行到一半就失敗.
那么有沒有其他辦法呢? 翻了翻 AWS autoscaling 文檔, 發(fā)現(xiàn) AWS 的新功能: Instance Protection for Auto Scaling, 試了一下, 發(fā)現(xiàn) autoscaling 會一直重試 scale-in 的操作, 直到你的 ec2 instance 把 scale-in 保護關(guān)閉. 正合我意!
0x04 最終實現(xiàn)
事情想清楚了, 行動就顯得簡單了.
首先, 給 worker 節(jié)點的 IAM 要加上對應(yīng)的 autoscaling 權(quán)限
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:SetInstanceProtection"
],
"Resource": "*"
}
]
}
其次, worker 代碼中的關(guān)鍵在于:
- 每次從 master 獲取任務(wù)前, 必須先打開 scale-in 保護
- 強烈建議累計 N 次獲取不到任務(wù)時, 才關(guān)閉 scale-in 保護, 避免任務(wù)隊列中依然有任務(wù), 但 worker 節(jié)點在上一個任務(wù)執(zhí)行完畢到獲取到下一個任務(wù)的間隙, 被 autoscaling 關(guān)閉
# 記錄連續(xù)沒有獲取到 task 的次數(shù)
task_poll_miss_count = 0
while True:
# 打開 scale-in 保護
protect_from_scalein()
try:
# 從 master 節(jié)點獲取任務(wù)
task = get_task_from_master()
if task is not None:
# 如果任務(wù)不為空, 則執(zhí)行任務(wù)
task_poll_miss_count = 0
execute_task(task)
else:
task_poll_miss_count += 1
finally:
if task_poll_miss_count >= 10:
# 連續(xù) 10 次沒有獲取到任務(wù), 應(yīng)該是系統(tǒng)空閑, 直接關(guān)閉 scale-in 保護, 留足夠時間給 autoscaling 關(guān)閉該節(jié)點
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))
由于任務(wù)執(zhí)行的周期性, 直接使用 autoscaling 的 schedued action 提前在任務(wù)執(zhí)行前啟動 worker, 預(yù)估一個執(zhí)行時間后直接將整個 autoscaling group 的節(jié)點數(shù)量設(shè)置成 0.
- 從下向上看, 前三個都是成功被系統(tǒng) scale in, 關(guān)閉節(jié)點
- 后續(xù)節(jié)點由于有任務(wù)執(zhí)行, scale-in 會失敗, 但系統(tǒng)會持續(xù)重試, 直到 scale-in 保護開關(guān)關(guān)閉為止.
0x05 總結(jié)
使用 autoscaling + scale-in 保護, 輕松實現(xiàn)山寨了一個 "AWS Batch". 雖然簡陋, 但很使用, 優(yōu)點就是大大提高了批處理任務(wù)并行度的同時, 還降低了成本. 但缺點也很明顯:
- 需要自己處理任務(wù)執(zhí)行失敗后, 失敗的任務(wù)需要重新投遞問題
- 需要固定的執(zhí)行時間, 以便定時擴容
- 需要在 worker 節(jié)點初始化的時候處理執(zhí)行程序分發(fā)問題, 或者獲取到任務(wù)后根據(jù)任務(wù)中的參數(shù)在執(zhí)行邏輯中添加 docker pull 邏輯
- 資源利用率以單個 instance 為單位, 不支持一個 worker 并發(fā)執(zhí)行多個任務(wù)
總之, 作為一個"山寨的 AWS Batch", 夠用就好. 如果 AWS Batch 中國區(qū)再不發(fā)布, 我就要考慮一下, 把這個山寨貨更完善一下, 畢竟批處理任務(wù)執(zhí)行系統(tǒng)是剛需.
-- EOF --