LambdaがS3からGetObjectするのにかかる時間を計測してみた [追記, 修正あり]

こんにちは、CX事業本部の夏目です。

サーバーレスアプリケーションの設計をしていたとき、めったに更新しないマスタデータならS3に置くのもありじゃねって思ったので、LambdaがS3からデータを落とすのにかかる時間を調べてみました。

2019/10/10 追記

はてブのコメントで「get_object で取得したデータのストリームに触れてないし、ファイルハンドラを作ってるだけでデータサイズ分読めてないんじゃないかな」「既にブコメにあるようにget_objectの戻り値にあるStreamingBodyをどげんかせんといかん気がする。」と指摘されました。

実際そのとおりで、StreamingBodyから値を受け取って長さを求める処理を入れたところ1GBのデータをダウンロードするのに15秒ほどかかりました。

間違ったデータを提供してしまい、申し訳ありません。

計測に使ったコードを修正し、測定結果はすべて差し替えました

試した環境

  • 東京リージョン
  • イベント実行でLambdaを500回動かして確かめた

Lambdaのメモリ量の寄与

条件

  • 10MBのデータをダウンロードさせてみた

結果

メモリサイズ(MB) 試行回数(秒) 平均値(秒) 中央値(秒) 最大値(秒) 最小値(秒)
3008 500 0.151557904 0.1410165 0.641314 0.122447
1024 500 0.160976518 0.148138 1.146616 0.122413
512 500 0.20648305 0.20103949999999998 0.483012 0.15911
256 500 0.38046901 0.36134299999999997 0.680586 0.301635
128 500 0.716364562 0.68135 1.379574 0.541196

落としてくるデータ量の寄与

メモリサイズ 512MBのとき

落としてきたデータサイズ 試行回数 平均値(秒) 中央値(秒) 最大値(秒) 最小値(秒)
100MB 500 1.361732588 1.314912 2.53633 1.283921
10MB 500 0.20648305 0.20103949999999998 0.483012 0.15911
128KB 500 0.070214688 0.064704 1.248748 0.040343
1KB 500 0.05961791 0.0573585 0.164185 0.035138

メモリサイズ 3008MBのとき

落としてきたデータサイズ 試行回数 平均値(秒) 中央値(秒) 最大値(秒) 最小値(秒)
1GB 500 13.39081825 13.4641925 22.006347 12.71388
100MB 500 1.145742728 1.13311 1.756455 1.12501
10MB 500 0.151557904 0.1410165 0.641314 0.122447
128KB 500 0.05326789 0.0440465 1.07839 0.025528
1KB 500 0.039890174 0.0355075 0.636321 0.020258

まとめ

予想に反して、落とすデータ量よりもLambdaのメモリ量の方が影響が大きそうだった。
それでも、メモリ量128MBでも10MBのデータを1秒かからずにダウンロードできたので、頻繁に更新しないマスタデータをS3に置いてもレスポンスが悪くなることはほぼなさそう。

2019/10/10 追記

コメントを受けてあらためて測定したのですが、500件程度ではサンプル数が足りなかったかもなぁといった感じでした。
(一部データでは再測定前よりも平均値が短くなっているものもあったので)

特に、メモリ量の寄与はおもったよりもしっかりと結果にあらわれました。

でもまぁ、128MBのメモリのLambdaで10MBのデータをダウンロードしても1秒かからずに取得することができたので、自分としては満足のいく結果です。

おまけ - 測定に使用したLambdaのコード -

S3 BucketやKey, 結果を流し込むためのKinesisは環境変数を使って渡した。

import boto3
import os
from datetime import datetime, timezone
from uuid import uuid4
import json


def get_bucket_name() -> str:
    return os.environ['TARGET_BUCKET_NAME']


def get_key_name() -> str:
    return os.environ['TARGET_KEY_NAME']


def get_region_name() -> str:
    return os.environ['AWS_REGION']


def get_stream_name() -> str:
    return os.environ['OUTPUT_STREAM_NAME']


def get_memory_size(context) -> str:
    return str(context.memory_limit_in_mb)


def measure_download_time() -> float:
    s3 = boto3.client('s3')
    bucket = get_bucket_name()
    key = get_key_name()
    start = datetime.now()
    resp = s3.get_object(Bucket=bucket, Key=key)
    binary = resp['Body'].read()
    size = len(binary)
    end = datetime.now()
    delta = end - start
    return delta.total_seconds()


def create_result_data(time: float, memory_size: str) -> bytes:
    key = get_key_name()
    region = get_region_name()

    return json.dumps({
        'id': str(uuid4()),
        'key': key,
        'region': region,
        'memory_size': memory_size,
        'download_time': time,
        'timestamp': datetime.now(timezone.utc).timestamp()
    }).encode()


def put_kinesis(data: bytes) -> None:
    option = {
        'PartitionKey': f'k-{uuid4()}',
        'StreamName': get_stream_name(),
        'Data': data
    }
    kinesis = boto3.client('kinesis')
    kinesis.put_record(**option)


def lambda_handler(event, context) -> None:
    download_time = measure_download_time()
    memory_size = get_memory_size(context)
    data = create_result_data(download_time, memory_size)
    put_kinesis(data)