속도 제한 패턴

서비스의 제한 제한 및 전체 용량 내에서 유지되도록 애플리케이션이 서비스에 요청을 보내는 속도를 제어합니다. 이 방법을 사용하면 제한 오류를 방지하거나 최소화하고 처리량을 보다 정확하게 예측할 수 있습니다.

속도 제한은 많은 시나리오에서 적합하지만 일괄 처리와 같은 대규모 반복 자동화 작업에 특히 유용합니다.

컨텍스트 및 문제점

제한된 서비스에 대해 많은 수의 작업을 수행하면 거부된 요청을 추적한 다음 작업을 다시 시도해야 하므로 트래픽이 증가하고 처리량이 감소할 수 있습니다. 작업 수가 증가함에 따라 제한 제한에는 여러 번의 재전송 데이터가 필요할 수 있으며, 이로 인해 성능에 더 큰 영향을 미칠 수 있습니다.

예를 들어, Azure Cosmos DB에 데이터를 수집하는 다음과 같은 문제가 있는 오류 발생 시 재시도 프로세스를 살펴보겠습니다.

  1. 애플리케이션은 Azure Cosmos DB에 10,000개의 레코드를 수집해야 합니다. 각 레코드는 수집할 RU(요청 단위)가 10개이므로 작업을 완료하려면 총 100,000RU가 필요합니다.

  2. Azure Cosmos DB 인스턴스에는 프로비저닝된 용량이 20,000RU입니다.

  3. 10,000개의 레코드를 모두 Azure Cosmos DB로 보냅니다. 2,000개의 레코드가 작성되고 8,000개의 레코드가 거부됩니다.

  4. 나머지 8,000개의 레코드를 Azure Cosmos DB로 보냅니다. 2,000개의 레코드가 작성되고 6,000개의 레코드가 거부됩니다.

  5. 나머지 6,000개의 레코드를 Azure Cosmos DB로 보냅니다. 2,000개의 레코드가 작성되고 4,000개의 레코드가 거부됩니다.

  6. 나머지 4,000개의 레코드를 Azure Cosmos DB로 보냅니다. 2,000개의 레코드가 작성되고 2,000개의 레코드가 거부됩니다.

  7. 나머지 2,000개의 레코드를 Azure Cosmos DB로 보냅니다. 모두 성공적으로 작성되었습니다.

수집 작업이 성공적으로 완료되지만 Azure Cosmos DB 30,000개의 레코드를 보낸 후에만 완료됩니다. 전체 데이터 세트는 10,000개의 레코드로만 구성됩니다.

이 예제에서는 고려해야 할 다른 요소가 있습니다.

  • 많은 수의 오류로 인해 이러한 오류를 기록하고 결과 로그 데이터를 처리하는 추가 작업이 발생할 수도 있습니다. 이전 방법은 20,000개의 오류를 처리하며 이러한 오류를 로깅하면 처리, 메모리 또는 스토리지 리소스 비용이 부과될 수 있습니다.

  • 수집 서비스의 제한 제한을 모르기 때문에 데이터 처리에 걸리는 기간에 대한 기대치를 설정할 방법이 없습니다. 속도 제한을 적용하면 데이터 수집에 필요한 시간을 계산할 수 있습니다.

해결 방법

속도 제한은 지정된 기간 동안 서비스로 전송되는 레코드 수를 줄여 트래픽을 줄이고 처리량을 향상시킬 수 있습니다.

서비스는 다음과 같이 시간에 따라 다른 메트릭에 따라 요청을 제한할 수 있습니다.

  • 작업 수(예: 초당 요청 20개)
  • 데이터 양(예: 분당 2GiB)
  • 작업의 상대적 비용(예: 초당 20,000RU)

제한에 사용하는 메트릭에 관계없이 속도 제한 구현에는 특정 기간 동안 서비스에 전송된 작업의 수 및/또는 크기를 제어하는 작업이 포함됩니다. 속도 제한은 제한 용량을 초과하지 않고 서비스 사용을 최적화합니다.

API가 제한된 수집 서비스에서 허용하는 것보다 더 빠르게 요청을 처리할 수 있는 시나리오에서는 서비스를 사용하는 속도를 관리해야 합니다. 서비스가 복구될 때까지 제한만 데이터 속도 불일치로 처리하고 수집 요청을 버퍼링하면 위험이 발생합니다. 이 시나리오에서 애플리케이션의 응답이 중지되면 버퍼링된 데이터가 손실될 수 있습니다.

이러한 위험을 방지하려면 전체 수집 속도를 처리할 수 있는 지속형 메시징 시스템으로 레코드를 보내는 것이 좋습니다 (Azure Event Hubs 같은 서비스는 초당 수백만 개의 작업을 처리할 수 있습니다.) 그런 다음 하나 이상의 작업 프로세서를 사용하여 제한된 서비스의 한도 내에 있는 제어된 속도로 메시징 시스템에서 레코드를 읽을 수 있습니다. 메시징 시스템에 레코드를 제출하면 지정된 시간 간격 동안 처리할 수 있는 레코드만 큐에서 제거할 수 있으므로 내부 메모리를 절약할 수 있습니다.

Azure는 다음을 포함하여 이 패턴에 사용할 수 있는 몇 가지 지속성 메시징 서비스를 제공합니다.

지속성 메시징 흐름을 보여 주는 다이어그램 세 개의 작업 프로세서가 제한된 서비스를 호출합니다.

레코드를 보낼 때 레코드를 해제하는 데 사용하는 기간은 서비스가 제한하는 기간보다 더 세분화될 수 있습니다. 시스템은 쉽게 이해하고 작업할 수 있는 시간 간격에 따라 제한을 설정하는 경우가 많습니다. 그러나 서비스를 실행하는 컴퓨터의 경우 이러한 기간은 정보를 처리할 수 있는 속도에 비해 매우 길 수 있습니다. 예를 들어 시스템은 처리 속도를 초당 또는 분당 기준으로 제한할 수 있지만, 일반적으로 코드의 처리 단위는 나노초 또는 밀리초 수준인 경우가 많습니다.

필수는 아니지만 처리량을 개선하기 위해 더 적은 수의 레코드를 더 자주 보내는 것이 좋습니다. 따라서 레코드를 초당 한 번 또는 분당 한 번씩 한꺼번에 릴리스하도록 처리하려 하기보다, 그보다 더 세분화하여 리소스 사용량(메모리, CPU, 네트워크)이 보다 고르게 유지되도록 할 수 있습니다. 이 방법은 갑작스러운 요청 버스트로 인한 잠재적인 병목 상태를 방지합니다. 예를 들어 서비스에서 초당 100개의 작업을 허용하는 경우 다음 그래프와 같이 200밀리초마다 20개의 작업을 해제하여 속도 제한기의 구현이 요청을 균등하게 처리할 수 있습니다.

시간에 따른 속도 제한 흐름을 보여 주는 차트입니다.

또한 서로 조율되지 않은 여러 프로세스가 속도 제한이 적용된 서비스를 공유해야 하는 경우도 있습니다. 이 시나리오에서 속도 제한을 구현하려면 서비스의 용량을 논리적으로 분할한 다음 분산 상호 제외 시스템을 사용하여 해당 파티션에 대한 배타적 잠금을 관리할 수 있습니다. 그런 다음, 정렬되지 않은 프로세스는 용량이 필요할 때마다 해당 파티션에 대한 잠금을 위해 경쟁할 수 있습니다. 프로세스가 잠금을 보유하는 각 파티션에 대해 일정 용량이 부여됩니다.

예를 들어 제한된 시스템에서 초당 500개의 요청을 허용하는 경우 초당 25개 요청당 20개의 파티션을 만들 수 있습니다. 프로세스가 100개의 요청을 발급해야 하는 경우 분산 상호 제외 시스템에 4개의 파티션을 요청할 수 있습니다. 시스템에서 10초 동안 두 개의 파티션을 부여할 수 있습니다. 그런 다음 프로세스는 초당 50개 요청으로 제한을 평가하고 2초 안에 작업을 완료한 다음 잠금을 해제합니다.

이 패턴을 구현하는 한 가지 방법은 Azure Storage 사용하는 것입니다. 이 시나리오에서는 컨테이너에서 논리 파티션당 하나의 0바이트 Blob을 만듭니다. 그러면 애플리케이션은 짧은 시간(예: 15초)동안 해당 Blob에 대해 직접 배타적 임대를 얻을 수 있습니다. 애플리케이션은 부여받은 각 리스에 대해 해당 파티션에 할당된 용량만큼 사용할 수 있습니다. 그런 다음, 애플리케이션은 시간이 만료되면 애플리케이션이 부여된 용량 사용을 중지할 수 있도록 임대 시간을 추적해야 합니다. 이 패턴을 구현할 때 용량이 필요할 때 각 프로세스에서 임의 파티션을 임대하려고 하는 경우가 많습니다.

대기 시간을 더 줄이기 위해 각 프로세스에 대해 소량의 배타적 용량을 할당할 수 있습니다. 그런 다음 프로세스는 예약된 용량을 초과해야 하는 경우에만 공유 용량에 대한 임대를 얻으려고 합니다.

Azure Blob Storage Blob 파티션에서 배타적 임대를 위해 경쟁하는 여러 프로세스를 보여 주는 다이어그램

Azure Storage 대신 ZooKeeper, etcd 및 Redis/Redsync와 같은 기술을 사용하여 이러한 종류의 임대 관리 시스템을 구현할 수도 있습니다.

문제 및 고려 사항

이 패턴을 구현하는 방법을 결정할 때 다음 사항을 고려합니다.

  • 속도 제한 패턴은 제한 오류 수를 줄일 수 있지만 애플리케이션은 발생할 수 있는 모든 제한 오류를 제대로 처리해야 합니다.

  • 재시도가 속도 제한과 조정되었는지 확인합니다. 무분별하거나 지나치게 공격적인 재시도는 부하를 증가시키고 재시도 폭주를 일으킬 수 있으므로, 백프레셔 신호(예: Retry-After가 포함된 HTTP 429)를 전파하고 시도 간에 짧은 무작위 지연을 두며 재시도 횟수는 제한하세요.

  • 애플리케이션에 동일한 제한된 서비스에 액세스하는 여러 작업 스트림이 있는 경우 모든 워크스트림을 속도 제한 전략에 통합해야 합니다. 예를 들어 데이터베이스에 레코드를 대량 로드할 뿐만 아니라 동일한 데이터베이스의 레코드에 대한 쿼리도 지원할 수 있습니다. 모든 작업 스트림이 동일한 속도 제한 메커니즘을 통해 제어되도록 하여 용량을 관리할 수 있습니다. 또는 각 작업 스트림에 대해 별도의 용량 풀을 예약할 수 있습니다.

  • 제한된 서비스는 여러 애플리케이션에서 사용될 수 있습니다. 경우에 따라 이 문서의 앞부분에서와 같이 해당 사용량을 조정할 수 있습니다. 예상보다 많은 스로틀링 오류가 나타나기 시작하면, 이러한 오류 수의 증가는 서비스에 액세스하는 애플리케이션 간에 경합이 있음을 나타낼 수 있습니다. 이 경우 다른 애플리케이션의 사용량이 감소할 때까지 속도 제한 메커니즘에 의해 부과되는 처리량을 일시적으로 줄이는 것을 고려해야 할 수 있습니다.

이 패턴을 사용하는 경우

다음과 같은 경우 이 패턴을 사용합니다.

  • 요청 수가 제한된 서비스에서 발생하는 스로틀링 오류를 줄여야 합니다.

  • 순진한 오류 재시도 방법과 비교하여 트래픽을 최소화하려고 합니다.

  • 레코드를 처리할 수 있는 충분한 용량이 있는 경우에만 레코드를 큐에서 해제하여 메모리 사용량을 줄여야 합니다.

이 패턴은 다음과 같은 경우에 적합하지 않을 수 있습니다.

  • 작업을 수행하려면 대기 시간이 매우 짧은 즉시 동기식 완료가 필요하며 큐 또는 지연된 처리를 허용할 수 없습니다.

  • 주된 병목은 요청률이 아니라 동시성 또는 리소스 경합(예: CPU 포화 또는 현재 처리 중인 장시간 실행 작업)입니다. 이러한 경우 크기 조정 또는 동시성 컨트롤이 더 적합합니다.

워크로드 디자인

워크로드 디자인에서 속도 제한 패턴을 사용하여 Azure Well-Architected Framework 핵심 요소에서 다루는 목표와 원칙을 해결하는 방법을 평가합니다. 다음 표에서는 이 패턴이 각 핵심 요소의 목표를 지원하는 방법에 대한 지침을 제공합니다.

핵심 요소 이 패턴으로 핵심 목표를 지원하는 방법
안정성 설계 결정을 통해 워크로드가 오작동에 대한 복원력을 높일 수 있으며 오류가 발생한 후 완전히 작동하는 상태로 복구 되도록 할 수 있습니다. 이 전술은 서비스가 과도한 사용을 방지하는 것을 선호하는 경우 서비스와 통신하는 제한 사항과 비용을 인정하고 적용하여 클라이언트를 보호합니다.

- RE:07 자기 보존

이 패턴이 하나의 기둥 내에서 절충을 도입하는 경우, 이를 다른 기둥의 목표와 비교해서 고려해 보세요.

Example

다음 예제 애플리케이션에서는 사용자가 다양한 유형의 레코드를 API에 제출할 수 있습니다. 각 레코드 형식에는 다음 단계를 수행하는 고유한 작업 프로세서가 있습니다.

  1. 유효성 검사
  2. 농축
  3. 데이터베이스에 레코드 삽입

애플리케이션의 모든 구성 요소(API, 작업 프로세서 A 및 작업 프로세서 B)는 독립적으로 확장할 수 있는 별도의 프로세스입니다. 프로세스는 서로 직접 통신하지 않습니다.

파티션된 임대 저장소가 스로틀링된 데이터베이스에 기록하는 다중 큐, 다중 프로세서 흐름을 보여 주는 다이어그램.

다이어그램은 공유 API를 통해 레코드를 제출하는 두 사용자를 보여줍니다. 각 레코드 형식은 별도의 큐로 라우팅되고, 전용 작업 프로세서에서 처리되며, Azure Storage Blob 파티션 임대에 의해 제어되는 제어된 속도로 제한된 데이터베이스에 기록됩니다. 두 사용자는 각각 API 구성 요소에 메시지 일괄 처리를 보냅니다. 한 사용자가 10,000개의 메시지를 제출하고 다른 사용자는 5,000개의 메시지를 제출합니다. API는 10,000개의 메시지를 큐 A로 라우팅하고 5,000개의 메시지를 큐 B로 라우팅합니다. 큐 A는 작업 프로세서 A에 연결되고 큐 B는 작업 프로세서 B에 연결됩니다. 작업 프로세서 A는 초당 300개의 레코드 속도로 데이터베이스에 씁니다. 작업 프로세서 B는 초당 500개의 레코드로 데이터베이스에 씁니다. 두 작업 프로세서 모두 "초당 800개의 레코드로 제한됨"이라는 레이블이 지정된 데이터베이스 구성 요소에 연결합니다. 두 작업 프로세서 아래에 Azure Storage 0에서 7까지 레이블이 지정된 8개의 Blob 파티션을 포함합니다. 참고는 각 파티션의 가치가 초당 100개 레코드임을 나타냅니다. 여러 화살표는 작업 프로세서를 Blob 파티션에 연결합니다. 녹색 화살표는 작업 프로세서 A에서 파티션 0, 4 및 6까지를 가리킵니다. 이는 프로세서가 이러한 세 파티션에 임대를 보유한다는 것을 나타냅니다. 이러한 임대 계약이 초당 300레코드의 처리율을 가능하게 합니다. 녹색 화살표는 작업 프로세서 B에서 파티션 1, 2, 3, 5 및 7로 가리킵니다. 이러한 화살표는 이 프로세서가 5개의 파티션에 임대를 보유함을 나타냅니다. 이러한 리스 덕분에 초당 500레코드의 처리 속도를 낼 수 있습니다. 실패한 임대 시도는 다른 프로세서가 이미 보유하고 있는 파티션으로 교차하는 빨간색 화살표로 표시됩니다. 이러한 화살표는 경합 해결을 나타냅니다. 다이어그램은 두 프로세서에서 보유된 모든 파티션의 합계가 데이터베이스 스로틀 제한과 일치하는 초당 800개의 레코드와 같은 것을 보여 줍니다.

이 예제에서 각 Blob 임대는 허용된 데이터베이스 처리량의 고정 공유를 나타냅니다. 프로세서는 현재 보유한 리스의 합산 속도로만 디큐 및 쓰기를 수행할 수 있습니다. 프로세서가 시간이 지남에 따라 임대를 얻거나 잃으면 허용되는 쓰기 속도가 변경되어 총 데이터베이스 트래픽이 구성된 제한 내에서 유지되지만 대기 중인 모든 작업 진행률이 유지됩니다.

