DevOps & SRE/Nginx

[Nginx RateLimit] Nginx에 RateLimit 적용해보기

반응형

Rate Limiting

주어진 특정한 시간 동안 제한된 HTTP 요청량을 제한할 수 있도록 하는 기능.


Rate Limiting을 하는 이유


  • 보안 측면의 목적 (Brute force attack 방지 등)
  • 비정상적인 요청 방지 (DDoS 공격 등 방지)
  • 서버가 처리할 수 있는 요청의 임계 값을 넘어선 요청을 방지함으로써 서버의 안정성을 높이는 역할

서버의 안정성을 위해

  • 예를 들어서 비정상 호출 방지를 위해 동일 IP에서 초당 요청할 수 있는 횟수를 10회로 제한한다.
  • 서버의 안정성을 위해 서버에 들어오는 최대 요청 수를 초당 500회로 제한한다.

등 제한을 추가할 필요가 있을 때, Nginx Rate Limiting 기능을 사용하면 유용하다.


Nginx


  • nginx에서도 RateLimit를 위한 모듈을 제공하고 있다.
  • ex) 참고로 RateLimit을 위해서 꼭 nginx를 사용해야 하는 것은 아니고, Nginx RateLimit 말고도 다양한 솔루션을 이용할 수 있다. 


Nginx Rate Limit 동작 방식


기본적으로 Nginx Rate Limit은 Leacky Bucket 알고리즘과 유사하게 동작합니다.


Leaky Bucket Algorithm 

