Nginx Rate Limiting 심화

Nginx Rate Limiting이 필요한 이유

API 서버를 운영하다 보면 DDoS 공격, 브루트포스 로그인 시도, 크롤러 폭주 등 비정상적인 트래픽에 노출됩니다. 애플리케이션 레벨에서 처리하면 이미 서버 리소스를 소모한 뒤이므로, 리버스 프록시 단에서 요청을 제한하는 것이 훨씬 효율적입니다. Nginx의 ngx_http_limit_req_modulengx_http_limit_conn_module은 이를 위한 강력한 내장 모듈입니다.

limit_req: 요청 속도 제한

가장 핵심적인 디렉티브입니다. Leaky Bucket(누수 버킷) 알고리즘을 기반으로 초당/분당 요청 수를 제한합니다:

http {
    # 1) 공유 메모리 존 정의: IP별 초당 10개 요청 허용
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # 2) 로그인 엔드포인트: 더 엄격하게 분당 5개
    limit_req_zone $binary_remote_addr zone=login_limit:5m rate=5r/m;

    server {
        listen 80;
        server_name api.example.com;

        # 일반 API: burst 20개까지 대기열 허용
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            proxy_pass http://backend;
        }

        # 로그인: 엄격한 제한
        location /api/auth/login {
            limit_req zone=login_limit burst=3 nodelay;
            proxy_pass http://backend;
        }
    }
}

주요 파라미터 설명:

파라미터 설명
zone 공유 메모리 이름:크기. 1MB당 약 16,000개 IP 상태 저장
rate 허용 속도. 10r/s=초당 10개, 5r/m=분당 5개
burst rate 초과 시 대기열 크기. 초과분은 503 반환
nodelay burst 내 요청을 지연 없이 즉시 처리
delay=N burst 중 N개까지는 즉시, 나머지는 지연 처리

burst와 nodelay/delay 동작 원리

Leaky Bucket의 동작을 정확히 이해해야 올바른 설정이 가능합니다:

# Case 1: burst 없음 → rate 초과 즉시 503
limit_req zone=api_limit;
# 10r/s → 100ms마다 1개만 통과, 나머지 전부 503

# Case 2: burst=20 (delay 없음) → 초과분 큐에 대기
limit_req zone=api_limit burst=20;
# 순간 30개 요청 → 10개 즉시 처리, 20개 큐 대기(100ms 간격 처리)
# 사용자 입장에서 응답 지연 발생

# Case 3: burst=20 nodelay → 초과분 즉시 처리 but 버킷 소모
limit_req zone=api_limit burst=20 nodelay;
# 순간 30개 → 30개 모두 즉시 처리, but 이후 버킷 충전까지 503
# 가장 일반적인 설정

# Case 4: burst=20 delay=8 → 하이브리드
limit_req zone=api_limit burst=20 delay=8;
# 처음 8개 즉시 처리, 9~30번째는 100ms 간격 지연, 31번째부터 503

운영 팁: 대부분의 API에는 burst=N nodelay 조합이 적합합니다. 순간적인 트래픽 스파이크를 허용하면서도 지속적인 과부하는 차단합니다.

limit_conn: 동시 연결 수 제한

limit_req가 “속도”를 제한한다면, limit_conn동시에 열린 연결 수를 제한합니다. 파일 다운로드나 WebSocket처럼 오래 유지되는 연결에 유용합니다:

http {
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        # IP당 동시 연결 최대 50개
        location /downloads/ {
            limit_conn conn_limit 50;
            limit_conn_status 429;       # 기본 503 대신 429 반환
            limit_rate 1m;               # 연결당 다운로드 속도 1MB/s
        }
    }
}

키 설계: IP 외 다양한 기준

$binary_remote_addr(클라이언트 IP)만으로는 부족한 경우가 많습니다. API 키, JWT 사용자, URI별로 차등 제한할 수 있습니다:

# API 키별 제한
map $http_x_api_key $api_key_limit {
    default         $binary_remote_addr;  # API 키 없으면 IP로 폴백
    "~."            $http_x_api_key;      # API 키 있으면 키 기준
}
limit_req_zone $api_key_limit zone=api_by_key:20m rate=100r/s;

# URI + IP 조합 (엔드포인트별 IP 제한)
limit_req_zone $binary_remote_addr$uri zone=per_endpoint:20m rate=5r/s;

# JWT sub 클레임 기준 (auth_request 모듈 사용 시)
limit_req_zone $http_x_user_id zone=per_user:10m rate=30r/s;

화이트리스트와 차등 제한

내부 서비스나 관리자 IP는 제한에서 제외하고, 유료·무료 사용자에게 차등 제한을 적용하는 패턴입니다:

# geo 블록으로 화이트리스트 설정
geo $rate_limit_key {
    default         $binary_remote_addr;
    10.0.0.0/8      "";     # 내부 네트워크 → 빈 문자열 = 제한 없음
    172.16.0.0/12   "";     # Docker 내부
    192.168.0.0/16  "";     # 사설 네트워크
}

# 빈 문자열은 limit_req_zone 키로 사용 불가 → 자동 제외
limit_req_zone $rate_limit_key zone=external:10m rate=10r/s;

# 차등 제한: 플랜별 다른 rate
map $http_x_plan $plan_rate_key {
    "premium"   "";                           # 프리미엄은 제한 없음
    default     $binary_remote_addr;          # 나머지는 IP 기준 제한
}
limit_req_zone $plan_rate_key zone=tiered:10m rate=20r/s;

