boto3のresourceやclientのインスタンスを再利用してTCPやTLSのオーバーヘッドを削減しよう

boto3のresourceやclientのインスタンスを再利用することでTCPやTLSの処理オーバーヘッドを削減し、パフォーマンスを改善することが可能というお話です
2020.03.12

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

先日のブログでboto3のresourceclientは処理コストが高いということを書きました。

PythonのコードをプロファイルしてLambdaのコストを最適化しよう!

上記のブログはコードをプロファイルする手法の紹介が主目的だったので詳細に触れませんでしたが、実際にはインスタンス作成の処理コストよりも気にすべきはNW関連のオーバーヘッドです。インスタンスを使いまわさない場合はTCPの3 WayハンドシェイクやTLSのネゴシエーション処理が無駄に発生し、パフォーマンスやコストに悪影響を及ぼします。このブログではNW関連のオーバーヘッドについてご紹介します。

パケットキャプチャで動作を確認する

ローカル環境で検証用のサンプルコードをパケットキャプチャしながら

  • インスタンスを使い回した場合
  • 使い回さない場合

それぞれで裏側の通信がどう変わるか比較してみます。

インスタンスを使い回す場合

まずインスタンスを使い回す場合です。

import boto3
dynamo = boto3.resource('dynamodb')
table = dynamo.Table('test')

for i in range(5):
    table.get_item(Key={'id': 1})

パケットキャプチャの結果です。TCPの3Wayハンドシェイクが1度しか実行されていないことが分かります。

インスタンスを使い回さない場合

続いてインスタンスを使い回さない場合です。

import boto3

for i in range(5):
  dynamo = boto3.resource('dynamodb')
  table = dynamo.Table('test')
  table.get_item(Key={'id': 1})

パケットキャプチャの結果です。今度はTCPの3WayハンドシェイクやTLSのネゴシエーション処理が何度も実行されていることが分かります。

無駄な処理が多いことが分かると思います。

計測してみる

実際にLambda実行環境で比較してみます。Python3.8、メモリ128Mの設定で以下のコードで処理時間を計測します。

インスタンスを使い回す場合

まずインスタンスを使い回す場合です。

import boto3
import time

dynamo = boto3.resource('dynamodb')
table = dynamo.Table('test')

def handler(event, context):

  start = time.perf_counter()
  for i in range(100):
    table.get_item(Key={'id': 1})
  end = time.perf_counter()
  print(end - start)  

平均すると大体3,600ms程度で処理が完了しました。

インスタンスを使い回さない場合

続いてインスタンスを使い回さない場合です。

import boto3
import time

def handler(event, context):

  start = time.perf_counter()
  for i in range(100):
    dynamo = boto3.resource('dynamodb')
    table = dynamo.Table('test')
    table.get_item(Key={'id': 1})
  end = time.perf_counter()
  print(end - start)

こちらは平均すると26,260ms程度という結果でした。実に22秒以上の差が出ています。今回はget_itemを100ループ回しているので、ざっくりですが22,000ms / 99回 → 1回のget_itemにつき222ms程度のパフォーマンス悪化を招いているようです。前回のブログの計測結果に基づくとboto3のresourceインスタンスを作成するのに40ms程度はかかるので、メモリ128Mの設定ではTCPの3WayハンドシェイクとTLSのネゴシエーション1回につき180ms程度のオーバーヘッドはかかっていそうです。

ちなみにTLSのネゴシエーション処理はCPUリソースが必要な処理なのでメモリ割当を増やせばオーバーヘッドはそこそこ改善します。Lambdaのメモリ割り当てを1,792M(フル vCPUが使える)まで上げると

  • インスタンスを使い回す場合:平均556ms
  • インスタンスを使い回さない場合:平均4,319ms

という結果でした。こちらは1ループあたりのオーバーヘッドが38ms程度という計算になります。

まとめ

boto3のresourceやclientのインスタンスを使い回すことで得られるパフォーマンス、コスト面でのメリットについてNW寄りの観点からご紹介しました。得られるメリットは大きいので、もし現状インスタンスを都度生成するような実装をしている場合はresourceやclientがシングルトンになるような実装への切り替えをオスススメします。