Leack Bucket Algorithm (http://www.yes24.com/Product/Goods/102819435) 참고

  • 요청 처리율이 고정되어 있는 FIFO 큐
  • 요청이 도착하면 큐가 가득 차 있는지 확인 후(버킷 크기), 빈자리가 있는 경우 큐에 요청을 추가.
  • 큐가 가득 차 있는 경우, 새 요청은 버려지며 (503 or 429 에러를 반환하는 등), 지정된 시간마다 (처리율에 따라) 큐에서 요청을 꺼내어 처리한다.

장점

  • 큐의 크기가 제한되어 있어 메모리 사용 측면에서 효율적
  • 고정된 처리율을 갖고 있어 안정적 출력이 필요한 경우 적합

단점

  • 단시간에 많은 트래픽이 몰리는 경우 큐에는 오래된 요청들이 쌓이게 되고, 그 요청들을 제때 처리하지 못하면 최신 요청들은 버려지게 된다.
  • 버킷 크기와 처리율을 올바르게 튜닝하기가 까다로움


RateLimit 적용


nginx에서 RateLimit 제한을 위해서 요청 수를 제한하는 limit_req, 커넥션 수를 제한하는 limit_conn 모듈을 제공하고 있습니다.

1. limit_req - 요청 제한

http { limit_req_zone $binary_remote_addr zone=request_limit_per_ip:10m rate=10r/s;

  server {
      location / {
          limit_req zone=request_limit_per_ip burst=10 nodelay;
      }
  }
}


2. limit_conn - 커넥션 제한

http {

  limit_conn_zone $server_name zone=request_limit_per_server:10m;

  server {
      location / {
          limit_conn request_limit_per_server 10;
      }
  }

}
  • limit_req와 마찬가지로 $binary_remote_addr, $server_name 등 변수 등을 상황에 맞게 사용 가능.
  • 동시에 서버에 연결되는 커넥션 수로 10개로 제한하겠다는 의미
limit_req_status        429; # RateLimit 제한에 걸리면 응답하는 Http Status
limit_req_log_level     error; # 로그 레벨: warn, error 등


예시로 보는 RateLimit


동일 IP 당 Rate를 10r/s로 제한한다

http {
  limit_req_zone $binary_remote_addr zone=request_limit_per_ip:10m rate=10r/s;      
   # TPS 제한에 대한 파라미터 정의
    
   server {
      location / {
        limit_req zone=request_limit_per_ip noburst delay=10; 
        # 특정 location에 대한 TPS 제한을 활성화 (noburst, delay는 아래에서 설명 예정)

         proxy_pass {{프록시할_서버_URL ex) <http://localhost:8080>}};
    }
}

Key: $binary_remote_addr

  • Client's IP Address
  • 각각의 클라이언트 IP를 키값으로 요청에 제한을 두겠다는 의미. (동일 IP당 10 TPS 제한)
  • $remoate_addr (IP Address as String)을 사용할 경우 좀 더 공간을 차지하기 때문에 $binary_remote_addr을 사용
  • 이외에 다양한 키를 잡을 수 있으며, nginx variables는 다음 링크를 참고해주세요 -> https://www.javatpoint.com/nginx-variables


Zone: 공유 메모리 개념

  • 예를 들어서 각 사용자당 1초에 10번씩 요청을 제한할 경우. 요청 횟수를 저장하는 FIFO 공유 메모리라고 생각하면 될 거 같음.
  • zone=keyword:10m 형태로 사용한다. 10m은 메모리 크기를 의미함.
  • 대략 16,000개의 IP가 1m을 차지함으로 10m면 160,000 주소를 저장할 수 있다고 볼 수 있음. (URL 등 다른 키 값을 잡을 경우 계산이 필요)


Rate: 최대 요청률을 지정.

  • 예를 들어 rate=10r/s인 경우 1000ms(1s) / 10으로 100ms마다 하나씩 요청을 허용하겠다는 의미
    (100ms 당 1개의 요청으로 고정된 처리율로 제한)

주의 할 점은 nginx RateLimit가 Leack Bucket Algorithm 기반으로, rate만 설정할 경우, 1초에 10개의 요청을 허용하고, 그 이후로 들어오는 요청들을 거부하지 않는 개념이 아니라, 100ms 마다 하나의 요청을 처리할 수 있는 개념이다.
(고정된 처리율을 제한하는 개념)

만약 10TPS 같이 1초에 10개의 요청까지만 허용하고, 그 이후에 들어오는 요청을 거부하는 형태로 제한하기 원하면, burst + nodelay 설정을 추가해서 버퍼 큐를 두어야 한다. (관련 내용은 Burst, nodelay에서 설명)


Burst: 최대 제한율을 넘어선 요청들을 잠시 보관해두는 큐 개념

  • 만약 최대 요청 제한보다 많은 요청이 들어왔을 경우, 503 에러(커스텀 가능)를 반환하는 것은 사용자 측면에서 좋지 못하다. 대신 조금 대기했다가, 요청을 보낼 수 있을 때, 요청을 보내 처리하게 하는 것이 Burst라고 할 수 있음.
  • burst=9이 의미하는 것은, 최대 제한율을 넘어 얼마나 요청이 초과될 수 있는지를 의미한다.


nodelay

  • burst 설정을 하면, Delay가 심해 사용자에게 느리게 응답할 수 있음.
  • nodelay 옵션은 요청들 사이에 허용되는 시간 간격을 제한하지 않고 속도 제한을 적용할 경우에 유용하다.
  • 이러한 이유들로 limit_req에 burst와 nodelay 파라미터를 포함하길 권장한다고 합니다.
limit_req zone=request_limit_per_server burst=1800 nodelay;
limit_req zone=request_limit_per_client burst=20 nodelay;

 

burst + nodelay 를 설정하면, 고정된 처리량으로 동작하는 Leacky bucket이 아닌 Token Bucket 알고리즘과 유사하게 동작시킬 수 있다. (Token Bucket의 경우 정해진 시간마다 토큰이 생성되는데, 토큰이 있는 경우 요청이 처리될 수 있음 다만, Leacky Bucket과 다르게 고정된 처리량이 아닌 토큰이 있으면 버스트성으로 요청을 처리할 수 있게 됨)

cf) Token Bucket 알고리즘

  • 토큰 버킷은 지정된 용량(버킷 크기)을 갖는 컨테이너로, 사전 설정된 양의 토큰(토큰 공급률)이 주기적으로 채워진다.
    • 각 요청은 처리될 때 마다 하나의 토큰을 사용한다.
    • 요청이 들어오면 먼저 충분한 토큰이 있는지 검사한 후, 있는 경우 버킷에서 토큰 하나를 꺼낸 후 요청을 시스템에게 전달한다.
    • 만약 충분한 토큰이 없는 경우, 해당 요청은 버려진다.


RateLimit 조건 (Key 값)을 무엇으로 잡을 것인가?


$binary_remote_addr

  • RateLimit Per IP (동일 IP당 요청 횟수를 제한한다)
limit_req_zone $binary_remote_addr zone=limit_request_per_ip:10m rate=10r/s; # 동일 IP당 10TPS로 제한
server {
    location / {
        limit_req zone=limit_request_per_ip;
    } 
}

 

$server_name

  • RateLimit Per Server 서버당 최대 요청 횟수를 제한한다
limit_req_zone $server_name zone=limit_request_per_server:100m rate=100r/s; # 서버의 전체 TPS를 100TPS로 제한

server {
    location / {
        limit_req zone=limit_request_per_server;
    } 
}

 

$binary_remote_addr:$uri

  • RateLimit Per IP & URL (동일 IP & 동일 URI로의 요청 횟수를 제한한다)
  • (URL의 길이가 길어짐에 따라, 메모리가 많이 필요해집니다)
limit_req_zone $binary_remote_addr:$uri zone=request_limit_per_client_and_uri:10m rate=3r/s;


cf) 여러 조건을 AND 조건으로 제한할 수도 있습니다.

ex) 동일 IP당 요청을 10TPS로 제한하면서, 동시에 서버당 요청 제한을 100 TPS로 제한

