본문 바로가기
WIKI 기술 지식 베이스

Concurrency limits

원문 보기 위키 갱신

출처: 문서

본문

동시성 제한

3.9부터 도입 {: .label .label-purple }

동시성 제한은 주어진 액션이 동시에 처리할 수 있는 요청 수를 제한해서, 노드가 처리 완료할 수 있는 양보다 많은 요청을 받아들이지 않도록 해요. 각 제한값은 관찰된 지연 시간에 맞춰 지속적으로 조정돼요. 노드가 처리량을 유지하는 동안에는 제한값이 늘어나고, 왕복 시간이 길어지거나 다운스트림 구성 요소가 요청 거부를 시작하면 줄어들어요. 제한값에 도달하면 추가 요청은 HTTP 429 Too Many Requests 응답과 함께 거부돼요.

기본적으로 동시성 제한은 비활성화되어 있어요. indices:data/read/search나 indices:data/write/bulk 같은 transport 액션에는 동시성 제한을 구성할 수 있어요. 구성하는 각 제한은 _리미터(limiter)_를 만들어요. 리미터는 해당 액션의 요청을 추적하고, 제한값을 조정하고, 제한값을 초과하는 요청을 거부하는 구성 요소예요. 코드 변경이나 재시작은 필요 없어요. 제한을 구성하거나 변경한 후 처음 5분 동안은 리미터가 요청을 거부하지 않고 보정해요. 이 기간은 warmup_duration으로 조정할 수 있어요.

동시성 제한 설정

모든 동시성 제한 설정은 동적 설정이에요. 동적 설정을 업데이트하는 방법은 동적 설정 문서를 참조하세요.

설정 이름은 concurrency_limit.action.<limiter_name>.<setting> 패턴을 따라요. 여기서 <limiter_name>은 직접 정하는 이름이에요. 이 이름은 하나의 리미터에 속한 설정들을 묶어 주는 역할일 뿐 그 자체로는 의미가 없어요. 어떤 액션을 제한할지는 action_name 설정이 지정해요. 리미터는 원하는 만큼 몇 개든 구성할 수 있어요.

