관리자 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 요청이 스로틀링(throttling) 대상이 되지 않도록 보장하는 방법은 무엇입니까?

aas님, 안녕하세요.

조금 더 맥락을 알려 주실 수 있을까요?

  • API 요청을 얼마나 많이 보내고 계신가요? 초당, 분당, 시간당, 하루 기준
  • 관리자용 API 키를 사용하고 계신지 확인하셨나요?
  • 이 모든 요청이 동일한 IP 주소에서 발생하고 있나요? 리버스 프록시 때문일 수도 있습니다.

nginx 또는 다른 소프트웨어가 해당 오류를 발생시키고 있는 것은 아닐까요?

Bas님, 안녕하세요.

늦게 답변드려 죄송합니다!

최근 Discourse 통합 기능을 출시했기 때문에, 속도 제한(rate limiting) 관련 문제가 발생하지 않도록 다시 확인 중입니다.

어떤 방식으로든 제한이 걸리지 않았는지 확인하기 위해 새 키로 테스트를 수행했습니다. 명확히 하자면, 'Admin 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 오류가 발생함을 확인하세요.
(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);
})();

추측컨대, 이것이 가장 가능성 높은 문제입니다. API 자체에서 속도 제한을 받는 것이 아니라, IP 주소 기준으로 제한을 받고 있는 것입니다.

아래 링크에서 IP별 설정을 확인해 보실 수 있습니다: Available settings for global rate limits and throttling - #27

이것이 실제로 문제를 해결했는지 알려주세요 :slight_smile:

고맙습니다, @Bas!

그 게시물에 언급된 설정과 무관하게, 제가 429 오류를 받는 것은 맞지 않다고 생각합니다. 제가 제공한 예시에서는 15개의 요청을 보냈는데, 이는 모든 기본 API 한도 미만입니다. 이 작업은 관리자 API 키와 사용자명을 사용하여 수행했습니다.

해당 예시는 다음 IP당 기본 한도를 초과하지 않습니다:

관리자가 아닌 한도조차도 초과하지 않습니다:

DISCOURSE_MAX_REQS_PER_IP_MODE를 warn 또는 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><center>nginx</center>\r\n</body>\r\n</html>\r\n

nginx를 언급하는 주제들에 대해 더 조사해 보겠습니다.

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의 값만 수정하면 될까요?

  • 이 값들을 수정하고 재빌드(rebuild) 후에도 변경 사항이 유지되도록 하는 올바른 방법은 무엇입니까?
    컨테이너 내에서 nginx 설정을 직접 편집한 후 컨테이너를 중지/시작하면 된다고 생각합니다. 하지만 이 값들은 원래 templates/web.ratelimited.template.yml에서 온 것으로 보이며, 재빌드 시 덮어쓰기(overwrite)될 수 있는 것 같습니다?

도움 주셔서 감사합니다! :pray:

아, 이제 내 전문 분야를 벗어난 것 같네요.

만약 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;
        }
```\n
예, 빌드 시 pups를 사용하여 치환 작업을 수행하여 이를 영구적으로 유지해야 합니다. 이를 어떻게 접근해야 하는지 확인하려면 `web.ssl.template.yml` 파일을 참고하세요.

또는 이 문제를 무시하고, 전략적인 위치에 일부 sleep을 삽입하여 API 클라이언트 스크립트의 실행 속도를 늦추는 방법도 있습니다. <-- 권장 방법

예를 들어, 레이트 리미팅(rate limiting)에 걸렸을 때 rescue 블록 안에서 처리하는 방식이죠. 저는 보통 그렇게 한다고 생각하는데, 실제로도 그렇게 하는 편입니다.

표준 설치 환경에서 API 요청의 최대 지속 가능한 속도는 분당 또는 초당 몇 건인가요?

초당 1건으로 시도했는데 한도에 도달한 것 같습니다. 반면, 7초에 1건으로 요청하면 문제가 없습니다.