limit_req_zone $binary_remote_addr zone=limit_request_per_ip:10m rate=10r/s; # 동일 IP당 10TPS로 제한
limit_req_zone $server_name zone=limit_request_per_server:100m rate=100r/s;

server {
  location / {
    limit_req zone=limit_request_per_ip;
    limit_req zone=limit_request_per_server;
  }
}


그 외 Tips...


1. 프록시 환경 실제 사용자의 IP를 추출하는 방법

  • MSA 구조에서 사용자로부터 직접적으로 들어오지 않고 서버를 거쳐서 들어오는 경우, remote_addr에는 실제 사용자의 IP가 아닌 이전 서버의 IP가 넘어오는 문제가 존재해서 real ip 설정 해줘야 함.


2. Whitelist 적용 방법

geo $limit {
    default 1;
    1.2.3.4 0; # 1.2.3.4 IP에 대해서 RateLimit를 적용하지 않는다
    2.3.4.5 0; # 2.3.4.5 IP에 대해서 RateLimit를 적용하지 않는다
}

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;

        # ...
    }
}

특정 IP Range에 대해서 RateLimit를 걸고 싶지 않으면 다음과 같이 WhiteList를 적용할 수도 있다.

  • 내부망 IP로 들어오는 경우 WhiteList를 추가하는 등 whiteList를 적용할 수 있다.


cf) 참고로 다음과 같이 모듈화를 해서 나눠서 관리할 수 있습니다.

  • ratelimit-http.conf
## RateLimit WhiteList
geo $limit {
    default 1;
    include {{NGINX 경로}}/nginx/conf/ratelimit-whitelist.conf;
}

map $limit $limit_per_ip {
    0 ""; # whiteList의 IP들은 따로 TPS Per IP 제한을 걸지 않음.
    1 $end_client_ip;
}

map $limit $limit_per_server {
    0 ""; # whiteList의 IP들은 따로 TPS Per Server 제한을 걸지 않음.
    1 $server_name;
}
  • ratelimit-whitelist.conf
### RateLimit WhiteList
1.2.3.4 0;
2.3.4.5 0;


3. dry-run 옵션

  • 실제로 운영에 내보낼 때, 바로 RateLimit를 적용하지 않고, dry-run 옵션을 켜면 실제로는 요청이 차단되지 않지만 로그만 찍히도록 하는 옵션


Nginx RateLimit dry run 설정