각 리미터는 다음 설정을 지원해요:

  • concurrency_limit.action.<limiter_name>.action_name (Dynamic, string): 제한할 transport 액션의 이름이에요. 예를 들어 indices:data/read/search 또는 indices:data/write/bulk가 있어요. 필수 항목이고, 이 설정을 지우면 리미터가 제거돼요.

  • concurrency_limit.action.<limiter_name>.mode (Dynamic, string): 리미터의 모드예요. 유효한 값은 disabled, monitor_only, enforced예요. 기본값은 disabled예요.

  • concurrency_limit.action.<limiter_name>.algorithm (Dynamic, string): 제한값을 조정하는 데 사용하는 알고리즘이에요. 유효한 값은 vegas, gradient2, aimd예요. 기본값은 vegas예요.

  • concurrency_limit.action.<limiter_name>.limit.initial (Dynamic, integer): 시작 동시성 제한값이에요. 1 이상이고 limit.max 이하여야 해요. 기본값은 20이에요.

  • concurrency_limit.action.<limiter_name>.limit.max (Dynamic, integer): 알고리즘이 도달할 수 있는 최대 동시성 제한값이에요. 1 이상이고 limit.initial 이상이어야 해요. 기본값은 200이에요.

  • concurrency_limit.action.<limiter_name>.warmup_duration (Dynamic, time unit): 구성 후 리미터가 요청을 거부하지 않고 보정하는 기간이에요. 0 이상이어야 해요. 기본값은 5m이에요.

  • concurrency_limit.action.<limiter_name>.vegas.updrift_factor (Dynamic, integer): Vegas 알고리즘이 제한값을 올리는 폭에 곱해지는 배수예요. 1 이상이어야 해요. 기본값은 1이고, 이는 표준 Vegas 동작과 같아요.

  • concurrency_limit.action.<limiter_name>.vegas.increase_barrier (Dynamic, integer): Vegas 알고리즘이 제한값을 올리기 전에 필요한, 조건을 만족하는 연속 샘플 수예요. 1 이상이어야 해요. 기본값은 1이에요.

  • concurrency_limit.action.<limiter_name>.vegas.decrease_barrier (Dynamic, integer): Vegas 알고리즘이 제한값을 내리기 전에 필요한, 조건을 만족하는 연속 샘플 수예요. 1 이상이어야 해요. 드롭은 이 장벽을 우회해서 제한값을 즉시 낮춰요. 기본값은 1이에요.

  • concurrency_limit.action.<limiter_name>.vegas.baseline_reset_load_threshold (Dynamic, double): 프로브가 무부하 지연 기준선을 재설정할 수 있도록 허용되는, 현재 제한값 대비 활성 요청 수의 최대 비율이에요. 값 범위는 [0, 1]이에요. 기본값은 0.5예요.

  • concurrency_limit.action.<limiter_name>.gradient2.rtt_tolerance (Dynamic, double): Gradient2 알고리즘이 제한값을 낮추기 전까지 단기 왕복 시간이 장기 왕복 시간을 초과할 수 있는 정도예요. 1.0 이상이어야 해요. 기본값은 1.5예요.

  • concurrency_limit.action.<limiter_name>.aimd.backoff_ratio (Dynamic, double): 요청이 드롭됐을 때 AIMD 알고리즘이 제한값에 곱하는 계수예요. 0.5 이상 1.0 미만이어야 해요. 기본값은 0.9예요.

  • concurrency_limit.action.<limiter_name>.burst.capacity (Dynamic, integer): 버스트 윈도우가 열려 있는 동안 적응형 제한값 위에 더해지는 여분의 버스트 용량이에요. 0 이상이어야 해요. 기본값은 0이고, 이 경우 버스팅이 비활성화돼요.

  • concurrency_limit.action.<limiter_name>.burst.close_after (Dynamic, integer): 버스트 윈도우가 닫히기까지 필요한 연속 포화 샘플 수예요. 1 이상이어야 해요. 기본값은 5예요.

  • concurrency_limit.action.<limiter_name>.burst.open_after (Dynamic, integer): 버스트 윈도우가 다시 열리기까지 필요한 연속 비포화 샘플 수예요. 1 이상이어야 해요. 기본값은 5예요.

  • concurrency_limit.action.<limiter_name>.partitions (Dynamic, list): 파티션 이름 목록이에요. 이 목록이 비어 있지 않으면 partition.resolver가 필요해요. 기본값은 빈 목록이에요.

  • concurrency_limit.action.<limiter_name>.partition.<name>.percent (Dynamic, double): 파티션 <name>에 예약되는 전체 제한값의 비율이에요. 값 범위는 [0, 1]이고, 모든 파티션의 비율 합계는 1.0 이하여야 해요. 기본값은 0.0이에요.

  • concurrency_limit.action.<limiter_name>.partition.<name>.delay_ms (Dynamic, integer): 파티션 <name>의 비율을 초과하는 요청을 거부하기 전에 대기하는 시간(밀리초)이에요. 어느 쪽이든 요청은 거부되지만, 이 대기 시간 동안 호출 스레드를 붙잡아 두어서 클라이언트가 요청을 보내는 속도를 늦춰요. 리미터당 최대 100개의 요청이 동시에 대기 상태가 될 수 있고, 그 외의 요청은 대기 없이 거부돼요. 0 이상이어야 해요. 기본값은 0이고, 이 경우 즉시 거부돼요.

  • concurrency_limit.action.<limiter_name>.partition.resolver (Dynamic, string): 요청을 파티션에 매핑하는 리졸버예요. 유효한 값은 byHeader, fixed, bySearchType이에요. partitions가 비어 있지 않으면 필수예요.

  • concurrency_limit.action.<limiter_name>.partition.resolver.fixed.partition (Dynamic, string): 리졸버가 fixed일 때 모든 요청을 받는 파티션이에요. partitions에 나열된 이름 중 하나여야 해요. 기본값은 default예요.

  • concurrency_limit.action.<limiter_name>.partition.resolver.bySearchType.aggregation (Dynamic, string): 리졸버가 bySearchType일 때 애그리게이션이 포함된 검색 요청을 받는 파티션이에요. 기본값은 aggregation이에요.

  • concurrency_limit.action.<limiter_name>.partition.resolver.bySearchType.filter (Dynamic, string): 리졸버가 bySearchType일 때 그 외의 모든 검색 요청을 받는 파티션이에요. 기본값은 filter예요.