커스텀 에러 응답

기본 503 대신 429 Too Many Requests와 함께 JSON 에러 바디를 반환하면 API 클라이언트가 올바르게 처리할 수 있습니다:

http {
    limit_req_status 429;       # 상태 코드 변경

    server {
        # 429 에러 페이지를 내부 location으로 처리
        error_page 429 = @rate_limited;

        location @rate_limited {
            default_type application/json;
            add_header Retry-After 60 always;
            add_header X-RateLimit-Limit 10 always;
            return 429 '{"error":"too_many_requests","message":"Rate limit exceeded. Retry after 60 seconds.","retry_after":60}';
        }

        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

로깅과 모니터링

Rate limiting이 실제로 동작하는지 모니터링하려면 로그 레벨커스텀 로그 포맷을 설정합니다:

http {
    # rate limit 로그 레벨 (기본 error → warn으로 낮춤)
    limit_req_log_level warn;
    limit_conn_log_level warn;

    # 제한 상태를 access_log에 포함
    log_format rate_log '$remote_addr - $status '
                        '$request_uri '
                        'limit_req_status=$limit_req_status '
                        'upstream_response_time=$upstream_response_time';

    # $limit_req_status 변수:
    # PASSED  = 정상 통과
    # DELAYED = burst 큐에서 지연 후 처리
    # REJECTED = 503/429 반환
    # DELAYED_DRY_RUN / REJECTED_DRY_RUN = dry_run 모드

    server {
        access_log /var/log/nginx/rate_limit.log rate_log;
    }
}

Prometheus로 메트릭을 수집하려면 nginx-prometheus-exporter와 함께 stub_status를 활용하세요.

dry_run 모드로 안전하게 테스트

프로덕션에 바로 적용하기 불안하다면 limit_req_dry_run on으로 실제 차단 없이 로그만 기록할 수 있습니다:

location /api/ {
    limit_req zone=api_limit burst=20 nodelay;
    limit_req_dry_run on;      # 차단하지 않고 로그만 기록
    proxy_pass http://backend;
}
# error.log에 "limiting requests, dry run" 메시지 확인 후
# dry_run off로 전환

실전 설정 예시: 전체 구성

종합적인 프로덕션 설정 예시입니다. Nginx 리버스 프록시 가이드의 기본 설정에 rate limiting을 추가한 형태입니다:

http {
    # === Rate Limit Zones ===
    limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
    limit_req_zone $binary_remote_addr zone=auth:5m rate=5r/m;
    limit_req_zone $binary_remote_addr zone=upload:5m rate=2r/s;
    limit_conn_zone $binary_remote_addr zone=download:10m;

    # === 전역 설정 ===
    limit_req_status 429;
    limit_conn_status 429;
    limit_req_log_level warn;

    # === 화이트리스트 ===
    geo $limit_key {
        default         $binary_remote_addr;
        10.0.0.0/8      "";
        127.0.0.1       "";
    }

    server {
        listen 443 ssl http2;
        server_name api.example.com;

        # 429 에러 핸들링
        error_page 429 = @rate_limited;
        location @rate_limited {
            default_type application/json;
            add_header Retry-After 30 always;
            return 429 '{"error":"rate_limit_exceeded"}';
        }

        # 일반 API
        location /api/ {
            limit_req zone=general burst=50 nodelay;
            proxy_pass http://backend;
        }

        # 인증 엔드포인트 (엄격)
        location /api/auth/ {
            limit_req zone=auth burst=3 nodelay;
            proxy_pass http://backend;
        }

        # 파일 업로드
        location /api/upload {
            limit_req zone=upload burst=5;
            client_max_body_size 50m;
            proxy_pass http://backend;
        }

        # 파일 다운로드 (동시 연결 + 속도 제한)
        location /files/ {
            limit_conn download 10;
            limit_rate 5m;
            alias /var/www/files/;
        }
    }
}

주의사항과 베스트 프랙티스

  • CDN/로드밸런서 뒤에서는 $binary_remote_addr 대신 $http_x_forwarded_for에서 실제 IP를 추출하세요. set_real_ip_fromreal_ip_header 디렉티브를 함께 설정해야 합니다.
  • 공유 메모리 크기를 너무 작게 잡으면 오래된 항목이 밀려나 제한이 풀립니다. 예상 동시 접속 IP 수의 2배로 설정하세요.
  • rate는 보수적으로 시작하고 dry_run 로그를 분석한 뒤 조정하세요. 정상 사용자가 차단되면 신뢰를 잃습니다.
  • 헬스체크 엔드포인트는 반드시 화이트리스트에 포함하여 모니터링 도구가 차단되지 않도록 하세요.
  • limit_req와 limit_conn을 함께 사용하면 속도와 동시 연결 모두 제어할 수 있어 더 강력합니다.

마치며

Nginx rate limiting은 애플리케이션 코드 변경 없이 인프라 레벨에서 트래픽을 보호하는 가장 효율적인 방법입니다. Leaky Bucket 알고리즘의 burst/nodelay 동작을 정확히 이해하고, dry_run으로 충분히 테스트한 뒤 적용하세요. geo 블록으로 화이트리스트를 관리하고, 429 응답에 Retry-After 헤더를 포함하면 클라이언트 친화적인 rate limiting을 구현할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux