はじめに
こんにちは、DevOps グループの中島です。
AWS を利用していれば、誰もが AWS CLI にもお世話になったことがあると思います。
ただ、AWS CLI (あるいは API) ってレスポンスが早いとは言えないところがありますよね。
1 回呼び出すくらいなら良いですが、何回も呼び出すとなるとその遅さが気になります。
そこで今回はまとめて大量に呼び出す場合に、どのようにしたら早く結果を得られるかについて調査してみました。
並列に呼び出す
早く結果を得る方法といっても、 内部で AWS の API を呼び出しているだけなので、
やれることとしては並列に呼び出すくらいしかありません。(当然アプリケーションの要件として並列に呼び出せる場合に限ります)
以下のように xargs のオプションで並列に実行してみます。
$ cat tables.txt table-name1 table-name2 table-name3 . . # for で回す $ for table_name in `cat tables.txt` do aws dynamodb describe-table --table-name ${table_name} > ${table_name}.json done # xargs を使い、 5 並列で実行する $ cat tables.txt | xargs -P 5 -I {} \ sh -c 'aws dynamodb describe-table --table-name {} > {}.json'
xargs を使ったほうは、-P オプション (同時実行数) で指定した多重度で実行され、for で回すよりも 5 倍くらい早く実行が終わります。では同時実行数に 9999 と指定すれば 9999 倍速で終わるのかというと、そういうわけではありません。
API の呼び出し頻度の制限に引っかかります。
API の呼び出し制限
例えば、EC2 の場合は ドキュメント に制限のされ方が明記されています。
それによると、Token bucket algoritm と呼ばれる方法で制限しているようです。
細かい内容はドキュメントを参照いただきたいのですが、おおまかには以下のようになります。
- Bucket には 1 秒ごとに Token が規定数供給される (Bucket に入る Token 数には上限がある)
- API を呼び出したとき、 Bucket から Token を 1 つ消費する
- Token が Bucket になかったら API は呼び出せずにエラーとなる
ということで、同時に大量に API を呼び出すとエラーとなってしまう仕様です。
するとうまくリトライしてやることを考えるわけですが、AWS CLI 自体にその機構が備わっています。
AWS CLI のリトライ
こちらも ドキュメント が存在します。
Legacy, Standard, Adaptive とリトライモードが紹介されているうちの標準的と思われる Standard を確認してみると以下のようにあります。
Any retry attempt will include an exponential backoff by a base factor of 2.
Exponential backoff は AWSのドキュメント にも記載があるとおり、
失敗するたびにリトライまでの時間を指数関数的に伸ばしていくアルゴリズムです。
上記 AWSのドキュメント にある疑似コードにならうと、100, 200, 800... ミリ秒待ってから再度実行される形になります。
しかしよく考えてみると xargs で並列実行した場合ほぼ同時に初回アクセスされるので、
そのときエラーになったリクエストが同じ時間待ってまたほぼ同時にアクセスすることになってしまい、あまり効率的にリトライされているとはいえません。
そのため、同 AWS のドキュメントにも以下のように記載があり、一般的にはランダムにディレイを設けるような実装がされているようです。
Most exponential backoff algorithms use jitter (randomized delay) to prevent successive collisions.
それでは AWS CLI ではどのようになっているのか、調べてみましょう。
https://github.com/aws/aws-cli の v2 ブランチに 以下のような処理 があります。
class ExponentialBackoff(BaseRetryBackoff): _BASE = 2 _MAX_BACKOFF = 20 def __init__(self, max_backoff=20, random=random.random): self._base = self._BASE self._max_backoff = max_backoff self._random = random def delay_amount(self, context): """Calculates delay based on exponential backoff. This class implements truncated binary exponential backoff with jitter:: t_i = min(rand(0, 1) * 2 ** attempt, MAX_BACKOFF) where ``i`` is the request attempt (0 based). """ # The context.attempt_number is a 1-based value, but we have # to calculate the delay based on i based a 0-based value. We # want the first delay to just be ``rand(0, 1)``. return min( self._random() * (self._base ** (context.attempt_number - 1)), self._max_backoff )
2^リトライ回数
の値に random()
をかけ算しているので、結構大きな範囲で待ちが発生する実装となっているようです。これなら xargs で同時並列に実行しても大きな問題にはならないでしょう。
一方で StepFunctions のドキュメント にはランダムなディレイについては記載がありませんし、GCP のドキュメント に記載の例では 2^リトライ回数
に対してランダムな数値を足し算する方法も紹介されています。
おわりに
AWS でリトライといえば Exponential Backoff と記憶している方も多いと思いますが、
今回はその細かい挙動を確認することができました。
最後までご覧いただきありがとうございました。
We're hiring!