Ruby on JetsとRDBを組み合わせた場合のコネクションの使われ方を検証してみた


一般的にAWS LambdaとRDS(RDB)は相性が悪いと言われています。
その理由の一つは「Lambdaのスケールアウトに伴いRDSへのコネクションが増加し、最悪の場合コネクション枯渇するため 」です。

この問題はLambdaを多用するRuby on Jetsでも同じだと思っていましたが、
Ruby on JetsのDocumentには以下のような記載があります。

On AWS Lambda, there’s something called the Lambda Execution Context. The Lambda Execution Context gets reused between lambda function runs. Jets establishes the DB connection within the Lambda Execution Context outside the handler. So DB connections get reused between subsequent lambda function runs. This prevents DB connections from ever-increasing. The AWS docs specifically point out to use the Lambda Execution Context for things like establishing DB connections.

要約すると「Lambda実行コンテキストを使うことでコネクションを使いまわし、コネクションが増加することを防ぎます」ということです。

実際のところコネクションはどのように使い回されるのか、検証してみました。

検証環境

以下記事でAWSへデプロイしたアプリケーションを活用します。

Rubyのサーバレスフレームワーク「Ruby on Jets」を使ってAWSへアプリケーションをデプロイする

検証方法

今回の検証ではheyというgolang製のCLIを使用します。
以下のようなコマンドをローカルマシンで実行することで、指定した並列数で指定した回数のリクエストを出すことができます。

$ hey -c (並列数) -n (実行数) https://(ホスト名)/posts

コネクション数はCloudWatchのDatabaseConnectionsというメトリクスで確認することとします。

検証実施

10並列で10回リクエスト

手始めに10並列から始めてみることにしました。

$ hey -c 10 -n 10 https://y2ew64ccd4.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:    3.0347 secs
  Slowest:  3.0346 secs
  Fastest:  0.2670 secs
  Average:  2.6873 secs
  Requests/sec: 3.2952

  Total data:   16090 bytes
  Size/request: 1609 bytes

Response time histogram:
  0.267 [1] |■■■■
  0.544 [0] |
  0.821 [0] |
  1.097 [0] |
  1.374 [0] |
  1.651 [0] |
  1.928 [0] |
  2.204 [0] |
  2.481 [0] |
  2.758 [0] |
  3.035 [9] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■


Latency distribution:
  10% in 2.9394 secs
  25% in 2.9424 secs
  50% in 2.9449 secs
  75% in 2.9680 secs
  90% in 3.0346 secs
  0% in 0.0000 secs
  0% in 0.0000 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.1575 secs, 0.2670 secs, 3.0346 secs
  DNS-lookup:   0.0553 secs, 0.0537 secs, 0.0570 secs
  req write:    0.0001 secs, 0.0001 secs, 0.0003 secs
  resp wait:    2.5293 secs, 0.1207 secs, 2.8786 secs
  resp read:    0.0001 secs, 0.0001 secs, 0.0003 secs

Status code distribution:
  [200] 10 responses

実行結果はいずれも200で特に問題無さそうです。

しかし、CloudWatchでDBのコネクションを確認したところ、10まで増加していました。

やはり、 Lambdaの同時実行数分はコネクションを張る ことがわかりました。
一方で、 LambdaのコンテナがSTOPするまでコネクションが張られっぱなし という状況なので、
次のリクエストではコネクションを使いまわしてくれるかもしれません。

10並列で100回リクエスト

今度はリクエスト数を増やして、検証してみます。

$ hey -c 10 -n 100 https://y2ew64ccd4.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:    4.1221 secs
  Slowest:  2.9957 secs
  Fastest:  0.0878 secs
  Average:  0.2688 secs
  Requests/sec: 24.2597

  Total data:   160900 bytes
  Size/request: 1609 bytes

Response time histogram:
  0.088 [1] |
  0.379 [94]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.669 [0] |
  0.960 [0] |
  1.251 [0] |
  1.542 [0] |
  1.833 [0] |
  2.123 [0] |
  2.414 [0] |
  2.705 [0] |
  2.996 [5] |■■


Latency distribution:
  10% in 0.1039 secs
  25% in 0.1119 secs
  50% in 0.1216 secs
  75% in 0.1325 secs
  90% in 0.2468 secs
  95% in 2.9185 secs
  99% in 2.9957 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0126 secs, 0.0878 secs, 2.9957 secs
  DNS-lookup:   0.0030 secs, 0.0000 secs, 0.0309 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0002 secs
  resp wait:    0.2558 secs, 0.0875 secs, 2.8686 secs
  resp read:    0.0002 secs, 0.0001 secs, 0.0039 secs

