Posts WEB - nginx 처리율 제한 살펴보기
Post
Cancel

WEB - nginx 처리율 제한 살펴보기

  • nginx는 처리율 제한을 위해 ‘leaky bucket algorithm’ 사용
  • leaky bucket algorithm
    • 물 : 사용자 요청
    • 버킷 : 요청이 대기하는 큐
    • 넘치는 물 : 큐가 다 차서 거절되는 요청
    • 새는 물 : 서버에 의해 처리되는 요청
1
2
3
4
5
6
7
8
9
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=mylimit;

        proxy_pass http://my_upstream;
    }
}

limit_req_zone

  • 문법 : limit_req_zone key zone=name:size rate=rate [sync];
  • Context : http

key

  • 어떤 기준으로 요청을 제한할지를 지정
  • 예제에서는 $binary_remote_addr를 사용하고 있는데, 이 변수는 클라이언트의 IP 주소를 이진 형태로 나타낸다.
  • 특정 api 호출 전체를 제한하려면 ?
    • $server_name 같은 공통키 사용

zone

  • 제한 상태(예: 각 IP가 얼마나 자주 요청했는지 등)를 저장하는 공유 메모리 공간을 지정
  • 공유 메모리를 사용하기 때문에, 이 정보는 여러 NGINX 워커 프로세스 간에 공유
  • IP 주소 하나당 상태 정보 저장에 약 64바이트(32bit), 128바이트(64bit) 가 필요
    • 따라서 32bit 기준 1MB로 약 16,000개 64비트 기준 약 8,000개 저장 가능
  • 새로운 항목을 만들 때마다 최근 60초 동안 사용되지 않은 항목 최대 2개를 제거해 메모리 고갈을 방지
  • 만약 저장 공간이 가득 찼는데 새로운 IP 주소의 상태 정보를 저장하려고 하면:
    • 가장 오래된 항목을 제거하고, 그래도 공간이 부족하면 503(Service Temporarily Unavailable) 상태 코드를 반환

rate

  • 최대 요청 속도를 지정
  • 예를 들어 10r/s라고 하면, 초당 10건의 요청만 허용
    • nginx는 밀리초 단위로 요청을 추적하므로, 이 설정은 100ms마다 1개의 요청을 허용한다는 의미
  • 지원 단위 : 초(r/s), 분(r/m)

sync ?

  • 워커 프로세스가 여러 개일 때 공유 메모리(zone)를 이용해 rate limit 상태를 공유하지만, 워크로드이 많거나 고속 요청 환경에서는 다음 문제가 생길 수 있다:
    • 워커 프로세스들이 동시에 zone을 업데이트하면 동기화 지연이나 race condition처럼 보이는 현상이 발생할 수 있음
    • 즉, 잠깐 동안 초과 요청이 제한 없이 들어가는 것처럼 보일 수 있음
  • sync 옵션은 이런 상황에서:
    • 정확도 향상 : 요청 제한 속도를 보다 정확하게 적용
    • 레이스 컨디션 방지 : 워커 간 동시 업데이트 시 충돌 완화
    • Burst 처리의 일관성 개선 : 여러 워커가 동시에 burst 큐를 채우는 상황을 제어

limit_req

  • 문법 : limit_req zone=name [burst=number] [nodelay | delay=number];
  • Context : http, server, location

burst

  • 만약 위 같은 예시에서 동일한 IP에서 100ms 이내에 2개의 요청이 들어오면, NGINX는 두 번째 요청에 대해 503 (Service Temporarily Unavailable) 응답을 보냄
  • 하지만 대부분의 애플리케이션은 순간적으로 요청이 몰리는 현상(burst)이 발생
1
2
3
4
5
location /login/ {
    limit_req zone=mylimit burst=20;

    proxy_pass http://my_upstream;
}
  • burst=20 : 지정된 속도(예: 초당 10회 요청)를 초과하는 최대 20개의 요청까지 큐에 저장 가능
    • 예를 들어, 동시에 21개의 요청이 들어오면:
      • 첫 번째 요청은 즉시 처리나머지 20개 요청은 큐에 저장
      • 이후 100ms마다 하나씩 처리됨 (설정된 속도: 10r/s에 맞춰)
      • 만약 큐가 꽉 찬 상태에서 또 요청이 들어오면, 그 요청은 503 에러로 거부됨

nodelay

  • burst 설정은 요청을 일정 간격으로 처리하므로 트래픽이 부드럽게 흐르지만, 사용자 입장에서는 사이트가 느려진 것처럼 느껴질 수 있다.
  • 예를 들어 큐의 마지막 요청(20번째)은 최대 2초까지 기다려야 하며, 이 응답은 이미 쓸모 없어졌을 수도 있다.