limit_req_dry_run on;

dry_run 설정을 enable 하면, 적용 수치 이상의 요청이 들어왔을 때, 실제로 에러가 반환되지 않고 (429 에러), 정상적으로 응답을 주되, 초과한 요청에 대해 로그만 찍힙니다.


4. RateLimit 모니터링을 위한 로그 설정

기존의 access.log, error.log와 별도로 RateLimit에 걸린 로그들만 별도로 관리해야 했습니다. (모니터링을 위해)

운영 환경에서 RateLimit에 대한 모니터링을 위해 access.log, error.log와 별도로 RateLimit 로그들을 별도로 관리해서, 해당 모니터링 시스템을 구축할 수 있다.

ex) filebeat로 RateLimit 로그들을 ES로 보내는 등… 모니터링 시스템을 구축할 수 있다


RateLimit(dry-run)에 걸린 로그들만 추출하는 방법

## RateLimit Logging Format
log_format ratelimit escape=json '{'
    ... (생략)
    '"limit_req_status": "$limit_req_status"' 
     # RateLimit에 걸렸는지 확인할 수 있는 유용한 파라미터 (넘어오는 값들은 아래를 참고)
'}';

# RateLimit에 걸린 로그들만 분리한다
map $limit_req_status $catch_ratelimit {
    REJECTED 1; # REJECT(RateLimit에 걸린 경우) -> true
    default 0; # 나머지는 false
}

# RateLimit dry-run에 걸린 로그들만 분리한다
map $limit_req_status $catch_ratelimit_dry_run {
    REJECTED_DRY_RUN 1;
    default 0;
}
  • limit_req_status
    • PASSED: RateLimt 정상 통과
    • REJECTED: 실제로 RateLimit에 걸린 상태
    • REJECTED_DRY_RUN: RateLimit 제한에 걸렸지만, dry-run 설정으로 로그만 뜨는 상태
    • DELAY, DELAYED_DRY_RUN: no-delay 설정으로 해당 상태가 반환되지 않음


로그 설정

access_log {{NGINX 로그 디렉터리}}/rate-limit.log ratelimit if=$catch_ratelimit;
access_log {{NGINX 로그 디렉터리}}/rate-limit-dry-run.log ratelimit if=$catch_ratelimit_dry_run;

1. access.log

가능한 사항

  • 실제 사용자의 IP를 로그에 포함할 수 있는 등 로그의 내용을 커스텀하게 설정할 수 있습니다.
  • RateLimit에 걸린 로그만 필터링해서 로그 파일을 생성할 수 있습니다.


불가능한 사항

  • access.log에서 error.log에만 포함되는 제한이 걸린 Nginx Rate Zone 등의 정보를 가져올 수 없는 문제가 존재합니다.


2. error.log

  • 참고로 어떤 RateLimit 조건 ex) TPS per IP, TPS per server에 걸렸는지 확인하려면 error.log에서 확인할 수 있다.

가능한 사항

  • 제한에 걸린 zone 정보 및 몇 개의 요청이 제한되었는지 등 RateLimit에 대한 상세 정보가 포함됩니다


불가능한 사항

  • 실제 사용자의 IP를 로그에 포함할 수 있는 등 에러 로그의 내용을 커스텀하게 설정할 수 없습니다.
  • RateLimit에 걸린 로그만 필터링해서 로그 파일을 생성할 수 없습니다.
2022/02/25 16:08:46 [error] 28205#0: *31 limiting requests, excess: 10.179 by zone "limit_request_per_client", client: 127.0.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:2000"

참고로 error.log에서 RateLimit 관련된 로그를 필터링해서 전송하려면, filebeat processors, logstash 등을 사용해서 error.log 중 RateLimit 관련된 로그를 (http status 429로 넘어오는 등을 통한 필터링을 통해) ES로 보내주는 방법 등이 있다.

반응형

'DevOps & SRE > Nginx' 카테고리의 다른 글

Nginx 동작 방식부터 프록시, 로드밸런서까지  (0) 2021.07.18