문제를 겪고 있는 NGINX 사용자를 지원하다 보면 다른 사용자의 구성에서 반복해서 보아 온 것과 동일한 구성 실수를 자주 발견합니다. 심지어 동료 NGINX 엔지니어가 작성한 구성에서도 이런 실수가 나타나곤 합니다! 이 블로그에서는 가장 흔한 오류 10가지를 살펴보고, 무엇이 잘못되었으며 어떻게 해결할 수 있는지 설명합니다.
- 워커당 파일 디스크립터 수 부족
- The
error_logoff지시어 - 업스트림 서버에 대한 keepalive 연결을 활성화하지 않음
- 지시어 상속이 어떻게 작동하는지 잊어버리는 것
- The
proxy_bufferingoff지시어 - 잘못된
if지시어 사용 - 과도한 상태 확인
- 메트릭에 대한 보안되지 않은 액세스
- 사용:
ip_hash(모든 트래픽이 동일한 /24 CIDR 블록에서 유입되는 경우) - 업스트림 그룹을 활용하지 않음
실수 1: 워커당 파일 디스크립터 수 부족
The worker_connections 지시어는 NGINX 워커 프로세스가 동시에 열어 둘 수 있는 최대 연결 수를 설정합니다(기본값은 512). 클라이언트 연결뿐만 아니라 프록시 서버와의 연결 등 모든 유형의 연결이 이 최대 연결 수에 포함됩니다. 하지만 워커당 동시 연결 수에는 궁극적으로 또 다른 제한이 있다는 점에 유의해야 합니다. 바로 각 프로세스에 할당되는 파일 디스크립터(FD)의 최대 개수에 대한 운영 체제의 제한입니다. 최신 UNIX 배포판의 기본 제한은 1024입니다.
아주 소규모인 경우를 제외하면 대부분의 NGINX 배포 환경에서 워커당 연결 수를 512개로 제한하는 것은 너무 적을 수 있습니다. 실제로 NGINX Open Source 바이너리 및 NGINX Plus와 함께 배포되는 기본 nginx.conf 파일에서는 이 제한을 1024개로 늘립니다.
흔히 발생하는 구성 오류는 파일 디스크립터(FD) 제한을 worker_connections 값의 두 배 이상으로 늘리지 않는 것입니다. 이 문제를 해결하려면 기본 구성 컨텍스트에서 worker_rlimit_nofile 지시어를 사용하여 해당 값을 설정해야 합니다.
더 많은 FD가 필요한 이유는 다음과 같습니다. NGINX 워커 프로세스에서 클라이언트 또는 업스트림 서버로 연결할 때마다 FD가 하나씩 사용됩니다. NGINX가 웹 서버로 작동할 때는 클라이언트 연결에 FD 하나, 제공되는 파일마다 FD 하나를 사용하므로 클라이언트당 최소 두 개의 FD가 필요합니다(단, 대부분의 웹 페이지는 여러 파일로 구성됩니다). 프록시 서버로 작동할 때는 클라이언트 연결과 업스트림 서버 연결에 각각 FD 하나를 사용하며, 서버의 응답을 임시로 저장하는 파일에 세 번째 FD를 사용할 수도 있습니다. 캐싱 서버로 작동할 때 NGINX는 캐시된 응답에 대해서는 웹 서버처럼 동작하고, 캐시가 비어 있거나 만료된 경우에는 프록시 서버처럼 동작합니다.
또한 NGINX는 로그 파일마다 하나의 FD를 사용하고 마스터 프로세스와 통신하는 데 몇 개의 FD를 사용하지만, 일반적으로 이 수는 연결 및 파일에 사용되는 FD 수에 비해 적습니다.
UNIX에서는 프로세스당 FD 수를 설정하는 여러 가지 방법을 제공합니다:
- 셸에서 NGINX를 시작하는 경우
ulimit명령 - NGINX를 서비스로 시작하는 경우
init스크립트 또는systemd서비스 매니페스트 변수 - The /etc/security/limits.conf 파일
하지만 사용할 방법은 NGINX를 시작하는 방식에 따라 달라지는 반면, worker_rlimit_nofile은 NGINX를 어떤 방식으로 시작하든 작동합니다.
FD 수에는 시스템 전체에 적용되는 제한도 있으며, OS의 sysctl fs.file-max 명령으로 설정할 수 있습니다. 일반적으로 충분히 큰 값이지만, 모든 NGINX 워커 프로세스가 사용할 수 있는 파일 디스크립터의 최대 개수(worker_rlimit_nofile * worker_processes)가 fs.file‑max보다 훨씬 작은지 확인하는 것이 좋습니다. NGINX가 어떤 이유로든 사용 가능한 FD를 모두 사용하면(예: DoS 공격 중) 문제를 해결하기 위해 시스템에 로그인하는 것조차 불가능해집니다.
실수 2: error_log off 지시어
흔히 하는 실수는 error_log off 지시문이 로깅을 비활성화한다고 생각하는 것입니다. 실제로 access_log 지시문과 달리 error_log에는 off 매개변수를 사용할 수 없습니다. 구성에 error_log off 지시문을 포함하면 NGINX는 NGINX 구성 파일의 기본 디렉터리(일반적으로 /etc/nginx)에 off라는 이름의 오류 로그 파일을 생성합니다.
오류 로그는 NGINX 관련 문제를 디버깅할 때 매우 중요한 정보원이므로 비활성화하지 않는 것이 좋습니다. 하지만 저장 공간이 매우 제한적이어서 로그 데이터가 사용 가능한 디스크 공간을 모두 소진할 가능성이 있다면 오류 로깅을 비활성화하는 것이 적절할 수 있습니다. 기본 구성 컨텍스트에 다음 지시어를 포함하세요:
error_log /dev/null emerg;이 지시문은 NGINX가 구성을 읽고 검증한 후에야 적용된다는 점에 유의하세요. 따라서 NGINX가 시작되거나 구성이 다시 로드될 때마다 구성 검증이 완료될 때까지 기본 오류 로그 위치(일반적으로 /var/log/nginx/error.log)에 로그가 기록될 수 있습니다. 로그 디렉터리를 변경하려면 -e <error_log_location> 매개변수를 nginx 명령에 포함하세요.
실수 3: 업스트림 서버에 대한 Keepalive 연결을 활성화하지 않음
기본적으로 NGINX는 새로운 요청이 들어올 때마다 업스트림(백엔드) 서버에 새 연결을 엽니다. 이는 안전하지만 비효율적입니다. 연결을 설정하려면 NGINX와 서버가 패킷 3개를 주고받아야 하고, 연결을 종료하려면 패킷 3~4개를 주고받아야 하기 때문입니다.
트래픽이 많은 상황에서 요청마다 새 연결을 열면 시스템 리소스가 고갈되어 연결을 전혀 열 수 없게 될 수 있습니다. 그 이유는 다음과 같습니다. 각 연결에서 소스 주소, 소스 포트, 대상 주소, 대상 포트로 구성된 4-튜플은 고유해야 합니다. NGINX에서 업스트림 서버로 연결할 때는 이 요소 중 세 가지(첫 번째, 세 번째, 네 번째)가 고정되므로 소스 포트만 변수로 남습니다. 연결이 닫히면 Linux 소켓은 2분 동안 TIME‑WAIT 상태로 유지되며, 트래픽이 많을 때는 이로 인해 사용 가능한 소스 포트 풀이 고갈될 가능성이 커집니다. 이런 상황이 발생하면 NGINX는 업스트림 서버에 새 연결을 열 수 없습니다.
해결 방법은 NGINX와 업스트림 서버 간에 keepalive 연결을 활성화하는 것입니다. 이렇게 하면 요청이 완료될 때 연결이 종료되지 않고 열린 상태로 유지되어 추가 요청에 재사용됩니다. 이를 통해 소스 포트가 고갈될 가능성을 줄이는 동시에 성능도 향상됩니다.
keepalive 연결을 활성화하려면:
- 각
keepalive지시어를 모든upstream{}블록에 포함하여 각 워커 프로세스의 캐시에 유지할 업스트림 서버와의 유휴 keepalive 연결 수를 설정합니다.keepalive지시어는 NGINX 워커 프로세스가 업스트림 서버에 열 수 있는 총 연결 수를 제한하지 않는다는 점에 유의하세요. 이는 흔히 잘못 알려진 내용입니다. 따라서keepalive의 매개변수는 생각만큼 크게 설정할 필요가 없습니다. 이 매개변수는upstream{}블록에 나열된 서버 수의 두 배로 설정하는 것이 좋습니다. 이 값은 NGINX가 모든 서버와 keepalive 연결을 유지하기에 충분히 크면서도, 업스트림 서버가 새로 들어오는 연결도 처리할 수 있을 만큼 작습니다. 또한upstream{}블록에서hash,ip_hash,least_conn,least_time또는random지시어를 사용하여 부하 분산 알고리즘을 지정하는 경우, 해당 지시어는keepalive지시어보다 위에 있어야 합니다. 이는 NGINX 구성에서 지시어의 순서는 중요하지 않다는 일반적인 규칙의 드문 예외 중 하나입니다. - 업스트림 그룹으로 요청을 전달하는
location{}블록에는proxy_pass지시어와 함께 다음 지시어를 포함합니다:
proxy_http_version 1.1;
proxy_set_header "Connection" "";- 기본적으로 NGINX는 업스트림 서버에 연결할 때 HTTP/1.0을 사용하므로, 해당 서버로 전달하는 요청에
Connection:close헤더를 추가합니다. 따라서upstream{}블록에keepalive지시어가 있어도 요청이 완료되면 각 연결이 종료됩니다.proxy_http_version지시어는 NGINX가 대신 HTTP/1.1을 사용하도록 하고,proxy_set_header지시어는Connection헤더에서close값을 제거합니다.
실수 4: 지시어 상속 방식 간과하기
NGINX 지시문은 아래 방향, 즉 “외부에서 내부로” 상속됩니다. 다시 말해 자식 컨텍스트(다른 컨텍스트, 즉 부모 컨텍스트 안에 중첩된 컨텍스트)는 부모 수준에 포함된 지시문의 설정을 상속합니다. 예를 들어 server{} 및 location{} 블록은 모두 http{} 컨텍스트에서 http 수준에 포함된 지시문의 값을 상속하며, server{} 블록의 지시문은 그 안에 있는 모든 자식 location{} 블록에 상속됩니다. 그러나 동일한 지시문이 부모 컨텍스트와 자식 컨텍스트에 모두 포함된 경우 값이 합쳐지는 것이 아니라 자식 컨텍스트의 값이 부모의 값을 재정의합니다.
흔히 저지르는 실수는 배열 지시어에 이 ‘재정의 규칙’이 적용된다는 사실을 잊는 것입니다. 배열 지시어는 여러 컨텍스트에 포함될 수 있을 뿐만 아니라, 하나의 컨텍스트 안에서도 여러 번 포함될 수 있습니다. 예를 들면 proxy_set_header와 add_header가 있습니다. 특히 두 번째 지시어는 이름에 ‘add’가 들어 있어 재정의 규칙을 잊기 쉽습니다.
다음 예를 통해 add_header의 상속 동작 방식을 살펴보겠습니다:
http {
add_header X-HTTP-LEVEL-HEADER 1;
add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;
server {
listen 8080;
location / {
return 200 "OK";
}
}
server {
listen 8081;
add_header X-SERVER-LEVEL-HEADER 1;
location / {
return 200 "OK";
}
location /test {
add_header X-LOCATION-LEVEL-HEADER 1;
return 200 "OK";
}
location /correct {
add_header X-HTTP-LEVEL-HEADER 1;
add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;
add_header X-SERVER-LEVEL-HEADER 1;
add_header X-LOCATION-LEVEL-HEADER 1;
return 200 "OK";
}
}
}포트 8080에서 수신 대기하는 서버의 경우, add_header 지시어가 server{} 블록과 location{} 블록 어디에도 없습니다. 따라서 상속 관계가 명확하며, http{} 컨텍스트에 정의된 두 헤더가 적용됩니다:
% curl -i localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Mon, 21 Feb 2022 10:12:15 GMT
Content-Type: text/plain
Content-Length: 2
Connection: keep-alive
X-HTTP-LEVEL-HEADER: 1 X-ANOTHER-HTTP-LEVEL-HEADER: 1
OK포트 8081에서 수신 대기하는 서버의 경우 add_header 지시어가 server{} 블록에는 있지만 그 하위 location / 블록에는 없습니다. server{} 블록에 정의된 헤더는 http{} 컨텍스트에 정의된 두 헤더를 재정의합니다:
% curl -i localhost:8081
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Mon, 21 Feb 2022 10:12:20 GMT
Content-Type: text/plain
Content-Length: 2
Connection: keep-alive
X-SERVER-LEVEL-HEADER: 1
OK하위 location /test 블록에는 add_header 지시어가 있으며, 이 지시어는 상위 server{} 블록의 헤더와 http{} 컨텍스트의 두 헤더를 모두 재정의합니다:
% curl -i localhost:8081/test
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Mon, 21 Feb 2022 10:12:25 GMT
Content-Type: text/plain
Content-Length: 2
Connection: keep-alive
X-LOCATION-LEVEL-HEADER: 1
OKIf we want a location{} 블록에서 로컬로 정의된 헤더뿐만 아니라 상위 컨텍스트에 정의된 헤더도 유지하려면, 상위 헤더를 location{} 블록 내에서 다시 정의해야 합니다. 이것이 바로 location /correct 블록에서 수행한 작업입니다:
% curl -i localhost:8081/correct
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Mon, 21 Feb 2022 10:12:30 GMT
Content-Type: text/plain
Content-Length: 2
Connection: keep-alive
X-HTTP-LEVEL-HEADER: 1 X-ANOTHER-HTTP-LEVEL-HEADER: 1 X-SERVER-LEVEL-HEADER: 1 X-LOCATION-LEVEL-HEADER: 1
OK실수 5: proxy_buffering off 지시어
NGINX에서는 기본적으로 프록시 버퍼링이 활성화되어 있습니다(proxy_buffering 지시어가 on으로 설정됨). 프록시 버퍼링이란 NGINX가 서버에서 응답을 수신하는 동안 이를 내부 버퍼에 저장하고, 전체 응답이 버퍼링될 때까지 클라이언트로 데이터를 전송하지 않는 것을 의미합니다. 버퍼링은 속도가 느린 클라이언트에 대한 성능을 최적화하는 데 도움이 됩니다. NGINX가 클라이언트가 전체 응답을 수신하는 데 걸리는 시간 동안 응답을 버퍼링하므로, 프록시 서버는 가능한 한 빨리 응답을 반환한 후 다른 요청을 처리할 수 있는 상태로 돌아갈 수 있습니다.
프록시 버퍼링이 비활성화되면 NGINX는 서버 응답을 클라이언트로 전송하기 전에 응답의 첫 부분만 버퍼링합니다. 이때 사용되는 버퍼의 기본 크기는 메모리 페이지 하나(운영 체제에 따라 4KB 또는 8KB)입니다. 일반적으로 이는 응답 헤더를 저장하기에 충분한 크기입니다. 그런 다음 NGINX는 응답을 수신하는 즉시 클라이언트에 동기식으로 전송하므로, 서버는 NGINX가 다음 응답 세그먼트를 수신할 수 있을 때까지 유휴 상태로 대기해야 합니다.
따라서 구성에서 proxy_buffering이 off로 설정된 경우가 자주 보인다는 점은 놀랍습니다. 클라이언트가 체감하는 지연 시간을 줄이려는 의도일 수 있지만, 그 효과는 미미한 반면 부작용은 많습니다. 프록시 버퍼링을 비활성화하면 속도 제한과 캐싱을 구성해도 작동하지 않고 성능이 저하되는 등의 문제가 발생합니다.
프록시 버퍼링을 비활성화하는 것이 적절할 수 있는 사용 사례(예: 롱 폴링)는 극히 일부에 불과하므로 기본값을 변경하지 않는 것이 좋습니다. 자세한 내용은 NGINX Plus 관리 가이드를 참조하세요.
실수 6: if 지시문의 잘못된 사용
The if 지시어는 사용하기 까다로우며, 특히 location{} 블록에서 그렇습니다. 예상대로 작동하지 않는 경우가 많고 세그멘테이션 오류를 일으킬 수도 있습니다. 실제로 매우 까다롭기 때문에 NGINX 위키에는 If is Evil이라는 제목의 문서도 있습니다. 관련 문제와 이를 피하는 방법에 대한 자세한 설명은 해당 문서를 참조하시기 바랍니다.
일반적으로 if{} 블록 내에서 항상 안전하게 사용할 수 있는 지시어는 return과 rewrite뿐입니다. 다음 예에서는 if를 사용하여 X‑Test 헤더가 포함된 요청을 감지합니다(단, 원하는 어떤 조건이든 검사할 수 있습니다). NGINX는 430 (Request Header Fields Too Large) 오류를 반환하고, 명명된 로케이션 @error_430에서 이를 가로챈 다음 요청을 b라는 업스트림 그룹으로 프록시합니다.
location / {
error_page 430 = @error_430;
if ($http_x_test) {
return 430;
}
proxy_pass http://a;
}
location @error_430 {
proxy_pass b;
}이 경우를 비롯해 if를 사용하는 많은 경우에는 이 지시어를 아예 사용하지 않을 수 있습니다. 다음 예에서는 요청에 X‑Test 헤더가 포함되어 있으면 map{} 블록이 $upstream_name 변수를 b로 설정하고, 요청은 해당 이름의 업스트림 그룹으로 프록시됩니다.
map $http_x_test $upstream_name {
default "b";
"" "a";
}
# ...
location / {
proxy_pass http://$upstream_name;
}실수 7: 과도한 상태 확인
여러 가상 서버가 동일한 업스트림 그룹으로 요청을 프록시하도록 구성하는 경우가 매우 흔합니다(즉, 여러 proxy_pass 지시문을 여러 server{} 블록에 동일하게 포함하는 것입니다). 이 경우 흔히 저지르는 실수는 모든 health_check 지시문을 각 server{} 블록에 포함하는 것입니다. 이렇게 하면 추가 정보는 얻지 못한 채 업스트림 서버의 부하만 증가합니다.
당연한 이야기일 수 있지만, 해결 방법은 upstream{} 블록마다 상태 확인을 하나만 정의하는 것입니다. 여기서는 이름이 b인 업스트림 그룹의 상태 확인을 특수한 이름 지정 로케이션에 정의하고 적절한 시간 제한과 헤더 설정도 함께 구성합니다.
location / {
proxy_set_header Host $host;
proxy_set_header "Connection" "";
proxy_http_version 1.1;
proxy_pass http://b;
}
location @health_check {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host example.com;
proxy_pass http://b;
}복잡한 구성에서는 이 예시와 같이 모든 상태 확인 위치를 NGINX Plus API 및 대시보드와 함께 하나의 가상 서버로 그룹화하면 관리를 더욱 간소화할 수 있습니다.
server {
listen 8080;
location / {
# …
}
location @health_check_b {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host example.com;
proxy_pass http://b;
}
location @health_check_c {
health_check;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
proxy_set_header Host api.example.com;
proxy_pass http://c;
}
location /api {
api write=on;
# directives limiting access to the API (see 'Mistake 8' below)
}
location = /dashboard.html {
root /usr/share/nginx/html;
}
}HTTP, TCP, UDP 및 gRPC 서버의 상태 확인에 대한 자세한 내용은 NGINX Plus Admin Guide를 참조하세요.
실수 8: 보안이 적용되지 않은 메트릭 액세스
NGINX 작동에 관한 기본 메트릭은 Stub Status 모듈에서 확인할 수 있습니다. NGINX Plus에서는 NGINX Plus API를 사용하여 훨씬 더 광범위한 메트릭을 수집할 수도 있습니다. 메트릭 수집을 활성화하려면 stub_status 또는 api 지시문을 각각 server{} 또는 location{} 블록에 포함합니다. 그러면 해당 블록이 메트릭을 확인하기 위해 액세스하는 URL이 됩니다. (NGINX Plus API의 경우 메트릭을 수집하려는 NGINX 엔터티(가상 서버, 업스트림 그룹, 캐시 등)에 대한 공유 메모리 영역도 구성해야 합니다. 자세한 지침은 NGINX Plus 관리 가이드를 참조하세요.)
일부 메트릭에는 웹사이트나 NGINX가 프록시하는 앱을 공격하는 데 악용될 수 있는 민감한 정보가 포함되어 있습니다. 사용자 구성에서 종종 발견되는 실수는 해당 URL에 대한 액세스를 제한하지 않는 것입니다. 여기서는 메트릭을 보호할 수 있는 몇 가지 방법을 살펴봅니다. 첫 번째 예에서는 stub_status를 사용합니다.
다음과 같이 구성하면 인터넷상의 누구나 http://example.com/basic_status에서 메트릭에 액세스할 수 있습니다.
server {
listen 80;
server_name example.com;
location = /basic_status {
stub_status;
}
}HTTP 기본 인증으로 메트릭 보호
메트릭을 HTTP 기본 인증으로 암호로 보호하려면 auth_basic 및 auth_basic_user_file 지시어를 포함하세요. 이 파일(여기서는 .htpasswd)에는 로그인하여 메트릭을 볼 수 있는 클라이언트의 사용자 이름과 비밀번호가 나열됩니다:
server {
listen 80;
server_name example.com;
location = /basic_status {
auth_basic “closed site”;
auth_basic_user_file conf.d/.htpasswd;
stub_status;
}
}Protect Metrics with the allow 및 deny 지시어로 메트릭 보호
승인된 사용자가 로그인하지 않아도 되게 하고 싶고, 이들이 메트릭에 액세스할 때 사용할 IP 주소를 알고 있다면 또 다른 방법으로 allow 지시어를 사용할 수 있습니다. 개별 IPv4 및 IPv6 주소와 CIDR 범위를 지정할 수 있습니다. deny all 지시어는 그 밖의 모든 주소에서의 액세스를 차단합니다.
server {
listen 80;
server_name example.com;
location = /basic_status {
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
allow 96.1.2.23/32;
deny all;
stub_status;
}
}두 가지 방법 결합하기
두 가지 방법을 모두 결합하려면 어떻게 해야 할까요? 특정 주소의 클라이언트는 비밀번호 없이 메트릭에 액세스할 수 있도록 허용하면서, 다른 주소에서 접속하는 클라이언트에게는 로그인을 요구할 수 있습니다. 이를 위해 satisfy any 지시어를 사용합니다. 이 지시어는 HTTP 기본 인증 자격 증명으로 로그인하거나 사전 승인된 IP 주소를 사용하는 클라이언트의 액세스를 NGINX가 허용하도록 합니다. 보안을 강화하려면 satisfy를 all로 설정하여 특정 주소에서 접속하는 사용자에게도 로그인을 요구할 수 있습니다.
server {
listen 80;
server_name monitor.example.com;
location = /basic_status {
satisfy any;
auth_basic “closed site”;
auth_basic_user_file conf.d/.htpasswd;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
allow 96.1.2.23/32;
deny all;
stub_status;
}
}NGINX Plus에서는 동일한 기법을 사용하여 NGINX Plus API 엔드포인트(http://monitor.example.com:8080/api/—다음 예시에서 사용—)와 다음 주소의 실시간 활동 모니터링 대시보드에 대한 액세스를 제한합니다: http://monitor.example.com/dashboard.html.
이 구성에서는 96.1.2.23/32 네트워크 또는 localhost에서 접속하는 클라이언트만 비밀번호 없이 액세스할 수 있습니다. 지시문이 server{} 수준에서 정의되므로 API와 대시보드 모두에 동일한 제한이 적용됩니다. 참고로 write=on 매개변수를 api에 지정하면 이러한 클라이언트가 API를 사용하여 구성을 변경할 수도 있습니다.
API 및 대시보드 구성에 대한 자세한 내용은 NGINX Plus Admin Guide를 참조하세요.
server {
listen 8080;
server_name monitor.example.com;
satisfy any;
auth_basic “closed site”;
auth_basic_user_file conf.d/.htpasswd;
allow 127.0.0.1/32;
allow 96.1.2.23/32;
deny all;
location = /api/ {
api write=on;
}
location = /dashboard.html {
root /usr/share/nginx/html;
}
}실수 9: ip_hash 사용 시 모든 트래픽이 동일한 /24 CIDR 블록에서 유입되는 경우
The ip_hash 알고리즘은 클라이언트 IP 주소의 해시를 기반으로 upstream{} 블록의 서버들에 트래픽을 분산합니다. 해싱 키로는 IPv4 주소의 처음 세 옥텟 또는 전체 IPv6 주소를 사용합니다. 이 방식은 세션 지속성을 유지하므로 서버를 사용할 수 없는 경우를 제외하면 클라이언트의 요청이 항상 동일한 서버로 전달됩니다.
고가용성으로 구성된 가상 사설망에 NGINX를 리버스 프록시로 배포했다고 가정해 보겠습니다. 다양한 소스(내부 네트워크, 파트너 네트워크, 인터넷 등)에서 들어오는 트래픽을 수신하여 업스트림 서버로 리버스 프록시하도록 NGINX에 전달하기 위해 NGINX 앞에 여러 방화벽, 라우터, 계층 4 로드 밸런서 및 게이트웨이를 배치합니다. 초기 NGINX 구성은 다음과 같습니다.
http {
upstream {
ip_hash;
server 10.10.20.105:8080;
server 10.10.20.106:8080;
server 10.10.20.108:8080;
}
server {# …}
}하지만 여기에는 문제가 있습니다. 모든 '인터셉트' 장치가 동일한 10.10.0.0/24 네트워크에 있으므로 NGINX에는 모든 트래픽이 해당 CIDR 범위의 주소에서 오는 것처럼 보입니다. ip_hash 알고리즘은 IPv4 주소의 처음 세 옥텟을 해싱한다는 점을 기억하세요. 이 배포 환경에서는 모든 클라이언트의 처음 세 옥텟이 10.10.0으로 동일하므로 해시도 모두 같아져 트래픽을 서로 다른 서버로 분산할 근거가 없습니다.
해결 방법은 대신 hash 알고리즘을 사용하고 $binary_remote_addr 변수를 해시 키로 지정하는 것입니다. 이 변수는 전체 클라이언트 주소를 가져와 IPv4 주소의 경우 4바이트, IPv6 주소의 경우 16바이트인 이진 표현으로 변환합니다. 이제 각 중간 장치마다 해시가 달라지므로 로드 밸런싱이 예상대로 작동합니다.
또한 기본값 대신 consistent 매개변수를 포함하여 ketama 해싱 방식을 사용합니다. 이렇게 하면 서버 구성이 변경될 때 다른 업스트림 서버로 다시 매핑되는 키의 수가 크게 줄어들어 캐싱 서버의 캐시 적중률이 높아집니다.
http {
upstream {
hash $binary_remote_addr consistent;
server 10.10.20.105:8080;
server 10.10.20.106:8080;
server 10.10.20.108:8080;
}
server {# …}
}실수 10: 업스트림 그룹을 활용하지 않음
가장 단순한 사용 사례 중 하나로, 포트 3000에서 수신 대기하는 단일 NodeJS 기반 백엔드 애플리케이션의 리버스 프록시로 NGINX를 사용한다고 가정해 보겠습니다. 일반적인 구성은 다음과 같습니다:
http {
server {
listen 80;
server_name example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://localhost:3000/;
}
}
}간단하죠? proxy_pass 지시어는 클라이언트의 요청을 어디로 보낼지 NGINX에 알려 줍니다. NGINX는 호스트 이름을 IPv4 또는 IPv6 주소로 확인하기만 하면 됩니다. 연결이 설정되면 NGINX는 해당 서버로 요청을 전달합니다.
여기서 흔히 하는 실수는 서버가 하나뿐이어서 부하 분산을 구성할 이유가 없으므로 upstream{} 블록을 만들어도 소용없다고 생각하는 것입니다. 하지만 실제로는 다음 구성에서 볼 수 있듯이 upstream{} 블록을 사용하면 성능을 개선하는 여러 기능을 활용할 수 있습니다.
http {
upstream node_backend {
zone upstreams 64K;
server 127.0.0.1:3000 max_fails=1 fail_timeout=2s;
keepalive 2;
}
server {
listen 80;
server_name example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://node_backend/;
proxy_next_upstream error timeout http_500;
}
}
}The zone 지시문은 호스트의 모든 NGINX 워커 프로세스가 업스트림 서버의 구성 및 상태 정보에 액세스할 수 있는 공유 메모리 영역을 설정합니다. 여러 업스트림 그룹이 이 영역을 공유할 수 있습니다. NGINX Plus에서는 이 영역을 통해 NGINX를 다시 시작하지 않고도 NGINX Plus API를 사용하여 업스트림 그룹의 서버와 개별 서버의 설정을 변경할 수 있습니다. 자세한 내용은 NGINX Plus 관리 가이드를 참조하세요.
The server 지시문에는 서버 동작을 조정하는 데 사용할 수 있는 여러 매개변수가 있습니다. 이 예에서는 NGINX가 서버를 비정상 상태로 판단하여 요청을 처리할 수 없도록 하는 조건을 변경했습니다. 여기서는 2초마다 통신 시도가 한 번이라도 실패하면 서버를 비정상 상태로 간주합니다(기본값은 10초마다 한 번 실패할 경우입니다).
이 설정을 proxy_next_upstream 지시문과 함께 사용하여 NGINX가 통신 시도 실패로 간주할 조건을 구성합니다. 이 경우 요청은 업스트림 그룹의 다음 서버로 전달됩니다. 기본 오류 및 시간 초과 조건에 http_500을 추가하여 NGINX가 업스트림 서버에서 반환된 HTTP 500 (내부 서버 오류) 코드를 통신 시도 실패로 간주하도록 합니다.
The keepalive 지시어는 각 워커 프로세스의 캐시에 유지되는 업스트림 서버에 대한 유휴 keepalive 연결 수를 설정합니다. 이에 따른 이점은 실수 3: 업스트림 서버에 대한 keepalive 연결을 활성화하지 않음에서 이미 설명했습니다.
NGINX Plus를 사용하면 업스트림 그룹에 추가 기능을 구성할 수 있습니다:
- 앞서 설명했듯이 NGINX Open Source는 시작 시 서버 호스트 이름을 IP 주소로 한 번만 확인합니다. 서버 지시문의
resolve매개변수를 사용하면 NGINX Plus가 업스트림 서버의 도메인 이름에 해당하는 IP 주소의 변경 사항을 모니터링하고, 재시작할 필요 없이 업스트림 구성을 자동으로 수정할 수 있습니다.service매개변수를 사용하면 NGINX Plus에서 포트 번호, 가중치 및 우선순위 정보가 포함된 DNSSRV레코드도 사용할 수 있습니다. 이는 서비스의 포트 번호가 동적으로 할당되는 경우가 많은 마이크로서비스 환경에서 매우 중요합니다. 서버 주소 확인에 대한 자세한 내용은 블로그의 NGINX 및 NGINX Plus를 통한 서비스 검색에 DNS 사용을 참조하세요. - server 지시문의
slow_start매개변수를 사용하면 NGINX Plus가 새롭게 정상 상태로 판단되어 요청을 수락할 수 있게 된 서버로 보내는 요청량을 점진적으로 늘릴 수 있습니다. 이를 통해 서버에 갑자기 요청이 몰려 과부하가 발생하고 다시 장애가 발생하는 것을 방지할 수 있습니다. - The
queue지시어를 사용하면 요청을 처리할 업스트림 서버를 선택할 수 없을 때 클라이언트에 즉시 오류를 반환하는 대신 NGINX Plus가 요청을 대기열에 넣을 수 있습니다.
리소스
- NGINX Plus 및 NGINX 구성 파일 만들기 - NGINX Plus 관리 가이드
- Gixy, GitHub에서 제공되는 NGINX 구성 분석기
- NGINX Amplify, 여기에는 Analyzer 도구가 포함되어 있습니다
NGINX Plus를 사용해 보려면 지금 30일 무료 체험을 시작하거나 사용 사례에 대해 논의하려면 문의해 주세요.
작성자 소개

관련 블로그 게시물
시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.
F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.
Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.
진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.
NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.
Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.