本文測試數據與實作細節由同事提供。

AWS Lambda 計費看起來很簡單:Memory × Runtime = Compute(GB-s),用多少付多少。

但真正去調校過才會發現,這個公式裡藏著不少眉角。Memory 設太小,執行慢、費用不一定比較低;連線池沒配好,偶發的 EOF 會讓呼叫方一頭霧水;Secrets Manager Extension 失敗時,如果沒有 fallback,整個函式直接壞掉。

這篇文章記錄三個我們實際跑過的調校,每一個都有數字、有程式碼、有前因後果。


一、Memory 調校:花 2.6% 換 45% 的速度

Lambda 的計費邏輯

Lambda 的費用由兩個維度決定:

  • Memory:分配給函式的記憶體大小
  • Runtime:函式實際執行所花的時間

兩者相乘才是真正的計費單位 GB-s。

有一個常被忽略的細節是:Memory 越大,AWS 同時也會配置更多 CPU 資源,所以給更多記憶體不只是讓函式能存更多東西——它會直接讓函式跑得更快。

這代表存在一個最佳平衡點。Memory 調高讓 runtime 縮短,但超過某個臨界點後,省下的時間已無法抵銷增加的記憶體費用。

測試結果

10,500 requests/day 的流量為基準,對同一份程式碼測試三種 Memory 設定:

Memory (MB)Runtime (ms)每月 Compute (GB-s)
1281,67266,749
24091568,491
51248477,289

以 128MB 為基準換算:

  • 240MB:成本 +2.6%,runtime 縮短 45%
  • 512MB:成本 +15.8%,runtime 縮短 71%

240MB 是最佳甜蜜點。

另外測試中發現 240MB 與 256MB 的 runtime 幾乎相同,代表在這個區間 CPU 資源沒有再增加,選 240MB 即可。

設定變更

 1# Before
 2resource "aws_lambda_function" "handler" {
 3  memory_size = 128
 4  timeout     = 30
 5}
 6
 7# After
 8resource "aws_lambda_function" "handler" {
 9  memory_size = 240
10  timeout     = 10
11}

Timeout 同步從 30s 縮短為 10s,反映實際執行時間,避免函式異常時白白等待過久。


二、Secrets Manager 穩定性:加一條 Fallback 路徑

背景

AWS Secrets Manager 是用來集中管理敏感設定(API 金鑰、憑證等)的服務。

為了加速存取,AWS 提供了一個 Lambda Extension,它會在 Lambda 容器內部建立本地快取,避免每次都打 API,降低延遲並節省費用。

問題

偶發性地,從這個本地快取讀取設定時會失敗,導致函式無法正常啟動。

原本的邏輯只有單一路徑:

1// Before: 只有一條路,失敗就直接回傳 error
2secretResponse, err := extensionService.GetSecret(secretName)
3if err != nil {
4    return nil, fmt.Errorf("unable to get secret: %w", err)
5}

解法

加入 fallback 機制:Lambda Extension 失敗時,改走 AWS SDK 直接呼叫 Secrets Manager API。

 1// After: Extension 失敗時,fallback 到 AWS SDK
 2func getSecretWithFallback(ctx context.Context, ext SecretGetter, sdk SecretReader, secretName string) (string, error) {
 3    secret, err := ext.GetSecret(secretName)
 4    if err == nil {
 5        return secret, nil
 6    }
 7
 8    // 只有特定錯誤類型才值得 fallback
 9    // 若是其他錯誤(例如 secret 不存在),fallback 也沒有意義
10    if !shouldFallback(err) {
11        return "", fmt.Errorf("extension failed: %w", err)
12    }
13
14    log.Printf("extension failed, falling back to SDK: %v", err)
15    return sdk.GetSecret(ctx, secretName)
16}
17
18func shouldFallback(err error) bool {
19    return errors.Is(err, ErrExtensionNotReady) ||
20        errors.Is(err, ErrExtensionRequest) ||
21        errors.Is(err, ErrExtensionServer)
22}

同時將 Lambda Extension layer 升級至最新版本,包含上游的穩定性修復。

注意 fallback 的條件要明確——不是所有錯誤都適合重試。Extension 尚未就緒、請求失敗、伺服器錯誤才值得 fallback;如果是 secret 根本不存在,fallback 到 SDK 也只是多打一次失敗的 API。


三、Connection Pool:EOF 錯誤的根因與修復

Connection Pool 是什麼

每次建立網路連線都有成本(TCP handshake、TLS 握手等)。Connection Pool 的概念是預先建立好一批連線並重複使用,避免每個請求都重新建立,藉此降低延遲。

問題

Connection Pool 裡的連線是有生命週期的。如果一條連線閒置太久,對端伺服器可能主動將它關閉,但 client 端不一定即時感知到。

當下一個請求拿到這條「已死」的連線去發送資料時,就會收到 EOF——代表連線已被對方切斷,沒有任何回應。

解法

兩個方向同時處理:

  1. 縮短 idle 連線的存活時間

idleConnTimeout 從 90 秒縮短為 5 秒,讓 pool 中的連線在被對端關閉之前就主動淘汰。

1// Before
2idleConnTimeout = 90 * time.Second
3
4// After
5idleConnTimeout = 5 * time.Second
  1. 偵測到死連線時自動重試

縮短 timeout 能降低機率,但無法完全消除競態條件——連線可能在剛被取出的瞬間被對端關閉。加入 EOF retry 作為第二道保險:

 1const maxRetries = 1
 2
 3func (s *Service) SendRequest(ctx context.Context, payload string) (int, []byte, error) {
 4    var lastErr error
 5
 6    for attempt := 0; attempt <= maxRetries; attempt++ {
 7        req, _ := http.NewRequestWithContext(ctx, http.MethodPost, s.url, strings.NewReader(payload))
 8
 9        resp, err := s.client.Do(req)
10        if err != nil {
11            if isEOFError(err) && attempt < maxRetries {
12                lastErr = err
13                continue
14            }
15            return 0, nil, err
16        }
17        defer resp.Body.Close()
18
19        body, _ := io.ReadAll(resp.Body)
20        return resp.StatusCode, body, nil
21    }
22
23    return 0, nil, fmt.Errorf("failed after retries: %w", lastErr)
24}
25
26// EOF 有多種形式,需要一併處理
27func isEOFError(err error) bool {
28    if errors.Is(err, io.EOF) {
29        return true
30    }
31    return strings.Contains(err.Error(), "EOF")
32}

兩個方向互補:縮短 idle timeout 降低發生機率,retry 確保萬一發生時能自動恢復,對呼叫方完全透明。


總結

項目變更效益
Memory 調整128MB → 240MB,timeout 30s → 10sRuntime -45%,成本僅增 2.6%
Secrets ManagerSDK fallback + Extension 升版消除偶發性啟動失敗
Connection PoolingidleConnTimeout 90s → 5s,加入 EOF retry消除因 idle 連線被關閉造成的請求失敗

這三個調校沒有一個是大改動,但每一個解決的都是真實發生過的問題。Lambda 的「簡單」有時候是一種假象——底層的連線管理、Extension 機制、計費模型,還是需要真正理解才能用得好。