「大規模盛土造成地」の判定関数で
geojsonファイルを扱うlambdaを作っていて、「AWS Lambdaのメモリは少なければ少ないほどいいとも限らない」ことを学んだので、自分用の備忘録として残しておきます。
作成しているlambda関数は、座標を指定して、国が公表している「大規模盛土造成地」に該当するかを判定するもの。
この「大規模盛土造成地」については、国土数値情報で該当する範囲がデータとして提供されています。
今回、形式はgeojsonを選択。可読性もあり、読み込みのためのライブラリも小さくて済むのでlambda向きだろうと考えての選択です。
試しに、ダウンロードしたgeojsonを地理院地図上に描画させてみるとこんな感じに大規模盛土造成地が地図上にマッピングされます。
このデータを使って、指定した座標がgeojsonで示されるエリア内かどうかを判定しよう、という関数を作成していきます。
判定コード
ClaudeCodeと相談しながら書いた(書かせた?)コードはこんな感じ。
大規模盛土造成地情報はAPIでは取得できないようなので、ダウンロードしたファイルをS3に格納して読み取っています。
他の防災情報はAPIで取得できるのに、なぜ大規模盛土造成地情報はAPI公開されていないのか…
また、大規模盛土造成地はそのデータ作成の経緯的に精度が出せず、座標ピンポイントだと誤判定が起きるため、指定された座標の周辺100mまで範囲を拡大して判定しています。
生のgeojsonを当たり判定していくと効率が悪いので、いったんR-tree空間インデックスに変換した上で探索する、という手法を取っています。
def get_large_scale_filled_land_info_from_geojson(
lat: float, lon: float, high_precision: bool = False
) -> dict:
"""
国土地理院の大規模盛土造成地情報をS3から取得し、中心点と半径100m以内の最大値を取得する。
R-treeインデックスを使用して高速化。
大規模盛土造成地は2値(あり/なし)なので、"あり"が見つかり次第早期終了する。
"""
start_time = time.time()
print(
f"[DEBUG] get_large_scale_filled_land_info_from_geojson 開始: lat={lat}, lon={lon}, high_precision={high_precision}"
)
num_search_points = 8 if high_precision else 4
search_points = _get_points_in_radius(lat, lon, 100, num_search_points)
max_info = {"description": "情報なし", "weight": 0}
center_info = {"description": "情報なし", "weight": 0}
found_any = False # 早期終了フラグ
# 都道府県別にグループ化して処理を最適化
pref_groups = {}
for i, (p_lat, p_lon) in enumerate(search_points):
pref_code = geocoding.get_pref_code(p_lat, p_lon)
if pref_code not in pref_groups:
pref_groups[pref_code] = []
pref_groups[pref_code].append((i, p_lat, p_lon))
# 中心点を最初に処理するため、中心点を含む都道府県を優先
center_pref_code = geocoding.get_pref_code(search_points[0][0], search_points[0][1])
pref_order = [center_pref_code] + [
code for code in pref_groups.keys() if code != center_pref_code
]
for pref_code in pref_order:
if found_any:
print(
f"[DEBUG] 早期終了: 既に'あり'が見つかったため、都道府県 {pref_code} の処理をスキップ"
)
break
points = pref_groups[pref_code]
pref_start_time = time.time()
# S3からGeoJSONファイルを取得
s3_key = f"{S3_LARGE_FILL_LAND_FOLDER}/{S3_LARGE_FILL_LAND_FILE_PREFIX}{pref_code}.geojson"
try:
geojson_start_time = time.time()
geojson = geojsonhelper.load_large_geojson(
S3_LARGE_FILL_LAND_BUCKET, s3_key
)
geojson_load_time = time.time() - geojson_start_time
print(
f"[DEBUG] 都道府県 {pref_code}: GeoJSON読み込み時間 = {geojson_load_time:.3f}秒"
)
if not geojson:
continue
# R-treeインデックスをキャッシュから取得または新規作成
rtree_start_time = time.time()
if pref_code not in _rtree_cache:
rtree_idx, features = _build_rtree_index(geojson)
_rtree_cache[pref_code] = (rtree_idx, features)
print(
f"[DEBUG] 都道府県 {pref_code}: R-treeインデックス構築完了 (features数={len(features)})"
)
else:
rtree_idx, features = _rtree_cache[pref_code]
print(
f"[DEBUG] 都道府県 {pref_code}: R-treeインデックスをキャッシュから取得"
)
rtree_build_time = time.time() - rtree_start_time
print(
f"[DEBUG] 都道府県 {pref_code}: R-tree準備時間 = {rtree_build_time:.3f}秒"
)
# 中心点を最初に処理するため、ポイントを並び替え
center_points = [(i, p_lat, p_lon) for i, p_lat, p_lon in points if i == 0]
other_points = [(i, p_lat, p_lon) for i, p_lat, p_lon in points if i != 0]
ordered_points = center_points + other_points
# 各ポイントをR-treeで検索
for i, p_lat, p_lon in ordered_points:
if found_any:
print(
f"[DEBUG] 早期終了: Point {i+1} の処理をスキップ(既に'あり'が見つかっている)"
)
break
point_start_time = time.time()
is_center_point = i == 0
point = Point(p_lon, p_lat)
current_info = {"description": "情報なし", "weight": 0}
search_start_time = time.time()
if _search_with_rtree(point, rtree_idx, features):
current_info = {"description": "あり", "weight": 1}
found_any = True # "あり"が見つかったので早期終了フラグを設定
print(
f"[DEBUG] Point {i+1}/{len(search_points)}: 大規模盛土造成地'あり'を発見!"
)
search_time = time.time() - search_start_time
print(
f"[DEBUG] Point {i+1}/{len(search_points)}: R-tree検索時間 = {search_time:.3f}秒"
)
if is_center_point:
center_info = current_info
if current_info["weight"] > max_info["weight"]:
max_info = current_info
point_total_time = time.time() - point_start_time
print(
f"[DEBUG] Point {i+1}/{len(search_points)}: 合計処理時間 = {point_total_time:.3f}秒"
)
# "あり"が見つかったら即座に終了
if found_any:
print(
f"[DEBUG] 早期終了: 大規模盛土造成地'あり'が見つかったため処理を終了"
)
break
except Exception as e:
print(
f"Error fetching large scale filled land info for pref {pref_code}: {e}"
)
# エラーの場合、この都道府県の全ポイントを「情報なし」として処理
for i, p_lat, p_lon in points:
if i == 0: # 中心点の場合
center_info = {"description": "情報なし", "weight": 0}
pref_total_time = time.time() - pref_start_time
print(
f"[DEBUG] 都道府県 {pref_code}: 都道府県別処理時間 = {pref_total_time:.3f}秒"
)
total_time = time.time() - start_time
skipped_message = " (早期終了により一部処理をスキップ)" if found_any else ""
print(
f"[DEBUG] get_large_scale_filled_land_info_from_geojson 完了: 総処理時間 = {total_time:.3f}秒{skipped_message}"
)
return {
"max_info": max_info["description"],
"center_info": center_info["description"],
}
パフォーマンスが出ない
さて、テスト的にいくつかの座標で実行してみたところ、特に神奈川県でパフォーマンスが出ない。
改めてダウンロードしたgeojsonを見ると、神奈川県だけべらぼうにファイルサイズが大きいようです。
上の画像は首都圏とその近郊のgeojsonファイル一覧です。同じ東京通勤圏である埼玉県(A54-23_11)や千葉県(A54-23_12)に比べて、神奈川県(A54-23_14)のファイルサイズが群を抜いていることが分かります。
実際に実行して時間を計測してみました。メモリ割り当ては無難に256MBで始めています。
[DEBUG] get_large_scale_filled_land_info_from_geojson 開始: lat=35.273495, lon=139.585183, high_precision=False
[DEBUG] 都道府県 14: GeoJSON読み込み時間 = 3.746秒
[DEBUG] 都道府県 14: R-treeインデックス構築完了 (features数=6277)
[DEBUG] 都道府県 14: R-tree準備時間 = 5.058秒
[DEBUG] Point 1/5: R-tree検索時間 = 0.021秒
[DEBUG] Point 1/5: 合計処理時間 = 0.021秒
[DEBUG] Point 2/5: 大規模盛土造成地'あり'を発見!
[DEBUG] Point 2/5: R-tree検索時間 = 0.000秒
[DEBUG] Point 2/5: 合計処理時間 = 0.000秒
[DEBUG] 早期終了: 大規模盛土造成地'あり'が見つかったため処理を終了
[DEBUG] 都道府県 14: 都道府県別処理時間 = 8.826秒
[DEBUG] get_large_scale_filled_land_info_from_geojson 完了: 総処理時間 = 9.732秒 (早期終了により一部処理をスキップ)
{'coordinates': {'latitude': 35.273495, 'longitude': 139.585183}, 'source': '座標: 35.273495, 139.585183 (入力座標系: wgs84)', 'input_type': 'latlon', 'datum': 'wgs84', 'requested_hazard_types': ['earthquake', 'flood', 'tsunami', 'high_tide', 'landslide', 'large_fill_land'], 'hazard_info': {'jshis_prob_50': {'max_prob': 0.764667, 'center_prob': 0.757195}, 'jshis_prob_60': {'max_prob': 0.069295, 'center_prob': 0.066908}, 'inundation_depth': {'max_info': '浸水なし', 'center_info': '浸水なし'}, 'tsunami_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'hightide_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'large_fill_land': {'max_info': 'あり', 'center_info': '情報なし'}, 'landslide_hazard': {'debris_flow': {'max_info': '該当なし', 'center_info': '該当なし'}, 'steep_slope': {'max_info': '該当なし', 'center_info': '該当なし'}, 'landslide': {'max_info': '該当なし', 'center_info': '該当なし'}}}, 'status': 'success'}
END RequestId: 98772a62-1814-48eb-9d96-7a58eca16516
REPORT RequestId: 98772a62-1814-48eb-9d96-7a58eca16516 Duration: 10773.03 ms Billed Duration: 10774 ms Memory Size: 256 MB Max Memory Used: 184 MB Init Duration: 882.96 ms
メモリ使用量は184MBで、割り当てメモリ内に収まってはいますが、実行時間が10秒を超えており、体感としてもちょっと"待たされる"感があります。
Lambdaはメモリ割り当て量に応じてCPUのスペックが変わる
実は、Lambdaではメモリの割り当て量に応じてCPUのスペックが変わるという仕様があります。
AWS Lambda のリソースモデルでは、お客様が関数に必要なメモリ量を指定すると、それに比例した CPU パワーとその他のリソースが割り当てられます。
重ための処理にはメモリ割り当て増やすとパフォーマンス上がるよ、とも書いてありますね。
インポートされたライブラリ、Lambda レイヤー、Amazon Simple Storage Service (Amazon S3)、または Amazon Elastic File System (Amazon EFS) を使用する関数では、メモリ割り当てを増やすことでパフォーマンスが向上します。
出典:Lambda関数のメモリを構成する (Google翻訳による日本語訳)
ということで、メモリを512MB, 1024MBに増量して、実行時間を計測してみることにしました。
どのテストも同じ座標、コールドスタートでの同条件です。
割り当てメモリ512MB
メモリを256MBから512MBに倍増させたところ、S3読み込み時間が3.7秒→1.8秒へと半減、R-treeの初期化時間も5秒から2.4秒へとほぼ倍のパフォーマンスとなりました。総実行時間も10.8秒→6.2秒とかなり良い感じです。
[DEBUG] get_large_scale_filled_land_info_from_geojson 開始: lat=35.273495, lon=139.585183, high_precision=False
[DEBUG] 都道府県 14: GeoJSON読み込み時間 = 1.837秒
[DEBUG] 都道府県 14: R-treeインデックス構築完了 (features数=6277)
[DEBUG] 都道府県 14: R-tree準備時間 = 2.364秒
[DEBUG] Point 1/5: R-tree検索時間 = 0.019秒
[DEBUG] Point 1/5: 合計処理時間 = 0.019秒
[DEBUG] Point 2/5: 大規模盛土造成地'あり'を発見!
[DEBUG] Point 2/5: R-tree検索時間 = 0.000秒
[DEBUG] Point 2/5: 合計処理時間 = 0.000秒
[DEBUG] 早期終了: 大規模盛土造成地'あり'が見つかったため処理を終了
[DEBUG] 都道府県 14: 都道府県別処理時間 = 4.221秒
[DEBUG] get_large_scale_filled_land_info_from_geojson 完了: 総処理時間 = 5.398秒 (早期終了により一部処理をスキップ)
{'coordinates': {'latitude': 35.273495, 'longitude': 139.585183}, 'source': '座標: 35.273495, 139.585183 (入力座標系: wgs84)', 'input_type': 'latlon', 'datum': 'wgs84', 'requested_hazard_types': ['earthquake', 'flood', 'tsunami', 'high_tide', 'landslide', 'large_fill_land'], 'hazard_info': {'jshis_prob_50': {'max_prob': 0.764667, 'center_prob': 0.757195}, 'jshis_prob_60': {'max_prob': 0.069295, 'center_prob': 0.066908}, 'inundation_depth': {'max_info': '浸水なし', 'center_info': '浸水なし'}, 'tsunami_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'hightide_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'large_fill_land': {'max_info': 'あり', 'center_info': '情報なし'}, 'landslide_hazard': {'debris_flow': {'max_info': '該当なし', 'center_info': '該当なし'}, 'steep_slope': {'max_info': '該当なし', 'center_info': '該当なし'}, 'landslide': {'max_info': '該当なし', 'center_info': '該当なし'}}}, 'status': 'success'}
END RequestId: 7748b919-da86-40ad-b573-cedb7caa0cf2
REPORT RequestId: 7748b919-da86-40ad-b573-cedb7caa0cf2 Duration: 6179.64 ms Billed Duration: 6180 ms Memory Size: 512 MB Max Memory Used: 186 MB Init Duration: 832.19 ms
でも、できれば5秒以内にレスポンスが返ってきてくれればうれしいところですが…
割り当てメモリ1024MB
ということで、メモリを倍プッシュの1024MBに増量し、同条件で再テストしてみました。
すると、S3読み込みも、R-tree初期化もさらに高速化し、総実行時間も3.9秒と希望通りに収まりました。
[DEBUG] get_large_scale_filled_land_info_from_geojson 開始: lat=35.273495, lon=139.585183, high_precision=False
[DEBUG] 都道府県 14: GeoJSON読み込み時間 = 0.996秒
[DEBUG] 都道府県 14: R-treeインデックス構築完了 (features数=6277)
[DEBUG] 都道府県 14: R-tree準備時間 = 1.178秒
[DEBUG] Point 1/5: R-tree検索時間 = 0.004秒
[DEBUG] Point 1/5: 合計処理時間 = 0.004秒
[DEBUG] Point 2/5: 大規模盛土造成地'あり'を発見!
[DEBUG] Point 2/5: R-tree検索時間 = 0.000秒
[DEBUG] Point 2/5: 合計処理時間 = 0.000秒
[DEBUG] 早期終了: 大規模盛土造成地'あり'が見つかったため処理を終了
[DEBUG] 都道府県 14: 都道府県別処理時間 = 2.179秒
[DEBUG] get_large_scale_filled_land_info_from_geojson 完了: 総処理時間 = 3.218秒 (早期終了により一部処理をスキップ)
{'coordinates': {'latitude': 35.273495, 'longitude': 139.585183}, 'source': '座標: 35.273495, 139.585183 (入力座標系: wgs84)', 'input_type': 'latlon', 'datum': 'wgs84', 'requested_hazard_types': ['earthquake', 'flood', 'tsunami', 'high_tide', 'landslide', 'large_fill_land'], 'hazard_info': {'jshis_prob_50': {'max_prob': 0.764667, 'center_prob': 0.757195}, 'jshis_prob_60': {'max_prob': 0.069295, 'center_prob': 0.066908}, 'inundation_depth': {'max_info': '浸水なし', 'center_info': '浸水なし'}, 'tsunami_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'hightide_inundation': {'max_info': '浸水想定なし', 'center_info': '浸水想定なし'}, 'large_fill_land': {'max_info': 'あり', 'center_info': '情報なし'}, 'landslide_hazard': {'debris_flow': {'max_info': '該当なし', 'center_info': '該当なし'}, 'steep_slope': {'max_info': '該当なし', 'center_info': '該当なし'}, 'landslide': {'max_info': '該当なし', 'center_info': '該当なし'}}}, 'status': 'success'}
END RequestId: 46d379a1-fcf4-49e7-acbc-4367fe866f6b
REPORT RequestId: 46d379a1-fcf4-49e7-acbc-4367fe866f6b Duration: 3882.52 ms Billed Duration: 3883 ms Memory Size: 1024 MB Max Memory Used: 184 MB Init Duration: 844.76 ms
メモリ割り当て量と実行時間のまとめ
今回は、Layerに5つのライブラリ(Shapely, request, boto3, pillow, rtree)を登録+S3アクセス+関数内でR-tree構築という重ためのLambdaということもあり、メモリ割り当て増によるCPUスペックの違いが結果に大きく影響しました。
以下に、メモリ割り当て量による実行時間の違いをまとめます。
メモリ割り当てが大きくなってもコールドスタート時間には大きな違いはなく、CPUスペックの違いがそのまま総所要時間の削減につながっていることが分かります。
追記:ちなみに、記事執筆後にメモリ1536MBまで増量して再計測してみましたが、処理単体では高速化は見られますが、おそらくオーバーヘッドなどの要因から総処理時間は頭打ちとなりました。
メモリ | 起動 | S3読み込み | インデックス構築 | 総所要時間 |
---|---|---|---|---|
256MB | 0.9秒 | 3.7秒 | 5.1秒 | 10.8秒 |
512MB | 0.8秒 | 1.8秒 | 2.4秒 | 6.2秒 |
1024MB | 0.8秒 | 1.0秒 | 1.2秒 | 3.9秒 |
1536MB | 0.9秒 | 0.7秒 | 0.8秒 | 3.9秒 |
コストとの兼ね合い
ただ、メモリ割り当て量を増やすと、コスト面が心配です。
2025年8月現在の価格ですが、AWS東京リージョン(x64)の場合、Lambdaの割り当てメモリ量に対する料金は以下のようになっています。
これを見ると、256MBと512MBでは料金に違いはない(?)ようなので、512MBまでは素直にメモリを増やした方がいいですね。
メモリ (MB) | 1 ミリ秒あたりの料金 |
---|---|
128 | USD 0.0000000021 |
512 | USD 0.0000000083 |
1,024 | USD 0.0000000167 |
1,536 | USD 0.0000000250 |
2,048 | USD 0.0000000333 |
あとは、1024MBまでスペックを増やした時にどのくらいのコスト増になるか、です。
先ほどのテスト結果で試算してみました。計算を単純化するため、Lambdaの実行時間に対するコストのみを計算しています。実際にはS3のコストなどもかかりますが、ここでは割愛しています。
計算してみると、256MBは単価が変わらないのに実行時間がながいことから結果的にコスト増につながっています。
そして、1024MBにメモリを増量した際のコストは、単価が倍増した代わりに実行時間が40%減となったので、結果的のコストは26%増加となりました。
今回の要件的には、応答時間5秒以内に収めたかったので、この程度であれば必要コストとして受け入れられそうです。
メモリ (MB) | ミリ秒単価 | 実行時間(ミリ秒) | 単価x時間 | 512MBに対する率 |
---|---|---|---|---|
256 | 0.0000000083 | 10773 | 0.000089416 | 174% |
512 | 0.0000000083 | 6180 | 0.000051294 | --- |
1024 | 0.0000000167 | 3883 | 0.000064846 | 126% |
まとめ
今回は、比較的重ためのLambda関数で、メモリ割り当てに伴うCPUスペック増加が、どの程度のパフォーマンス向上に繋がるかを実験してみました。
メモリ使用料が少ないLambda関数であっても、処理が重たい場合は、(予算内で)メモリ割り当てを積極的に増やした方がいいこともありそうです。