去年參加了 AWS re:Invent 2016, 最喜歡也最需要的新產品莫過于 AWS Batch. 回來幾個月了發現中國區依舊沒有上線, 然后就手癢癢"山寨"了一個, 跑著還很穩定. 記錄一下"山寨"過程中的思考.
0x00 AWS Batch 拆解
什么是 AWS Batch? 官方介紹:
AWS 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 保護開關關閉為止.
0x05 總結
使用 autoscaling + scale-in 保護, 輕松實現山寨了一個 "AWS Batch". 雖然簡陋, 但很使用, 優點就是大大提高了批處理任務并行度的同時, 還降低了成本. 但缺點也很明顯:
- 需要自己處理任務執行失敗后, 失敗的任務需要重新投遞問題
- 需要固定的執行時間, 以便定時擴容
- 需要在 worker 節點初始化的時候處理執行程序分發問題, 或者獲取到任務后根據任務中的參數在執行邏輯中添加 docker pull 邏輯
- 資源利用率以單個 instance 為單位, 不支持一個 worker 并發執行多個任務
總之, 作為一個"山寨的 AWS Batch", 夠用就好. 如果 AWS Batch 中國區再不發布, 我就要考慮一下, 把這個山寨貨更完善一下, 畢竟批處理任務執行系統是剛需.
-- EOF --