이 다이어그램은 다음 워크플로를 통합합니다.

  1. 사용자가 API에 A 형식의 레코드 10,000개 제출
  2. API는 해당 10,000개 레코드를 큐 A에 추가합니다.
  3. 사용자는 B 형식의 레코드 5,000개를 API에 제출합니다.
  4. API는 해당 5,000개 레코드를 대기열 B에 추가합니다.
  5. 작업 프로세서 A는 큐 A에 레코드가 있음을 확인하고 blob 2에 대한 독점 임대를 확보하려고 시도합니다.
  6. 작업 프로세서 B는 큐 B에 레코드가 있음을 확인하고 blob 2에 대한 독점 임대를 획득하려고 시도합니다.
  7. 작업 프로세서 A가 리스를 획득하지 못합니다.
  8. 작업 프로세서 B는 blob 2에 대한 임대를 15초 동안 획득합니다. 이제 데이터베이스에 대한 제한 요청을 초당 100의 속도로 평가할 수 있습니다.
  9. 작업 프로세서 B는 큐 B에서 레코드 100개를 꺼내 기록합니다.
  10. 1초가 지나갑니다.
  11. 작업 프로세서 A는 큐 A에 더 많은 레코드가 있음을 확인하고 Blob 6에서 배타적 임대를 얻으려고 시도합니다.
  12. 작업 프로세서 B는 큐 B에 레코드가 더 남아 있는 것을 보고 Blob 3에 대한 독점 임대를 확보하려고 시도합니다.
  13. 작업 프로세서 A는 Blob 6에 대한 임대권을 15초 동안 획득합니다. 이제 데이터베이스에 대한 제한 요청을 초당 100의 속도로 평가할 수 있습니다.
  14. 작업 프로세서 B는 Blob 3에 대한 임대를 15초 동안 획득합니다. 이제 데이터베이스에 대한 제한 요청을 초당 200의 속도로 평가할 수 있습니다 (Blob 2의 리스도 보유하고 있습니다.)
  15. 작업 프로세서 A는 큐 A에서 레코드 100개를 꺼내 기록합니다.
  16. 작업 프로세서 B는 큐 B에서 200개의 레코드를 꺼내 기록합니다.
  17. 1초가 지나갑니다.
  18. 작업 프로세서 A는 큐 A에 더 많은 레코드가 있음을 확인하고 Blob 0에서 단독 임대를 얻으려고 시도합니다.
  19. 작업 프로세서 B는 큐 B에 더 많은 레코드가 있음을 확인하고 Blob 1에서 배타적 임대를 얻으려고 합니다.
  20. 작업 프로세서 A는 15초 동안 blob 0에 대한 임대를 획득합니다. 이제 데이터베이스에 대한 제한 요청을 초당 200의 속도로 평가할 수 있습니다 (또한 Blob 6에 대한 임차권도 보유하고 있습니다.)
  21. 작업 프로세서 B는 Blob 1에 대한 15초 임대를 획득합니다. 이제 데이터베이스에 대한 제한 요청을 초당 300의 속도로 평가할 수 있습니다 (또한 Blob 2와 3의 임차권도 보유하고 있습니다.)
  22. 작업 프로세서 A는 큐 A에서 레코드 200개를 꺼내 기록합니다.
  23. 작업 프로세서 B는 큐 B에서 레코드 300개를 꺼내 기록합니다.
  24. 그리고 등등.

15초 후에도 하나 또는 두 작업이 모두 완료되지 않습니다. 리스가 만료되면 프로세서는 큐에서 꺼내어 기록하는 요청 수도 줄여야 합니다.

이 패턴의 구현은 다양한 프로그래밍 언어로 사용할 수 있습니다.

  • Go 구현은 GitHub에서 사용할 수 있습니다.
  • Java 구현은 GitHub에서 사용할 수 있습니다.

다음 단계

다음 지침은 이 패턴을 구현할 때도 관련이 있을 수 있습니다.

다음 패턴 및 지침은 이 패턴을 구현할 때도 관련이 있을 수 있습니다.

  • Throttling. 속도 제한 패턴은 일반적으로 제한된 서비스에 대한 응답으로 구현됩니다.

  • Retry. 제한된 서비스에 대한 요청으로 인해 제한 오류가 발생하는 경우 일반적으로 적절한 간격 후에 해당 요청을 다시 시도하는 것이 적절합니다.

  • 큐 기반 부하 평준화는 속도 제한 패턴과 유사하지만 몇 가지 중요한 점에서 다릅니다.

    • 속도 제한은 반드시 큐를 사용하여 부하를 관리할 필요는 없지만 지속형 메시징 서비스를 사용해야 합니다. 예를 들어 속도 제한 패턴은 Apache Kafka 또는 Event Hubs와 같은 서비스를 사용할 수 있습니다.

    • 속도 제한 패턴은 파티션에 분산 상호 배제 시스템의 개념을 도입하여 동일한 제한된 서비스와 통신하는 여러 조정되지 않은 프로세스에 대한 용량을 관리할 수 있습니다.

    • Queue-Based 부하 평준화 패턴은 서비스 간에 성능이 일치하지 않거나 복원력을 향상하려는 경우에 적용됩니다. 따라서 속도 제한보다 더 광범위한 패턴이며, 특히 제한된 서비스에 효율적으로 액세스하는 것과 관련이 있습니다.