01

執行環境生命週期與 Cold Start 的解剖

FaaS 不是「每次呼叫生一個用完即丟的行程」,而是一台經歷 INIT、多次 INVOKE 凍結與解凍、最後 SHUTDOWN 的隔離微虛擬機。

在運算模型光譜篇,我們看過 VM、編排容器、Serverless 容器與 FaaS 四個刻度的責任邊界與月費交叉點。當決定採用 AWS Lambda 或 Google Cloud Run functions 時,真正決定系統尾延遲(p99)、吞吐上限與下游資料庫生死的,不再是作業系統排程,而是執行環境(Execution Environment)的生命週期與並發模型。

以 AWS Lambda 為例,每個執行環境跑在獨立的 Firecracker microVM 內,生命週期嚴格分為三個階段:INIT、INVOKE 與 SHUTDOWN。只有當現有已初始化的環境都在忙碌、或環境因閒置與定期輪替被回收時,新請求才會觸發完整的冷啟動(Cold Start);隨後的請求只要落入已就緒的環境,就會走解凍重用的暖啟動(Warm Start)。

sequenceDiagram
    participant R as 呼叫端 / 事件源
    participant C as Lambda 控制平面
    participant E as 執行環境 (microVM)
    R->>C: 第 1 次請求 (無可用暖環境)
    C->>E: 配置 microVM + INIT (Extension -> Runtime -> Function 靜態碼)
    C->>E: INVOKE (執行 handler)
    E-->>R: 回應結果 (計費含 Init Duration + Duration)
    Note over E: 凍結 (Freeze):背景計時器與執行緒全部暫停
    R->>C: 第 2 次請求 (命中暖環境)
    C->>E: 解凍 (Unfreeze) + INVOKE (跳過 INIT)
    E-->>R: 回應結果 (僅收本次 Duration)
    Note over C,E: 閒置逾時後發送 SHUTDOWN (預留最多 2 秒供 Extension 刷出遙測)
  

冷啟動慢在 INIT 的三步串列初始化;暖重用跳過 INIT 直接進入 INVOKE,但兩次呼叫之間環境處於凍結狀態,不可依賴背景非同步工作。

INIT 階段的三個子步驟與 2025/2026 計費變革

當新環境啟動時,INIT 依序執行三件事:啟動所有外部監控與安全性代理(Extension init)、拉起語言執行期(Runtime init,如 JVM、Node.js V8 或 Python 解譯器),最後執行 handler 函式以外的全域靜態程式碼(Function init,如載入大型 SDK、初始化資料庫連線池或讀取參數)。

  • INIT 階段全面納入計費(2025 年 8 月起標準化):早期 AWS Lambda 對使用受管執行期(Managed Runtime)與 ZIP 封裝的 on-demand 函式,不會將 Init Duration 計入帳單;但自 2025 年 8 月起,Lambda 已將計費規則統一——不論是 ZIP、OCI 容器映像、自訂 Runtime 或 Provisioned Concurrency,冷啟動的 Init Duration 一律加進 Billed Duration 按 GB·s 收費。把肥大的依賴樹塞在全域載入,現在不只傷 p99 延遲,還會直接拉高冷啟動帳單。
  • 10 秒 INIT 超時與雙重計費陷阱:標準 on-demand 呼叫的 INIT 階段硬上限是 10 秒。若靜態初始化在 10 秒內沒跑完,Lambda 會強制中止並緊接著在第一次 INVOKE 內以函式自身的 timeout 重跑一次 INIT——兩次嘗試的時間都會被計入帳單。
  • 跨呼叫重用與凍結邊界:全域變數與 /tmp 暫存目錄(可配置 512 MB 至 10,240 MB)在同一個執行環境的多次 warm invocation 之間會保留。然而,一旦 handler 回傳,環境立即被凍結(如同收到 SIGSTOP):任何未 await 的背景 Promise、未刷出的非同步 log 或計時器都會停擺,直到下一次呼叫解凍才突然吐出,造成跨請求的日誌與狀態污染。
陷阱把環境重用當成「有狀態快取」來寫業務邏輯

全域變數只適合放「丟掉也能重建」的資源(例如 SDK client、連線池、唯讀設定快取)。你永遠無法保證下一筆請求會落進同一個執行環境,也無法阻止平台在背景輪替基礎設施時直接銷毀環境。

一句話答法FaaS 的冷啟動發生在配置環境與三步 INIT(Extension、Runtime、全域靜態碼);環境雖會被凍結並跨呼叫重用,但業務邏輯必須維持無狀態,且所有在 handler 回傳前未完成的背景非同步工作都會被凍結截斷。
02

消弭冷啟動的兩條路:Provisioned Concurrency 與 SnapStart

一個是用常駐預算換取預熱好的活環境,一個是在發佈版本時把初始化後的記憶體拍成快照。兩者不能混用,各自有不同的失效邊界。

當裁剪依賴套件、改用延遲載入(Lazy Initialization)仍壓不下 JVM 或大型框架動輒數秒的冷啟動時,平台提供兩條截然不同的架構路徑:Provisioned Concurrency(預置並發,對應 Cloud Run 的 min-instances)與 Lambda SnapStart。

階段
On-Demand 冷啟動
Provisioned Concurrency
Lambda SnapStart
發佈版本時
僅上傳程式碼產物
依設定數量預先建立環境並跑完 INIT(最長 15 分鐘)
跑一次 INIT + beforeCheckpoint,拍下加密記憶體/磁碟快照
閒置待命期
縮到 0,不收待命費
環境常駐待命,按配置 GB·s 持續計費
僅存快照,無常駐環境(Java 免額外費;Python/.NET 收快取費)
突發首筆請求
當場跑完整 INIT + INVOKE,尾延遲最高
配額內直接進 INVOKE;超出配額的溢出流量退回一般冷啟動
從快取載入快照(Restore)+ afterRestore + INVOKE

三種路徑把 INIT 的時間成本放在不同時刻:On-Demand 放在使用者請求當下、Provisioned Concurrency 放在常駐待命帳單、SnapStart 放在版本發佈時的快照。

Provisioned Concurrency(PC)的成本折衷與溢出行為

Provisioned Concurrency 會在流量進來前就建立指定數量的執行環境並跑完 INIT。它的優勢是完全不挑語言與框架,且預熱環境可搭配 Application Auto Scaling 依排程或利用率調整。但設計時有兩個常被忽略的限制:

  1. 溢出(Spillover)不是排隊,而是退回冷啟動:若設定 PC = 100,瞬間湧入 140 個並發請求,前 100 個命中預熱環境、零冷啟動,但多出的 40 個會立即以標準 On-Demand 模式拉起新環境——也就是那 40 個請求依然會吃到完整冷啟動。
  2. 60% 利用率分水嶺:因為預置環境全天候收費,AWS 官方實務建議在該預置量能維持 60% 以上穩定利用率時才最具成本效益;若流量極度稀疏卻為了消弭偶發冷啟動而長開 PC,就失去了 FaaS 縮到零的計費優勢。

SnapStart 的快照還原原理與三大面試考點

Lambda SnapStart 採用 Firecracker 虛擬機快照技術:在發佈函式版本(Publish Version)時啟動一個環境跑完 INIT,觸發 beforeCheckpoint hook,接著將整個 microVM 的記憶體頁與磁碟狀態凍結成加密快照並放入低延遲快取。之後每當需要擴縮新環境時,直接從快照 Restore 記憶體狀態並執行 afterRestore hook,把數秒的類別載入縮短至數百毫秒內。

比較維度 Provisioned Concurrency (PC) Lambda SnapStart Cloud Run min-instances
核心機制 預先拉起並維持活著的執行環境 發佈時執行 INIT 並拍下 Firecracker 快照,擴縮時還原 預先維持常駐容器實例(單實例可接多並發)
支援 Runtime 全部 Runtime 與容器映像 Java、Python、.NET 受管執行期(不支援容器映像) 全部語言與自訂容器
計費結構 預置時間(每 GB·s)+ 請求 + 執行時間 Java 免收快照費;Python / .NET 收快照快取與每次還原費 閒置實例收低費率或依 instance-based 計價
互斥與硬限制 不可與 SnapStart 同版本並用 不可與 PC、EFS、或大於 512 MB 的 /tmp 並用 需自行確保實例內多執行緒/並行請求安全

SnapStart 解決了「突發擴縮不想付常駐待命費」的痛點,但把狀態複製的副作用留給了工程師。

陷阱SnapStart 的「唯一性複製」與「死連線」事故

因為所有新環境都是從同一份記憶體快照複製(Clone)出來的,任何在 INIT 階段生成的隨機數種子(PRNG seed)、預先產生的 UUID、或是暫存的短期 IAM 憑證,在數百個還原環境中都會一模一樣;同時,在 INIT 建立的長 TCP 連線在數小時後還原時也早已失效。使用 SnapStart 必須遵守兩條鐵律:(1) 唯一 ID 與隨機數一律放進 handler 或 afterRestore 重新播種;(2) 網路連線在 afterRestore 驗證或重建。

一句話答法Provisioned Concurrency 用常駐預熱環境消弭冷啟動,適合穩定基載;SnapStart 用版本發佈時的記憶體快照省下常駐費,適合冷啟動重的 Java / Python / .NET,但必須透過 afterRestore 處理隨機數種子重複與過期連線。
03

並發模型、擴縮速率與配額數學

標準 AWS Lambda 一個環境同時只接一個請求;Cloud Run functions 一個實例可接數十至上千個並行請求。兩者差了一個並發除數 k。

在系統設計面試中估算 FaaS 容量,起點永遠是 Little's Law:系統同時在途的請求數 L,等於每秒到達率 λ(rps)乘上單次平均執行時間 W(秒)。如果每個執行環境能同時處理 k 個請求,所需的活躍環境數 E 就是:

L = λ × W   ,   所需環境數 E = ceil(L ÷ k)
標準 AWS Lambda Event Functions(k = 1)

每個 microVM 在同一時刻只處理 1 個請求。當 λ = 600 rps、每次呼叫等下游 API 或資料庫 W = 0.4 秒,系統同時需要 600 × 0.4 = 240 個獨立執行環境。好處是請求之間具備硬體級隔離、不會因隔壁請求 OOM 而陪葬;代價是所有 I/O 等待時間都一比一放大成環境數、冷啟動次數與下游連線數。

Cloud Run functions / Lambda 多並發模式(k > 1)

Cloud Run functions(第 2 代起底層即為 Cloud Run service)預設每實例並發為 80 × vCPU(最高可設 1,000);AWS 亦在 Lambda Web Functions 與 Managed Instances 引入單環境多並發。同樣 L = 240 個在途請求,若每實例可並行處理 k = 40 個 I/O 請求,只需 ceil(240 ÷ 40) = 6 個實例,冷啟動與連線數直接縮小 40 倍,但程式必須具備並行安全且共享故障半徑。

k = 1 把並行問題交給平台開更多 microVM;k > 1 把並行交回容器內的事件迴圈或執行緒池,換取極低的冷啟動與連線開銷。

「帳號總並發配額」與「每 10 秒擴縮速率」是兩把不同的尺

許多工程師以為把 AWS 帳號的區域並發配額(Account Concurrency Quota,預設 1,000,可申請調高至數萬)拉高,就能無上限吸收瞬間洪峰。事實上,AWS Lambda 對每一個 Event Function 還有一道獨立的擴縮速率上限(Concurrency Scaling Rate):

  • 每函式每 10 秒最多新增 1,000 個執行環境(或相當於每 10 秒增加 10,000 rps 的處理能力)。這條限制是 per-function 的,不會因為別的函式閒置就借給你,也不會因為帳號總配額調高到 10,000 就自動消失。
  • 冷啟動期間的「峰值環境膨脹」:在突發流量的第一波,尚未預熱的環境必須先跑完 Tinit 才能進入 W,代表該環境被佔用的總時間變成 Tinit + W。若 W = 400 ms 而冷啟動 Tinit = 800 ms,冷啟動期間每個新環境的周轉率只有暖環境的三分之一,瞬時並發環境需求會短暫膨脹為穩態的 1 + Tinit / W = 3 倍!一旦超過首個 10 秒視窗可供給的 PC + 1,000 個環境,多出的同步請求就會收到 429 TooManyRequestsException。
  • Reserved Concurrency(預留並發)的雙面刃:Reserved Concurrency 同時扮演「保底配額」與「硬上限(Hard Cap)」。替關鍵函式預留 200 並發,代表另外 800 個帳號配額留給其他函式,但也代表這支關鍵函式自己永遠不能超過 200 並發;且 Reserved Concurrency 只切配額、完全不會預熱環境(設為 0 則常作為緊急斷路開關)。
關鍵下游變慢時,FaaS 會自動把小故障放大成並發枯竭

根據 L = λ × W,當請求到達率 λ 完全沒變(例如維持 500 rps),只因下游支付閘道或資料庫延遲從 100 ms 退化到 2,000 ms,Lambda 的並發環境數就會從 50 瞬間暴漲 20 倍到 1,000,直接吃乾整個帳號的並發配額!這就是為什麼 FaaS 呼叫外部依賴時,必須設定嚴格的短 Timeout 與失敗快速退回,絕不能放任函式乾等 15 分鐘。

一句話答法FaaS 的並發數由 λ × W ÷ k 決定:標準 Lambda 因 k = 1 會將 I/O 等待時間一比一放大為環境數,且受限於每函式每 10 秒最多擴增 1,000 個環境的速率上限。
04

資料庫連線風暴與 RDS Proxy 收斂架構

