RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

AWS CLI を大量に呼び出す

はじめに

こんにちは、DevOps グループの中島です。

AWS を利用していれば、誰もが AWS CLI にもお世話になったことがあると思います。
ただ、AWS CLI (あるいは API) ってレスポンスが早いとは言えないところがありますよね。
1 回呼び出すくらいなら良いですが、何回も呼び出すとなるとその遅さが気になります。
そこで今回はまとめて大量に呼び出す場合に、どのようにしたら早く結果を得られるかについて調査してみました。

並列に呼び出す

早く結果を得る方法といっても、 内部で AWSAPI を呼び出しているだけなので、
やれることとしては並列に呼び出すくらいしかありません。(当然アプリケーションの要件として並列に呼び出せる場合に限ります)
以下のように 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 backoffAWSのドキュメント にも記載があるとおり、
失敗するたびにリトライまでの時間を指数関数的に伸ばしていくアルゴリズムです。

上記 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!

rarejob-tech.co.jp