如何使用管理员API密钥避免速率限制?

我收到“429 Too Many Requests”消息,即使我已将自托管实例的 API 请求:

  • DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE 增加到 600
    • 我在 app.yml 的 env 部分设置了此项,然后运行了 ./launcher rebuild 并确认该变量已在重建的容器中设置。
    • 这远远超过了我尝试的每分钟请求数
  • 一个不受限制的管理员 API 密钥

似乎之前已经讨论过这个问题,但没有明确的答案说明为什么更改 DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE 似乎不起作用:

如何确保使用管理员密钥/用户的 API 请求不受限制?

您好 @aas

您能提供一些背景信息吗?

  • 您正在发出多少 API 请求?每秒、每分钟、每小时、每天
  • 您确定您使用的是管理员 API 密钥吗?
  • 所有这些请求都来自同一个 IP 地址吗?可能是由于反向代理?

会不会是 nginx 或其他软件给您返回了此错误?

1 个赞

您好 @Bas

抱歉回复延迟!

我现在正在重新审视这个问题,因为我们已经集成了 Discourse,并且希望确保不会遇到任何与速率限制相关的问题。

我用一个新密钥进行了测试,以确保它没有任何限制。明确地说,您所说的管理员 API 密钥具体指什么?

我创建了一个具有以下设置的密钥:

它显示,“API 密钥没有限制,所有端点均可访问。”

我正在通过从本地 Python shell 发出 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。
(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 个请求返回所有 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 错误。在我提供的示例中,我发送了 15 个请求,这低于所有默认 API 限制。我使用的是管理员 API 密钥和用户名完成的此操作。

该示例未超过以下每个 IP 的默认限制:

甚至未超过非管理员限制:

DISCOURSE_MAX_REQS_PER_IP_MODE 更改为 warnnone 均无效。

我是否遗漏了什么?: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 配置中两个相关部分似乎是:

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'; # remember the certificate for a year and automatically connect to HTTPS for this domain
  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 个赞

恐怕我现在要超出我的能力范围了。

如果你受到 nginx 的速率限制,那么调整这些设置并使其不那么严格是有意义的。我不确定 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 个赞

标准安装下的 API 请求每分钟或每秒的最大持续速率是多少?

我尝试过每秒一次,但似乎达到了限制。而每 7 秒一次请求则运行正常。