幾百個瞬間拉起的 microVM 各自握手一條資料庫連線,會在抵達運算配額前先把 PostgreSQL / MySQL 的 max_connections 擊穿。

在傳統常駐容器架構中,20 個 Pod 各自維護一個 max = 10 的本地連線池,資料庫最多只會看到 200 條連線;當流量暴增時,請求是在應用程式記憶體裡的連線池排隊。但在標準 AWS Lambda(k = 1)中,每個執行環境同一時間只跑 1 個請求,環境之間完全不共享記憶體與連線池——600 個並發環境就意味著 600 個獨立行程各自向資料庫發起 TCP + TLS + 認證交握,並把連線握在全域變數裡直到環境銷毀。

600 個 Lambda 環境
k = 1,各自握 1 條長連線
──600 條前端連線──▸
RDS Proxy / PgBouncer
交易級別複用 (Transaction Pooling)
──收斂至 ~38 條──▸
RDS PostgreSQL
max_connections = 200 安全線內

RDS Proxy 擋在數百個短命或凍結的執行環境前面:只有正在執行 SQL 交易的那幾十毫秒才借用後端資料庫連線,其餘時間立即歸還池中。

三種收斂連線風暴的工程手段與「連線釘住(Pinning)」陷阱

  1. 部署外部交易級連線池(RDS Proxy / PgBouncer):Lambda 雖然維持與 Proxy 的連線,但 Proxy 對後端資料庫採交易級複用(Transaction-level Multiplexing)。若一次 400 ms 的函式呼叫只有 25 ms 真正在跑 SQL,理論複用倍率可達 16 倍。但務必小心 Session Pinning(連線釘住):只要程式碼執行了改變工作階段狀態的指令(如 SET 變更時區或參數、使用暫存表、開啟未關閉的 Prepared Statement),RDS Proxy 為了安全就無法把該連線借給別的環境,複用倍率瞬間退化回 1:1。
  2. 提高每實例並發度(Cloud Run functions 或 Lambda Managed Instances):當單一容器實例同時承接 k = 40 個請求,這 40 個請求就能像傳統微服務一樣共用實例內的同一個連線池,從源頭把實例數與連線池總數除以 k。
  3. 改用無狀態 HTTP 資料層或上游限流:DynamoDB 走無狀態 HTTPS API,天生沒有 TCP 連線池上限問題;若後端是關聯式資料庫且無法承受高並發,則應以 SQS 佇列削峰加上 Reserved Concurrency(或 Event Source Mapping 的 MaximumConcurrency)把同時寫入資料庫的環境數鎖在安全範圍內。
互動 · FaaS 並發擴縮與資料庫連線風暴模擬器 調整流量、執行與冷啟動耗時,即時重算環境峰值與 DB 連線負載

以下模型以 Little's Law(L = λ × W)為核心,並計入冷啟動期間未預熱環境因 INIT 佔用時間拉長為 W + Tinit 所造成的瞬時環境膨脹、AWS Lambda 每 10 秒最多擴增 1,000 個環境的速率限制,以及對後端資料庫(設 max_connections = 200)的連線衝擊。

單一環境/實例並發數 k
資料庫連線收斂機制(複用倍率 M)
到達率 λ600 rps
執行時間 W400 ms
冷啟動 INIT800 ms
預熱環境數 PC100 個
穩態環境 / 突發峰值環境
—

首波冷啟動比例 / 突發 p99
—

10 秒擴縮步階(上限 +1,000/10s)
—

後端 DB 連線數(上限 200)
—

首個 10 秒視窗環境供給率(預熱 PC + 首波 1,000 擴縮額度) —
後端資料庫連線水位(對比純 On-Demand 直連 vs 目前設定) —
配額與連線充裕 有冷啟動或連線過半 觸發 429 限流或擊穿 DB 連線池

試著將「資料庫連線收斂機制」切到「直連 DB(1:1)」,或把執行時間 W 從 400 ms 拉到 1,200 ms:即使到達率不變,冷啟動期間膨脹的環境數也會立刻把 200 條資料庫連線衝爆。

一句話答法標準 Lambda 每環境一請求的模型會讓環境數與資料庫連線數呈 1:1 暴增;連關聯式資料庫時務必透過 RDS Proxy 做交易級別連線複用(並避免 Session Pinning),或改用多並發實例與無狀態資料 API。
05

非同步事件承接、部分批次失敗與冪等鐵律

FaaS 承接佇列與事件流時,平台內建的重試與批次拉取保證的是 At-Least-Once;沒有開啟部分批次失敗回報與冪等保護,一筆壞資料就會重播整批交易。

