LambdaのINITフェーズではメモリ128MでもCPUパワーをフルに使える?!boost host CPUの動きを確認してみた

INITフェーズの最初の10秒間で利用可能なCPUパワーはメモリ割当とは無関係です
2024.01.22

CX事業本部@大阪の岩田です。

Lambdaのランタイムではメモリ割り当てに応じて利用可能なCPUパワーが変わることが知られており、メモリ割当1769Mから1vCPUのフルパワーが利用可能になります。私も以前は勘違いしていたのですが、このメモリ割り当てに比例してCPUパワーが変わるという挙動はINITフェーズにおいては適用されません。

このブログではLambdaのINITフェーズにおけるCPUパワーについて紹介&検証していきます。

boost host CPU

皆さんはboost host CPUという概念をご存知でしょうか?LambdaのINITフェーズにおいて、最初の10秒間に関してはメモリ割り当てに応じたvCPUではなく、ホストCPU容量のバーストが割り当てられるという仕様があり、この仕様を指してboost host CPUと呼ぶようです。

boost host CPUの概念については以下のような資料で解説されています

re: Postより

Lambda 関数を初期化すると、Lambda はホスト CPU 容量のバーストを最大 10 秒間割り当てます。

Java Lambda 関数のパフォーマンスを向上させる | AWS re:Post

AWS Lambda Performance Tuning Deep Diveより

re:Invent 2019のセッション「Best practices for AWS Lambda and Java」

これらの資料で解説されているboost host CPUの振る舞いが期待通りかを検証していきたいと思います。

環境

今回検証に利用した環境です

  • メモリ割当: 検証パターンに応じて128Mと1769Mを切り替え
  • CPUアーキテクチャ: arm64
  • タイムアウト:120秒
  • ランタイム: Python3.12
  • ランタイムバージョンARN: arn:aws:lambda:ap-northeast-1::runtime:5eaca0ecada617668d4d59f66bf32f963e95d17ca326aad52b85465d04c429f5

検証手順

色々なパターンでCPUバウンドな処理を実行し、実行にかかった所要時間を比較してみます。まずChtGPTにお願いしてCPUバウンドな処理を書いてもらいました。

def calculate_prime_numbers(limit):
    # 簡単な素数計算を模倣するCPUバウンドな処理
    primes = []
    for num in range(2, limit + 1):
        is_prime = all(num % i != 0 for i in range(2, int(num**0.5) + 1))
        if is_prime:
            primes.append(num)
    return primes

この関数を利用して、以下のようなLambdaのコードを準備します。

import time
import json

def calculate_prime_numbers(limit):
    start = time.perf_counter()
    primes = []
    for num in range(2, limit + 1):
        is_prime = all(num % i != 0 for i in range(2, int(num**0.5) + 1))
        if is_prime:
            primes.append(num)
    end = time.perf_counter()
    processing_time = end -start
    print(json.dumps({'processing_time': processing_time}))
    return primes

# 1.boost host CPUの恩恵を受けるパターン
calculate_prime_numbers(1000000)

# 2.INITフェーズだが、10秒経過したためboost host CPUの恩恵を受けられないパターン
# time.sleep(10)
# calculate_prime_numbers(1000000)

def lambda_handler(event, context):
    # 3.INVOKEフェーズのためboost host CPUの恩恵を受けられないパターン
    # calculate_prime_numbers(1000000)
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

コメントアウト箇所を切り替えながら、色々なパターンで所要時間を計測してみます。

boost host CPUの恩恵を受けるパターン

メモリ割り当て128MでINITフェーズ内でCPUバウンドな処理を実行するパターンです。以下の部分のコメントアウトを解除して実行します。

# 1.boost host CPUの恩恵を受けるパターン
calculate_prime_numbers(1000000)

calculate_prime_numbersの実行に要した時間は6.021214539秒でした。

INITフェーズだが、10秒経過したためboost host CPUの恩恵を受けられないパターン

メモリ割り当ては128のままで、以下のコメントアウトを解除して検証します。10秒のsleepを挟んでいるので、calculate_prime_numbersを実行する頃にはboost host CPUの恩恵が受けられなくなり、メモリ128Mに対応した貧弱なCPUパワーしか利用できないはずです。以下のようにコメントアウトを解除します。

time.sleep(10)
calculate_prime_numbers(1000000)

このパターンだとcalculate_prime_numbersの実行に要した時間は84.34408051400001秒でした。先程のパターンと比較して14倍程度の時間を要しています。

INVOKEフェーズのためboost host CPUの恩恵を受けられないパターン(メモリ128M)

次はメモリ割当128のままINVOKEフェーズ内でcalculate_prime_numbersを実行してみます。以下のようにコメントアウトを解除して実行します。

def lambda_handler(event, context):
    # 3.INVOKEフェーズのためboost host CPUの恩恵を受けられないパターン
    calculate_prime_numbers(1000000)

このパターンだとcalculate_prime_numbersの実行に要した時間は85.31145541300003秒でした。INITフェーズかつboost host CPUの恩恵無しのパターンと同程度の所要時間となっていることが分かります。

INVOKEフェーズのためboost host CPUの恩恵を受けられないパターン(メモリ1769M)

最後にメモリ割当を1769Mに変更し、先程と同様INVOKEフェーズ内でcalculate_prime_numbersを実行してみます。結果、calculate_prime_numbersの実行に要した時間は6.078802296999925}秒で、boost host CPUの恩恵を受けたパターンと同程度の所要時間でした。

計測結果のまとめ

計測結果をまとめると以下のようになりました。各パターン1回ずつしか計測していませんが、メモリ割当が128Mの場合でもboost host CPUの恩恵によってCPUバウンドな処理が高速に実行できていることが分かります。

boost host CPU 実行箇所 メモリ割り当て 所要時間
INITフェーズ 128M 約6.02秒
INITフェーズ 128M 約84.34秒
INVOKEフェーズ 128M 約85.31秒
INVOKEフェーズ 1769M 約6.08秒

まとめ

boost host CPUについて検証してみました。Lambdaのパフォーマンスチューニングやコールドスタートの改善に取り組む場合はboost host CPUの仕様について理解しておくと良いでしょう。

参考