前陣子受 Pahud 大大對 Lambda 的心得啟發,做了一些小測試以理解其特性,也希望能更加善用 Lambda。底下測試使用 Python / NodeJS (0.10 and 4.3),歡迎喜愛 Java 的大大補完。

AWS 會在 Server Pool 上散佈 Lambda Funcion 程式碼,並以 Container 一類的方式運行。Manual 裡提到應將 handler function 作為程式的主要進入點,並避免使用 handler function 外的變數 (Global),這立刻讓我懷疑 Lambda 可能透過 event loop 或是多執行緒,使多組 handler 在同一個環境下並行;然而實測後並非如此。

Lambda Container Reuse

參考 Understanding Container Reuse in AWS Lambda,Lambda Host 在運行 Function 時,會重覆利用 Container;Container 若正常結束,Lambda 會保存當下狀態 (freeze) 備日後復用。當該 Function 再次被調用時,若該 Frozen Container 仍然可用,Lambda 會重新啟動 (thaw) 並調用 handler,無需再次初始化。多次(不並行)執行下列 Function 就能體驗此狀況:

var myId = (new Date()).getMilliseconds()

module.exports.handler = function(evt, ctx, cb) {
return (cb || ctx.done)(null, myId)
}

由於 Container Reuse, myId 會在首次執行時被定義,後面幾次則不刷新。

更多事實

handler() 不並行

var myId = (new Date()).getMilliseconds()

module.exports.handler = function(evt, ctx, cb) {
console.log(myId + ' initiated')

setTimeout(function() {
console.log(myId + ' completed')
(cb || ctx.done)(null, myId)
}, 2000)
}

修改範例使它需要兩秒執行時間,並且改用命令列調用,立即會發現執行時間重疊的 Invocation,拿到的 myId 也不一樣。因此可以推論出 AWS 目前未實現 runtime 層級的復用,開發者因而先不用擔心 thread safe 的問題。

復用排程邏輯

若在刷出兩線 container 後恢復單線調用,則回應的 myId 又都是同一個;可以推論出 AWS 選擇排程的算法偏好最近用過 (thaw / freeze) 的 Container。

Timeout & Error

在程式發生各種錯誤後,AWS Lambda 會丟棄該 Container,以免錯誤的資料影響下次執行結果。

在 Lambda Node (0.10)/ 4.3 呼叫 context.done() 時,Lambda 會以類似 setTimeout 的方式排程,以 freeze 該 container。若當時有其它任務已在 event loop 中,Lambda 下次復用該 container 時 (透過 thaw), 這些任務會一併被運行。

// timeout: 2 seconds
var myId = (new Date()).getMilliseconds()

exports.handler = function(evt, ctx, cb) {
console.log('[' + myId + '] started')

setTimeout(function(){
console.log('[' + myId + '] finishing')
ctx.done(null, myId)
console.log('[' + myId + '] done')
}, 1000)

setTimeout(function(){
console.log('[' + myId + ' forgot something')
}, 2200)
};

執行結果如下,id 相同代表 container 被復用;第二次執行時可以看到第一次留下的任務。由於 container 在調用 context.done() 後即結束,Lambda 的執行時間(用於計費)也只計算到此。

START RequestId: 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 Version: $LATEST
2016-09-09T08:15:44.069Z 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 [59] started
2016-09-09T08:15:45.072Z 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 [59] finishing
END RequestId: 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8
REPORT RequestId: 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 Duration: 1013.25 ms Billed Duration: 1100 ms
START RequestId: b796654b-7665-11e6-b3e7-cb3a395e4762 Version: $LATEST
2016-09-09T08:16:32.527Z 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 [59] done
2016-09-09T08:16:32.528Z 9a9c2eab-7665-11e6-9f9b-2bbf72889dc8 [59] leftover
2016-09-09T08:16:32.529Z b796654b-7665-11e6-b3e7-cb3a395e4762 [59] started
2016-09-09T08:16:33.531Z b796654b-7665-11e6-b3e7-cb3a395e4762 [59] finishing
END RequestId: b796654b-7665-11e6-b3e7-cb3a395e4762
REPORT RequestId: b796654b-7665-11e6-b3e7-cb3a395e4762 Duration: 1002.25 ms Billed Duration: 1100 ms

4.3 提供的 callback() 行為則略有不同,它似乎只負責回傳資料,不介入控制程序;Lambda 會持續執行,直到 event loop 清空或 timeout。

var myId = (new Date()).getMilliseconds()

module.exports.handler = function(evt, ctx, cb) {
console.log(myId + ' initiated')

setInterval(function(){ // 超時的 setTimeout 也有一樣效果
console.log(myId + ' oops')
}, 300)

setTimeout(function() {
console.log(myId + ' completed')
cb(null, myId)
}, 500)

}
START RequestId: 236f6237-7667-11e6-8438-af4d75d76e02 Version: $LATEST
2016-09-09T08:26:43.068Z 236f6237-7667-11e6-8438-af4d75d76e02 66 initiated
2016-09-09T08:26:43.399Z 236f6237-7667-11e6-8438-af4d75d76e02 66 oops
2016-09-09T08:26:43.598Z 236f6237-7667-11e6-8438-af4d75d76e02 66 completed
2016-09-09T08:26:43.699Z 236f6237-7667-11e6-8438-af4d75d76e02 66 oops
2016-09-09T08:26:43.999Z 236f6237-7667-11e6-8438-af4d75d76e02 66 oops
END RequestId: 236f6237-7667-11e6-8438-af4d75d76e02
REPORT RequestId: 236f6237-7667-11e6-8438-af4d75d76e02 Duration: 1003.23 ms Billed Duration: 1000 ms Memory Size: 128 MB Max Memory Used: 14 MB
2016-09-09T08:26:44.069Z 236f6237-7667-11e6-8438-af4d75d76e02 Task timed out after 1.00 seconds

Prewarm ?

這有點尷尬,但我覺得如果 Lambda Function 需要預熱,應該已經落入了 Lambda 的 anti-pattern。常見的原因有功能太過龐大、Eager initialization 等等。

若是無法避免,在 Lambda concurrency 足夠的前提下,定期大量並行某些 lambda function,確實會減低用戶執行時碰到新 container,執行(自己寫的)初使化的等待。