身為 Java 後端開發者,我們都愛 Java 的強大生態與穩定,但把 Java 丟上 AWS Lambda 時,那個動輒 5 到 10 秒的冷啟動(Cold Start) 總是讓我們在 Serverless 的大門前卻步。

雖然 GraalVM Native Image 是一個解法,但編譯時間長、反射(Reflection)設定複雜,對現有的 Spring Boot 專案並不友善。直到 AWS Lambda SnapStart 出現,它透過 MicroVM 快照技術,讓啟動速度提升了 10 倍以上

今天這篇文章,我將分享在生產環境啟用 SnapStart 的實測數據,以及那些「官方文件沒告訴你」的踩坑經驗。


一、 為什麼 Java 在 Lambda 那麼慢?

Java 的冷啟動延遲主要來自三個階段:

  1. JVM 啟動:載入 Runtime 環境(如載入 JRE)。
  2. 類別載入 (Class Loading):這在 Spring Boot 這種依賴注入(DI)框架中尤其嚴重,需要掃描數千個類別與 Bean。
  3. 框架初始化:建立 Bean 實例、初始化資料庫連線池等。

SnapStart 的原理:它在發布版本時,會先幫你把 Lambda 執行到「初始化完成」的狀態,然後將整個實體(含記憶體與磁碟狀態)拍一張快照(Snapshot)。當請求進來時,它不再從頭啟動 JVM,而是直接從快照恢復(Restore),繞過了最耗時的載入與初始化階段。


二、 支援的 Runtime

SnapStart 目前只支援以下 Java managed runtime

  • Java 11 (Corretto)
  • Java 17 (Corretto)
  • Java 21 (Corretto)

以下情況不支援 SnapStart:

  • Custom Runtime(provided.al2provided.al2023
  • Docker Image 部署(Container Image)
  • 其他語言(Python、Node.js 等):SnapStart 目前僅支援 Java managed runtime,不適用其他語言

在套用之前,請先確認你的 Lambda 使用的是上述其中一個 Runtime。


三、 如何開啟 SnapStart

AWS Console

  1. 進入 Lambda 函數頁面
  2. 點選 Configuration → General configuration → Edit
  3. 找到 SnapStart,選擇 PublishedVersions
  4. 儲存後,重新發布一個 Version,SnapStart 才會生效

注意:SnapStart 只作用在已發布的 Version 上,對 $LATEST 無效。

SAM Template

1Resources:
2  MyFunction:
3    Type: AWS::Serverless::Function
4    Properties:
5      Runtime: java21
6      SnapStart:
7        ApplyOn: PublishedVersions
8      AutoPublishAlias: live

CDK (TypeScript)

1const fn = new lambda.Function(this, 'MyFunction', {
2  runtime: lambda.Runtime.JAVA_21,
3  snapStart: lambda.SnapStartConf.ON_PUBLISHED_VERSIONS,
4});
5
6fn.addAlias('live');

四、 實測數據:SnapStart 真的有用嗎?

我們以一個標準的 Spring Cloud Function + Java 21 專案為例,分配 1024MB 記憶體進行測試:

階段標準 Lambda (Cold Start)啟用 SnapStart (Restore)提升幅度
Init Phase~4,800 msN/A (已預處理)-
Restore Phase-~280 ms-
Invoke Phase~850 ms~150 ms82%
總計延遲~5,650 ms~430 ms約 13 倍快

五、 生產環境的三大坑:請務必小心!

雖然 SnapStart 很強,但在生產環境直接開啟可能會遇到嚴重的副作用。這是因為「快照」會把當時的狀態完全凍結。

1. 隨機數重複問題 (Uniqueness)

如果你的代碼使用了 java.util.Random,快照會連同隨機數種子一起凍結。恢復出來的每一個 Lambda 實體,產生的第一個隨機數可能完全一樣

  • 解法:請改用 java.security.SecureRandom,它在 Linux 環境下會自動處理快照恢復後的「熵源」更新。

2. 資料庫連線失效 (Network Connections)

快照發生時,資料庫連線可能是建立好的。但當快照被恢復時,該 TCP 連線早就被資料庫端(如 RDS)超時中斷了。

  • 解法:利用 CRaC (Coordinated Restore at Checkpoint) 介面在恢復時重新建立連線(詳見第六章)。

3. 狀態一致性

如果初始化過程會抓取「當下的時間」或讀取動態配置,這些值會被固定在快照中,導致恢復後的資料過時。

  • 解法:在 afterRestore() 中重新呼叫配置中心(如 AWS AppConfig、Parameter Store),刷新快取的動態設定。時間戳記則避免在 init 階段快取,改為在每次 invoke 時動態讀取。

六、 如何正確實作:使用 CRaC 鉤子

為了處理上述連線問題,我們必須實作 Resource 介面。

首先引入 org.crac:crac 依賴:

Maven

1<dependency>
2    <groupId>org.crac</groupId>
3    <artifactId>crac</artifactId>
4    <version>1.4.0</version>
5</dependency>

Gradle

1implementation 'org.crac:crac:1.4.0'

接著實作 Resource 介面:

 1import org.crac.Context;
 2import org.crac.Resource;
 3import org.crac.Core;
 4
 5public class DatabaseHandler implements Resource {
 6    public DatabaseHandler() {
 7        // 向 CRaC 註冊此資源
 8        Core.getGlobalContext().register(this);
 9    }
10
11    @Override
12    public void beforeCheckpoint(Context<? extends Resource> context) {
13        System.out.println("準備拍快照,正在關閉連線池...");
14        // 這裡要優雅地關閉 HikariCP 或其他連線資源
15    }
16
17    @Override
18    public void afterRestore(Context<? extends Resource> context) {
19        System.out.println("快照恢復,正在重新建立連線...");
20        // 重新初始化資料庫連線或刷配置
21    }
22}

七、 總結:該不該用 SnapStart?

SnapStart 是目前 Java 在 AWS Lambda 上的最佳平衡點。 它不需要像 Native Image 那樣大改專案架構,只要加上簡單的 Runtime 鉤子,就能獲得近乎秒開的效能。

計費說明

SnapStart 並非免費。Restore 階段的執行時間(Duration)會照常計費,但因為 Restore 通常只需約 200-300 ms,實際費用影響極小。快照本身的儲存不額外收費

我的建議:

  • 如果你是 Spring Boot / Micronaut 用戶:必開,這能大幅提升使用者體驗。
  • 如果你對冷啟動極度敏感(如面向用戶的 API):必開
  • 如果你只是跑背景非同步任務(如 SQS 消費者):可以不開,影響有限。