Cluster Settings API는 모든 값을 검증하고, 값이 범위를 벗어나거나, 파티션 비율의 합계가 1.0을 넘거나, partition.resolver 없이 partitions가 설정되면 업데이트를 거부해요. 액션에 대한 제한을 멈추려면 해당 액션의 mode를 disabled로 설정하거나 리미터의 action_name 설정을 제거하세요. {: .note}

모드

각 리미터는 mode 설정으로 지정하는 세 가지 모드 중 하나로 동작해요:

  • disabled (기본값): 리미터가 비활성 상태예요. 요청을 추적하거나 거부하지 않아요.
  • monitor_only: 리미터가 요청을 추적하고 제한값을 조정하지만 절대 거부하지 않아요. 원래라면 거부되었을 요청도 total_rejected 통계에 집계돼요. 제한을 강제하기 전에 관찰하려면 이 모드를 사용하세요.
  • enforced: 제한값에 도달하고 워밍업 기간이 지나면 리미터가 요청을 거부해요.

monitor_only 모드로 시작해서 current_limit과 total_rejected 통계를 모니터링하고, 제한값이 적절한 값으로 안정되면 enforced로 전환하는 방법을 권장해요. {: .tip}

거부된 요청

거부된 요청은 HTTP 상태 429와 rejected_execution_exception이 반환되며 실패해요. 응답은 대략 다음과 같아요:

{
  "error": {
    "root_cause": [
      {
        "type": "rejected_execution_exception",
        "reason": "request rejected: concurrency limit reached for action [indices:data/read/search]"
      }
    ],
    "type": "rejected_execution_exception",
    "reason": "request rejected: concurrency limit reached for action [indices:data/read/search]"
  },
  "status": 429
}

OpenSearch는 이런 응답에 Retry-After 헤더를 추가하지 않아요. 지수 백오프 방식으로 재시도하도록 클라이언트를 구성하세요.

알고리즘

algorithm 설정은 제한값이 적응하는 방식을 선택해요. 세 가지 알고리즘은 모두 Netflix concurrency-limits 라이브러리에서 제공해요. 제한값은 항상 limit.initial과 limit.max 사이에 머물러요.

완료된 요청 각각은 요청의 왕복 시간과 성공 여부로 이루어진 샘플(sample) 하나를 만들어요. 알고리즘은 이 샘플들을 바탕으로 제한값을 조정해요. 성공적으로 완료되면 제한값을 올릴 수 있고, 스레드 풀 거부 같은 다운스트림 rejected_execution_exception은 드롭으로 간주되어 제한값을 낮춰요. 그 외의 실패는 제한값에 영향을 주지 않아요.

Vegas

기본 알고리즘인 vegas는 TCP Vegas 혼잡 제어에 기반을 둬요. 관찰된 가장 낮은 왕복 시간을 무부하 기준선으로 기록하고, 새 샘플마다 이 기준선과 비교해요. 지연 시간이 기준선에 가까운 상태를 유지하면 제한값이 늘어나고, 지연 시간이 올라가면 줄어들어요. Vegas는 대부분의 워크로드에 잘 맞으며 기본값이에요.

다음 표는 Vegas 알고리즘을 구성하는 설정을 보여줘요.

설정 설명
vegas.updrift_factor 알고리즘이 제한값을 올리는 폭에 곱해지는 배수예요. 짧은 트래픽 스파이크가 있는 워크로드에 적합해요. 기본값인 1은 표준 Vegas 동작과 같아요.
vegas.increase_barrier 제한값이 올라가기 전에 조건을 만족하는 샘플이 연속으로 여러 개 필요하도록 해서 진동을 줄여요.
vegas.decrease_barrier 제한값이 내려가기 전에 조건을 만족하는 샘플이 연속으로 여러 개 필요하도록 해서 진동을 줄여요. 드롭은 이 장벽을 우회해서 제한값을 즉시 낮춰요.
vegas.baseline_reset_load_threshold 과부하 상태에서 측정된 지연 시간이 무부하 기준선을 대체하지 못하도록 막아요. 활성 요청 수가 현재 제한값의 이 비율 미만일 때만 프로브가 기준선을 재설정해요.

Gradient2

gradient2 알고리즘은 단기 왕복 시간 평균을 장기 평균과 비교해요. 단기 지연 시간이 장기 지연 시간보다 gradient2.rtt_tolerance을 넘어서 높아지면 리미터가 제한값을 낮춰요. Vegas보다 서서히 진행되는 지연 드리프트에 부드럽게 반응하며, 기준 지연 시간이 시간이 지나면서 변하는 워크로드에 적합해요.

AIMD

aimd 알고리즘은 가산적 증가와 승법적 감소를 사용해요. 성공한 샘플이 있을 때마다 제한값이 1씩 늘어나고, 요청이 드롭될 때마다 aimd.backoff_ratio를 곱해요. 단순하고 예측 가능하지만 지연 상승이 아니라 드롭에만 반응해요. 그래서 다운스트림 구성 요소가 포화 상태가 되면 요청을 명시적으로 거부하는 액션에 가장 잘 맞아요.

버스트 용량

burst.capacity 설정은 적응형 제한값 위에 고정된 여유분을 더해서, 짧은 스파이크가 거부되지 않고 흡수되도록 해요. 버스트 윈도우는 열린 상태로 시작하기 때문에 실효 제한값은 적응형 제한값에 burst.capacity를 더한 값이에요. 활성 요청 수가 적응형 제한값에 도달한 샘플이 burst.close_after번 연속으로 이어지면 윈도우가 닫히고, 적응형 제한값 아래에 머문 샘플이 burst.open_after번 연속으로 이어지면 다시 열려요. burst.capacity를 0(기본값)으로 설정하면 버스팅이 비활성화돼요.

파티션

동시성 제한을 이름이 지정된 하위 풀로 나눠서, 한 종류의 트래픽이 다른 트래픽이 사용할 수 있는 용량을 모두 소진하지 못하게 하세요. partitions에 풀 이름을 나열하고, partition.<name>.percent를 사용해 각 풀에 전체 제한의 몫을 할당하세요. 몫의 합은 최대 1.0이어야 해요. 이름이 지정된 파티션에 해당하지 않는 요청은 내장 unknown 풀로 라우팅되는데, 이 풀은 전체 제한에 도달한 후에는 한 번에 하나의 요청만 허용해요. 할당하지 않고 남겨둔 몫은 부하 상황에서 어떤 파티션에도 사용될 수 없으므로, 대부분의 경우 몫의 합을 1.0으로 맞추는 게 좋아요.

파티션 몫은 리미터가 전체 제한에 도달한 상태일 때만 강제돼요. 전체 제한 이하에서는 파티션이 자신의 몫을 넘어 남는 용량을 사용할 수 있어요. 전체 제한에 도달하면 각 파티션은 자신의 몫으로 제한돼요. 0.0 몫을 가진 파티션을 포함해 모든 파티션은 최소 하나의 동시 요청이 보장되므로, 0.0 몫은 부하 상황에서 아예 요청을 받지 못하는 게 아니라 한 번에 하나의 요청을 허용해요.

partitions를 설정하면 각 요청을 파티션에 매핑하는 partition.resolver도 반드시 선택해야 해요. 다음 표는 사용 가능한 리졸버를 정리한 거예요.

