管理者APIキーでスロットリング制限を回避する方法

セルフホストインスタンスへのAPIリクエストで、「429 Too Many Requests」というメッセージが表示されます。これは、以下の設定を行っても発生します。

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE を 600 に増やしました。
    • これは app.yml の env セクションで設定し、./launcher rebuild を実行して、再構築されたコンテナで変数が設定されていることを確認しました。
    • これは、試行している毎分あたりのリクエスト数よりもはるかに多い数です。
  • 無制限の管理者APIキーを使用しています。

DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE を変更しても機能しない理由について、明確な回答がないまま議論されてきたようです。

管理者キー/ユーザーを使用したAPIリクエストがスロットリングの対象にならないようにするにはどうすればよいですか?

Hi @aas

いくつか状況を教えていただけますか?

  • APIリクエストはどのくらいの頻度で行っていますか?1秒あたり、1分あたり、1時間あたり、1日あたり
  • Admin APIキーを使用していることは確かですか?
  • これらはすべて同じIPアドレスから来ていますか?リバースプロキシなどによるものですか?

nginxやその他のソフトウェアがそのエラーであなたにヒットしている可能性はありますか?

「いいね!」 1

Basさん、こんにちは。

返信が遅くなり申し訳ありません。

Discourse連携をローンチしたため、レート制限に関連する問題が発生しないように、現在再度確認しています。

制限されていないことを確認するために、新しいキーでテストしました。明確にするために、管理者APIキーとは具体的に何を意味しますか?

以下の設定でキーを作成しました。

「APIキーには制限がなく、すべてのエンドポイントにアクセスできます。」と表示されています。

ローカルのPythonシェルからAPIリクエストを行ってテストしているため、これらはすべて同じIPアドレスから来ています。サーバーでスクリプトを実行した際にもレート制限に遭遇しました。その場合、すべてのリクエストは同じIPアドレスから来ていました。

以下のコードでレート制限にヒットすることを確認しました。

async def get_topic_post_stream(topic_id):
    url = f"{DISCOURSE_URL}/t/{topic_id}"
    async with httpx.AsyncClient(headers=HEADERS) as client:
        topic = await client.get(url)
    return topic.status_code


async def get_topic_post_streams(topic_ids):
    tasks = [functools.partial(get_topic_post_stream, topic_id) for topic_id in topic_ids]
    topics = await aiometer.run_all(
        tasks,
        # max_per_second=1,
        )
    return topics

# topic_idsのトピックから15個のスライスを取得してテストします。
topics = asyncio.run(get_topic_post_streams(topic_ids[:15]))

max_per_second パラメータはコメントアウトされており、リクエスト数に制限がないことに注意してください。

これは2.05秒で完了し、15件のリクエストのうち2件が 429 を返します。

max_per_second=1 で実行すると、すべて正常に完了します。

さらに詳細が必要な場合はお知らせください。ありがとうございます。

@Bas、これはPythonコードのJavaScript相当のもので、開発者ツールコンソールを使用して再現しやすくしたものです。

const DISCOURSE_URL = '';
const HEADERS = {
    'Api-Key': '',
    'Api-Username': '',
    'Content-Type': 'application/json'
};


const topicIds = Array.from({ length: 100 }, (_, i) => i + 1);

async function getTopicPostStream(topicId) {
    const url = `${DISCOURSE_URL}/t/${topicId}`;
    const response = await fetch(url, { headers: HEADERS });
    return response.status;
}

async function getTopicPostStreams(topicIds) {
    const results = await Promise.all(topicIds.map(topicId => getTopicPostStream(topicId)));
    return results;
}

// リクエストのレート制限を行わず、429が2回返されることを確認します。
(async () => {
    const topics = await getTopicPostStreams(topicIds.slice(0, 15));
    console.log(topics);
})();

async function getTopicPostStreamsRateLimited(topicIds) {
    const results = [];
    for (const topicId of topicIds) {
        const result = await getTopicPostStream(topicId);
        results.push(result);
        await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒遅延
    }
    return results;
}

// 1秒あたり1リクエストで、すべて200が返されます
(async () => {
    const topics = await getTopicPostStreamsRateLimited(topicIds.slice(0, 15));
    console.log(topics);
})();
「いいね!」 1

推測するに、これが最も可能性の高い問題です。APIではなく、IPアドレスに基づいてレート制限されています。

こちらでIPごとの設定を確認できます: Available settings for global rate limits and throttling - #27

それが実際に問題の解決につながるかどうか教えてください :slight_smile:

「いいね!」 1

ありがとうございます、@Bas さん!

投稿で言及されている設定に関係なく、これらの 429 エラーを受け取るべきではないように思えます。提供した例では、デフォルトの API 制限を下回る 15 件のリクエストを送信しました。これは管理者 API キーとユーザー名を使用して行いました。

例では、以下の IP ごとのデフォルト制限を超えていません。

管理者以外の制限さえ超えていません。

DISCOURSE_MAX_REQS_PER_IP_MODEwarn または none に変更しても効果はありませんでした。

何か見落としていることはありますか?:thinking:

ちなみに、設定は app.yml を編集し、./launcher destroy app && ./launcher start app を実行して変更しました。

/var/log/nginx/access.log で IP アドレスが正しいことが確認できるため、Discourse がすべてのリクエストを同じ IP からのものとして扱っているとは思いません。

管理画面でもユーザーの IP アドレスを確認できます。

変更した設定は以下のとおりです。

  DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE: 1200
  DISCOURSE_MAX_USER_API_REQS_PER_MINUTE: 60
  DISCOURSE_MAX_REQS_PER_IP_MODE: none
  DISCOURSE_MAX_REQS_PER_IP_PER_10_SECONDS: 100
  DISCOURSE_MAX_REQS_PER_IP_PER_MINUTE: 400

編集:失敗したリクエストの応答内容を確認したところ、nginx に関する記述がありました。

<html>\r\n<head><title>429 Too Many Requests</title></head>\r\n<body>\r\n<center><h1>429 Too Many Requests</h1></center>\r\n<hr>\n<center>nginx</center>\r\n</body>\r\n</html>\r\n

nginx に関するトピックについて、さらに調査します。

「いいね!」 1

nginx の設定の関連する 2 つのセクションは次のようになります。

limit_req_zone $binary_remote_addr zone=flood:10m rate=12r/s;
limit_req_zone $binary_remote_addr zone=bot:10m rate=200r/m;
limit_req_status 429;
limit_conn_zone $binary_remote_addr zone=connperip:10m;
limit_conn_status 429;
server {
  listen 80;
  return 301 https://community.ankihub.net$request_uri;
}

および

  location @discourse {
add_header Strict-Transport-Security 'max-age=31536000'; # 証明書を 1 年間記憶し、このドメインの HTTPS への自動接続を有効にします
  limit_conn connperip 20;
  limit_req zone=flood burst=12 nodelay;
  limit_req zone=bot burst=100 nodelay;
    proxy_set_header Host $http_host;
    proxy_set_header X-Request-Start "t=${msec}";
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $thescheme;
    proxy_pass http://discourse;
  }
}

残りの質問は次のとおりです。

  • Discourse の設定に合わせて両方のセクションを編集する必要がありますか?それとも location @discourse の値だけですか?

  • これらの値を変更し、再構築後も変更を永続させる正しい方法はありますか?
    コンテナ内の nginx 設定を直接編集してから、コンテナを停止/開始できると想定しています。しかし、これらの値は元々 templates/web.ratelimited.template.yml から来ているようで、再構築時に上書きされる可能性がありますか?

ご協力ありがとうございます!:pray:

「いいね!」 1

うーん、それは私の専門分野から外れてしまいますね。\n\nnginxによってレート制限されているのであれば、それらの設定を変更して制限を緩めることは理にかなっています。NginxがIPアドレスをホワイトリストに登録できるかどうかはわかりません。

以下のようなものを使用します。

map $remote_addr $exclude_from_limit {
    default 0;
    192.168.1.1 1;
    192.168.1.2 1;
}

そして、制限を if で囲みます。

        if ($exclude_from_limit = 0) {
            limit_req zone=flood burst=24 nodelay;
            limit_req zone=bot burst=400 nodelay;
            limit_conn connperip 20;
        }

はい、ビルド中に pups を使用してこれを永続化する必要があります。アプローチについては、たとえば web.ssl.template.yml を参照してください。

または、これを忘れて、戦略的な場所に sleep を挿入して API クライアント スクリプトの実行を遅くすることもできます。 ← 推奨されるアプローチ

「いいね!」 4

レート制限がかかったときにrescueで実行するなど。それが私が通常行うことだと思います。

「いいね!」 1

標準インストールでの1分あたりまたは1秒あたりのAPIリクエストの最大持続レートはいくつですか?

1秒あたり1回試しましたが、制限に達したようです。一方、7秒あたり1回の要求は正常に機能します。