佇列本身的語義(Visibility Timeout、DLQ)與事件驅動的服務拆分(Outbox、Choreography),分別在訊息佇列基礎篇與事件驅動架構篇討論過。本節聚焦於 FaaS 執行環境在最末端如何承接這些呼叫——因為呼叫模式不同,重試主體與失敗邊界完全不同。

呼叫模式 典型上游來源 誰負責重試與退避? FaaS 端的關鍵防護設計
同步呼叫 (Synchronous) API Gateway、ALB、Cloud Run HTTP 觸發 平台不自動重試,直接回傳錯誤給呼叫端 API 層帶 Idempotency-Key,控制 INIT 與下游 Timeout 避免觸發 29 秒/60 秒閘道逾時
非同步呼叫 (Asynchronous) EventBridge、S3 事件、SNS(推入 Lambda 內部佇列) Lambda 內建佇列自動重試最多 2 次(共 3 次執行,含退避) 設定 MaximumEventAgeInSeconds 與 OnFailure Destination(保留完整錯誤上下文)
事件源映射 (Event Source Mapping) SQS、Kinesis、Managed Kafka(由 Lambda Poller 批次拉取) 整批拉取,失敗回退至來源佇列/分片重試直到過期或入 DLQ 必開 ReportBatchItemFailures + 外部冪等表 + 限制 MaximumConcurrency

最容易出事的是第三類 Event Source Mapping:Poller 幫你把 10 筆訊息包成一個 Batch 餵進同一次 INVOKE,預設只要 1 筆拋出例外,整批 10 筆都會被當成失敗重試。

Partial Batch Response 與為何「開了部分失敗回報仍必須冪等」

當 Lambda 透過 Event Source Mapping 消費 SQS 或 Kinesis(例如 batchSize = 10)時,若第 7 筆訊息因格式錯誤或下游暫時超時而讓 handler 拋出例外,預設行為是整批 10 筆訊息都不刪除,待 Visibility Timeout 到期後,前 6 筆已經成功扣款或發信的訊息會被重新投遞再跑一次。

flowchart LR
    B[Poller 拉取 10 筆訊息] --> H[Handler 逐筆處理]
    H --> C{開啟 ReportBatchItemFailures?}
    C -->|未開啟: 拋出例外| A[整批 10 筆退回重試]
    C -->|已開啟: 回傳失敗 ID 清單| P[前 6 筆刪除, 僅失敗項重試]
    P --> I[下游仍須以 event_id 做冪等檢查]
  

開啟 ReportBatchItemFailures 能擋掉「單筆失敗拖累同批成功項」,但擋不掉「第 6 筆剛寫入資料庫、函式卻在回傳前 OOM 或 Timeout」的跨次重試。

因此,在 FaaS 處理非同步事件時,必須同時落地兩道防線:

  1. 啟用 ReportBatchItemFailures(Partial Batch Response):捕捉單筆訊息的例外,將失敗的 itemIdentifier(如 SQS messageId)放入 batchItemFailures 陣列回傳,讓成功處理的訊息正常確認刪除。
  2. 以業務冪等鍵(Idempotency Key)守住寫入端:就算開啟了部分批次失敗回報,如果函式在寫完資料庫之後、回傳 JSON 給 Lambda Runtime 之前剛好撞上函式 Timeout 或容器崩潰,Lambda 無法收到回傳清單,依然會把整批訊息退回重試。寫入下游時必須攜帶唯一事件 ID(例如在 DynamoDB 使用 ConditionExpression: "attribute_not_exists(event_id)" 搭配狀態與 TTL,或在關聯式資料庫以唯一索引做 INSERT ... ON CONFLICT DO NOTHING)。
關鍵SQS 觸發器的「失控擴縮」要用 MaximumConcurrency 鎖住

當 SQS 堆積數萬筆訊息時,Lambda Event Source Mapping 預設會以每分鐘擴增數十個並發的速度一路拉高到 1,000 並發,瞬間把下游慢速 API 或資料庫打垮;若你在函式上把 Reserved Concurrency 設小,又會導致 Poller 拉了訊息卻因函式自身被 429 節流而直接把訊息推往 DLQ。正確作法是在 SQS Event Source Mapping 上設定 MaximumConcurrency——它直接限制 Poller 最多只用幾個執行環境拉取,不會產生假性節流與誤入 DLQ。

一句話答法FaaS 消費佇列與事件流屬 At-Least-Once 投遞:工程上必須同時開啟 ReportBatchItemFailures 避免單筆毒丸重播整批、在 Event Source Mapping 設定 MaximumConcurrency 保護下游,並以持久化冪等鍵處理 Timeout 造成的重試。