1
2
3
4
5
location /login/ {
    limit_req zone=mylimit burst=20 nodelay;

    proxy_pass http://my_upstream;
}
  • nodelay는 큐 슬롯은 유지하지만, 큐에 넣은 요청을 즉시 처리
  • 단, 속도 제한은 여전히 적용되며, 슬롯을 차지한 후 일정 시간(예: 100ms)이 지나야 슬롯이 다시 사용 가능
    • 즉, 매100ms마다 슬롯 한 개를 비움

예시 상황

  1. 큐가 비어 있는 상태에서 IP 하나가 동시에 21개의 요청을 보냄
    • 21개 모두 즉시 처리
    • 이 중 20개는 큐 슬롯을 ‘점유 중’으로 표시
    • 슬롯은 이후 100ms마다 하나씩 해제됨
  2. 501ms 후 20개 요청이 들어옴
    • 슬롯 5개가 해제되어 있음
    • 5개 요청 처리, 15개 요청 거부 (503)

delay

1
2
3
4
5
6
7
8
9
limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;

server {
    listen 80;
    location / {
        limit_req zone=ip burst=12 delay=8;
        proxy_pass http://website;
    }
}
  • delay를 지정하면 일정 개수까지는 바로 처리하고, 나머지는 초당 요청 제한(rate)을 지키며 처리

image

출처 : https://blog.nginx.org/blog/rate-limiting-nginx

특정 IP 별도 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/24 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;

server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;

        # ...
    }
}
  • geo
    • 클라이언트 IP를 기반으로 $limit 변수 값을 설정
    • 10.0.0.0/8 및 192.168.0.0/24 대역에 속한 IP는 $limit = 0
    • 그 외 IP는 $limit = 1 (기본값)
  • map
    • $limit 값을 기준으로 $limit_key를 설정
      • $limit == 0$limit_key = "" (빈 문자열)
      • $limit == 1$limit_key = $binary_remote_addr
  • limit_req_zone
    • $limit_key를 기준으로 요청 속도를 제한
    • 키가 빈 문자열(““)이면 속도 제한이 적용되지 않음
      • 즉, 허용된 IP(allowlist)는 제한 없이 통과되고, 나머지는 초당 5건으로 제한됨

여러 개의 limit_req 디렉티브를 동시에 사용

  • 각 제한이 서로 충돌하면: 가장 강한 제한(최소 속도 or 가장 긴 지연)이 적용됨
1
2
3
4
5
6
7
8
9
10
11
12
http {
    limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;

    server {
        location / {
            limit_req zone=req_zone burst=10 nodelay;
            limit_req zone=req_zone_wl burst=20 nodelay;
            # ...
        }
    }
}
대상 IPreq_zone (5r/s) 적용req_zone_wl (15r/s) 적용실제 제한
허용된 IPX (key가 “”)O (binary IP로 적용됨)15r/s 제한
허용되지 않은 IPOO5r/s 제한 (더 엄격한 제한 적용됨)

드는 생각

1. 정확한 요청 수로 제한해야하는 경우에는 적합하지 않은 것 같다.

  • 예를 들어, 1초에 최대 1000개의 요청을 허용하는 것을 기대하고 1000r/s로 세팅한 경우, busrt, nodelay를 사용하면 한번에 동시에 1000개 요청이 업스트림 서버로 전달될 수 있지만, 0.99초에 1000개가 한번에 들어오고 1.01초에 1000개가 또 들어오면 아직 점유된 슬롯들이 대부분 해제되지 않은 상태라 대부분의 요청이 거절될 것
  • 또한, limit_req_zone의 sync 설명에서처럼 요청량이 많은 상황에서 공유 메모리가 어느정도로 잘 동기화 되는지는 미지수인 것 같음
  • 가장 길게 줄 수 있는 지연이 1r/m 이기 때문에 그 이상으로 제한하기는 어렵다.
  • rate=1r/60m 이런식의 설정이 안되는 이유 (아래 코드 참고, r/s, r/m만 가능)

ngx_http_limit_req_module.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ngx_http_limit_req_module.c
static char *ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ...

      if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
          len = value[i].len;
          p = value[i].data + len - 3;

          if (ngx_strncmp(p, "r/s", 3) == 0) {
              scale = 1;
              len -= 3;

          } else if (ngx_strncmp(p, "r/m", 3) == 0) {
              scale = 60;
              len -= 3;
          }

          rate = ngx_atoi(value[i].data + 5, len - 5);
          if (rate <= 0) {
              ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                 "invalid rate \"%V\"", &value[i]);
        return NGX_CONF_ERROR;
      }

      continue;
  }

  ...
}

2. 1차적인 어뷰징 방지 수단으로는 적절할 수 있을 것 같다.

  • 예를 들어 웹사이트의 메인 페이지에서 로그인하지 않아도 이용할 수 있는 기능들이 있을 때, (사용자에 의해서든, 봇에 의해서든) 불필요하게 많이 호출되는 것을 방지하기 위해 IP 기반으로 처리율을 제한하면 불필요하게 업스트림 서버까지 요청이 가는 것을 어느정도는 막을 수 있을 것으로 생각

참고 자료

This post is licensed under CC BY 4.0 by the author.