리졸버 설명
byHeader X-Request-Tier 요청 헤더를 읽고, 헤더 값과 정확히 일치하는 이름의 파티션으로 요청을 라우팅해요. 대소문자를 구분해요. 헤더가 없거나 인식할 수 없는 값이면 unknown 풀로 보내요. 헤더 이름은 변경할 수 없어요.
fixed 모든 요청을 partition.resolver.fixed.partition에 지정된 단일 파티션으로 라우팅해요.
bySearchType 애그리게이션이 포함된 검색 요청은 partition.resolver.bySearchType.aggregation에 지정된 파티션으로, 그 외 모든 검색은 partition.resolver.bySearchType.filter에 지정된 파티션으로 라우팅해요. 검색이 아닌 요청은 unknown 풀로 보내요.

예시: 프리미엄 트래픽을 위한 용량 예약

다음 요청은 검색 요청에 제한을 걸고, X-Request-Tier: premium을 담은 요청에 제한의 70%를 예약하며, X-Request-Tier: standard를 담은 요청에는 30%를 남겨요:

PUT /_cluster/settings
{
  "persistent": {
    "concurrency_limit.action.search.action_name": "indices:data/read/search",
    "concurrency_limit.action.search.mode": "enforced",
    "concurrency_limit.action.search.partitions": ["premium", "standard"],
    "concurrency_limit.action.search.partition.resolver": "byHeader",
    "concurrency_limit.action.search.partition.premium.percent": 0.7,
    "concurrency_limit.action.search.partition.standard.percent": 0.3
  }
}

{% include copy-curl.html %}

그다음 클라이언트는 각 검색 요청에 헤더를 설정해요:

curl -X GET "http://localhost:9200/my-index/_search" \
  -H "X-Request-Tier: premium" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "match_all": {} } }'

{% include copy.html %}

동시성 제한 모니터링

각 노드에서 구성된 모든 리미터를 모니터링하려면 Nodes Stats API에서 concurrency_limiter 메트릭을 요청하세요:

GET _nodes/stats/concurrency_limiter

{% include copy-curl.html %}

통계는 응답의 concurrency_limiters 키 아래에 나타나요. 각 통계에 대한 설명은 concurrency_limiters를 참고하세요:

{
  "_nodes": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "cluster_name": "opensearch-cluster",
  "nodes": {
    "T7aqO6zaQX-lt8XBWBYLsA": {
      "timestamp": 1755530400000,
      "name": "node-1",
      "transport_address": "127.0.0.1:9300",
      "host": "127.0.0.1",
      "ip": "127.0.0.1:9300",
      "roles": [
        "cluster_manager",
        "data",
        "ingest",
        "remote_cluster_client"
      ],
      "attributes": {
        "shard_indexing_pressure_enabled": "true"
      },
      "concurrency_limiters": {
        "search": {
          "action_name": "indices:data/read/search",
          "mode": "enforced",
          "algorithm": "vegas",
          "current_limit": 42,
          "in_flight": 7,
          "total_rejected": 128,
          "last_rtt_millis": 12,
          "rtt_no_load_millis": 4
        }
      }
    }
  }
}

Cluster Stats API에는 동시성 리미터 통계가 포함되지 않아요. {: .note}

메트릭

metrics framework는 OpenSearch 텔레메트리를 외부 모니터링 백엔드로 내보내는 실험적 기능이에요. 활성화하면 각 활성 리미터가 다음 게이지를 발행하는데, 이 게이지는 해당하는 통계와 같은 값을 보고해요.

게이지 해당 통계
concurrency_limit.current_limit current_limit
concurrency_limit.in_flight in_flight
concurrency_limit.total_rejected total_rejected
concurrency_limit.last_rtt last_rtt_millis
concurrency_limit.rtt_noload rtt_no_load_millis

모든 게이지에는 action_name, mode, algorithm 속성이 붙고, 리미터 이름을 담은 alias 속성도 함께 붙어요.

더 알아보기 (Learn more)