Status code distribution:
  [200] 100 responses

実行結果はいずれも200で問題なし。
Lambdaのコンテナが立ち上がっていたためかレスポンスまでの時間が速いですね。

コネクション数は10でした。
つまり、 リクエストを逐次実行する場合には前のコネクションを使いまわしてくれている ことがわかります。

100並列で100回リクエスト

最後に100並列で100リクエストを実行してみました。
検証に使用しているRDSは t2.micro なのでおそらく 100並列できずにコネクションが枯渇する はずです。

$ hey -c 100 -n 100 https://y2ew64ccd4.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:    5.8446 secs
  Slowest:  5.8292 secs
  Fastest:  0.3307 secs
  Average:  3.0235 secs
  Requests/sec: 17.1097

  Total data:   146743 bytes
  Size/request: 1467 bytes

Response time histogram:
  0.331 [1] |■
  0.881 [18]    |■■■■■■■■■■■■■■■■■■
  1.430 [0] |
  1.980 [0] |
  2.530 [0] |
  3.080 [6] |■■■■■■
  3.630 [32]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  4.180 [40]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  4.730 [0] |
  5.279 [0] |
  5.829 [3] |■■■


Latency distribution:
  10% in 0.3975 secs
  25% in 3.1703 secs
  50% in 3.5691 secs
  75% in 3.7160 secs
  90% in 3.7609 secs
  95% in 3.7784 secs
  99% in 5.8292 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.2313 secs, 0.3307 secs, 5.8292 secs
  DNS-lookup:   0.0392 secs, 0.0350 secs, 0.0466 secs
  req write:    0.0001 secs, 0.0000 secs, 0.0004 secs
  resp wait:    2.7916 secs, 0.1158 secs, 5.6134 secs
  resp read:    0.0002 secs, 0.0000 secs, 0.0018 secs

Status code distribution:
  [200] 91 responses
  [502] 9 responses

いくつか502(Bad Gateway)が返ってきました。

コネクション数は82で頭打ちとなっていました。

Lambdaの同時実行数を10として100並列で100回リクエスト

AWS Lambdaの設定に「同時実行数」というものがあるので、これを10に設定しました。

同時実行数を設定しても冪等性を確保できるとは限らないとのことですが、
Lambdaの同時起動を抑制できればコネクションの枯渇は回避できるかもしれません。

$ hey -c 100 -n 100 https://y2ew64ccd4.execute-api.ap-northeast-1.amazonaws.com/dev/posts

Summary:
  Total:    0.3776 secs
  Slowest:  0.3767 secs
  Fastest:  0.3097 secs
  Average:  0.3345 secs
  Requests/sec: 264.8057

  Total data:   19330 bytes
  Size/request: 193 bytes

Response time histogram:
  0.310 [1] |■■
  0.316 [6] |■■■■■■■■■■
  0.323 [13]    |■■■■■■■■■■■■■■■■■■■■■■■
  0.330 [23]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.337 [20]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.343 [11]    |■■■■■■■■■■■■■■■■■■■
  0.350 [17]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.357 [2] |■■■
  0.363 [2] |■■■
  0.370 [1] |■■
  0.377 [4] |■■■■■■■


Latency distribution:
  10% in 0.3178 secs
  25% in 0.3251 secs
  50% in 0.3326 secs
  75% in 0.3438 secs
  90% in 0.3497 secs
  95% in 0.3680 secs
  99% in 0.3767 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.2676 secs, 0.3097 secs, 0.3767 secs
  DNS-lookup:   0.0030 secs, 0.0012 secs, 0.0035 secs
  req write:    0.0001 secs, 0.0000 secs, 0.0003 secs
  resp wait:    0.0667 secs, 0.0404 secs, 0.1289 secs
  resp read:    0.0001 secs, 0.0000 secs, 0.0004 secs

Status code distribution:
  [200] 10 responses
  [500] 90 responses

残念ながら同時実行数を超えたリクエストは、キューイングされずに500を返すようです。

まとめ

Ruby on JetsとRDS(RDB)を組み合わせて使う場合に考慮すべきことは以下の通りです。

  • 同時実行数分のコネクションが張られることを留意する必要がある
  • 同時実行されなければコネクションは使い回される
  • Lambdaの初回起動には若干(検証では3秒前後)の時間がかかる
  • Lambdaの同時実行数を超えたリクエストはキューイングされずに500を返す

少し残念な結果となってしまいましたが、これさえ留意しておけば運用に耐えうるものにはできそうです。