디버그 서버 NGINX를 사용하여 5xx 에러 캡처
NGINX를 애플리케이션의 리버스 프록시 또는 로드 밸런서로 사용하는 경우 프로덕션 환경에서 디버깅하는 데 도움이 되는 여러 기능이 있습니다. 이 포스트에서는 디버그 서버 를 구축하여 일반적인 리버스 프록시 애플리케이션 인프라를 탐색하면서 error_page 지시문의 특정 사용에 대해 설명합니다.
소프트웨어를 엄격하게 테스트하거나 오랜 시간 동안 테스트하더라도, 제품 환경에서만큼 버그를 드러내기에 좋은 것은 없습니다. 실제 트래픽의 예측할 수 없는 동시성 패턴으로 인한 이상한 경합 조건이나, 사용자가 입력할 것이라고 상상도 못 한 데이터에 대한 입력 유효성 검사 실패 같은 경우, “500 에러를 발생시키는 것”은 큰 문제입니다.
HTTP 5xx 에러 메시지는 사용자의 눈에 매우 잘 띄고, 비즈니스에게는 매우 부끄러울 뿐만 아니라 몇 번의 에러로 인해 평판 훼손이 짧은 시간 내에 발생할 수 있습니다. 게다가 제품 환경에서 이러한 에러를 디버깅하는 것은 극히 어려울 수 있습니다. 일단, 로그 데이터의 양이 방대하여 문제가 있는 세션을 분리하는 일은 마치 바늘을 건초더미에서 찾는 것과 같은 작업이 될 수 있습니다. 그리고 모든 구성 요소의 로그를 모으더라도 문제를 이해하기에 충분한 데이터가 없을 수 있습니다.
만약 5xx 에러가 발생했지만 아무도 그것을 보지 않는다면, 그것은 여전히 에러로 간주될까요?

Tony Moorey(CC) 님의 사진
목차
1. 디버그 서버 소개
2. 구성
3. 요청 재시도 시 멱등성 고려
4. 결론
1. 디버그 서버 소개
특이한 점은 우리가 애플리케이션 서버(디버그 서버)를 설정하고 일반 애플리케이션 서버에서 에러를 일으킨 요청만 제공한다는 것입니다. NGINX는 상위 애플리케이션 서버에서 돌아오는 5xx 에러를 감지하고, 해당 에러를 다른 upstream 그룹(우리의 경우 디버그 서버를 포함한 그룹)으로 재시도할 수 있는 기능을 활용합니다. 따라서 디버그 서버는 이미 에러를 발생시킨 요청만을 받게 되며, 따라서 로그 파일에는 에러 이벤트만이 포함되어 있어 조사에 용이합니다. 이렇게 하면 건초더미에서 바늘 몇 개로 바늘 검색 범위가 줄어듭니다.
기본 애플리케이션 서버와 달리 디버그 서버는 성능을 위해 구축할 필요가 없습니다. 따라서 다음과 같이 원하는 대로 사용 가능한 모든 로깅 및 진단 도구를 활성화할 수 있습니다.
- 전체 스택 추적이 활성화된 디버그 모드에서 애플리케이션 실행
- 애플리케이션 서버의 디버그 로깅
- 프로세스 간 시간 초과를 식별할 수 있도록 애플리케이션 프로파일링
- 서버 리소스 사용량 로깅
이러한 디버깅 도구는 일반적으로 개발 환경에 보관되는 것이 일반적이며. 생산 환경은 성능에 최적화되어 있습니다. 그러나 디버그 서버는 에러가 발생한 요청만을 받기 때문에 가능한 한 많은 구성 요소에서 디버그 모드를 안전하게 활성화할 수 있습니다.

이상적으로는 디버그 서버의 프로비저닝과 구성이 애플리케이션 서버와 동일해야 하지만, 디버그 서버를 가상 머신으로 구축하여 오프라인 분석을 위해 복제하고 복사할 수 있습니다. 그러나 이는 심각한 문제로 인해 5xx 에러가 갑자기 증가할 경우 서버가 과부하될 위험이 있습니다. NGINX Plus를 사용하면 다음과 같은 샘플 구성에서 server 지시문에 max_conns 매개변수를 포함하여 디버그 서버를 이러한 과부하로부터 보호할 수 있습니다.
또한, 주 애플리케이션 서버보다 디버그 서버의 부하가 적기 떄문에 애플리케이션 서버에서 5xx 에러가 발생하는 모든 상황이 디버그 서버에서도 동일한 에러를 발생시키지는 않습니다. 이러한 상황은 주 애플리케이션 서버의 확장 한계에 도달하고 리소스 고갈이 문제의 근본 원인이 되는 경우일 수 있습니다. 원인이 무엇이든 간에 이러한 상황은 사용자가 5xx 에러를 겪지 않도록 하여 사용자 경험을 향상시킵니다.
2. 디버그 서버 구성
다음 샘플 NGINX 구성은 기본 애플리케이션 서버에서 이미 5xx 에러를 생성한 요청을 수신하도록 디버그 서버를 구성하는 방법을 보여줍니다.
upstream app_server {
server 172.16.0.1;
server 172.16.0.2;
server 172.16.0.3;
}
upstream debug_server {
server 172.16.0.9 max_conns=20;
}
server {
listen *:80;
location / {
proxy_pass http://app_server;
proxy_intercept_errors on;
error_page 500 503 504 @debug;
}
location @debug {
proxy_pass http://debug_server;
access_log /var/log/nginx/access_debug_server.log detailed;
error_log /var/log/nginx/error_debug_server.log;
}
}
가장 먼저 할 일은 upstream app_server 블록에 애플리케이션 서버의 주소를 지정하는 것입니다. 그런 다음 upstream debug_server 블록에서 디버그 서버의 단일 주소를 지정합니다.
첫 번째 location 블록은 단순한 리버스 프록시를 구성합니다. proxy_pass 지시문을 사용하여 app_server upstream 그룹의 애플리케이션 서버들에게 요청을 로드 밸런싱 합니다(로드 밸런싱 알고리즘을 지정하지 않았으므로 기본적으로 라운드 로빈(Round Robin) 알고리즘이 사용됩니다). proxy_intercept_errors 지시문은 HTTP 코드 300 이상의 모든 응답을 error_page 지시문으로 처리하도록 합니다. 우리의 구성에서는 500, 503, 504 에러만 가로채고 @debug location 으로 전달합니다. 404와 같은 다른 응답 코드는 수정 없이 클라이언트에게 반환됩니다.
@debug 블록은 두 가지 작업을 수행합니다. 첫 번째는 모든 요청을 debug_server upstream 그룹에 프록시 합니다. 이 그룹에는 특별한 디버그 서버가 포함되어 있습니다. 두 번째는 별도의 access 및 error log 파일에 중복된 로그 항목을 작성합니다. 애플리케이션 서버에서 발생한 에러 요청에 대한 메시지를 일반 액세스 메시지와 분리함으로써, 디버그 서버 자체에서 생성된 에러와 상관관계를 보다 쉽게 파악할 수 있습니다.
access_log 지시문은 상세라는 특수 로그 형식을 참조합니다. server 블록 위의 최상위 http 컨텍스트에 다음 log_format 지시문을 포함하여 형식을 정의합니다.
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_length $request_time '
'$upstream_response_length $upstream_response_time '
'$upstream_status';
자세한 형식은 디버그 서버로 전달된 요청 및 해당 응답에 대한 추가 정보를 제공하는 추가 5개의 변수로 기본 결합 형식을 확장합니다.
- $request_length – 헤더와 본문을 포함한 총 요청 크기(바이트(Byte))
- $request_time – 요청 처리 시간 (밀리초(ms))
- $upstream_response_length – 디버그 서버에서 얻은 응답 길이 (바이트(Byte))
- $upstream_response_time – 디버그 서버에서 응답을 받는 데 소요된 시간(밀리초(ms))
- $upstream_status – 디버그 서버 응답의 상태 코드
로그에 추가된 이러한 추가 필드들은 잘못된 형식의 요청과 긴 실행 시간을 가진 요청을 감지하는 데 매우 유용합니다. 후자는 애플리케이션 내의 타임아웃이나 다른 프로세스 간 통신 문제를 가리킬 수 있습니다.
3. 요청 재시도시 멱등성 고려
디버그 서버로 실패한 요청을 보내고 싶지 않을 때도 있습니다. 예를 들어, 여러 개의 데이터베이스 레코드에 변경을 가하는 요청 중에 애플리케이션이나 API가 중간에 실패한 경우, 해당 요청을 다시 시도하면 이미 성공적으로 완료된 데이터베이스 작업을 반복하게 될 수 있습니다. 이렇게 되면 데이터베이스가 실패한 요청을 다시 시도하지 않은 경우보다 더 큰 혼란 상태로 남을 수 있습니다.
요청이 멱등(idempotent)한 경우에만 해당 요청을 다시 시도하는 것은 안전합니다. 멱등이란 해당 요청을 몇 번을 해도 항상 동일한 결과를 반환하는 특성을 의미합니다. HTTP, GET, PUT, DELETE 요청은 멱등으로 정의되어 있지만 반면 POST는 그렇지 않습니다. 그러나 어떤 HTTP 메서드가 애플리케이션에서 멱등한지는 공식적인 정의와 다를 수 있습니다.
멱등성 문제는 디버그 서버에 대한 세 가지 옵션을 제공합니다.
- 읽기 전용 데이터베이스 연결로 디버그 서버를 실행합니다. 변경이 불가능하기 때문에 요청을 반복해도 안전합니다. 여전히 5xx 에러를 발생시킨 요청을 로그에 기록하지만, 디버그 서버에서는 원인을 조사하기 위한 진단 정보가 적을 수 있습니다.
- 디버그 서버로는 멱등한 요청만을 전송하십시오. 이렇게 하면 5xx 에러를 발생시킨 요청을 분리할 수 있지만, POST 메서드를 사용한 경우에는 분리되지 않습니다.
- 두 번째 디버그 서버를 배포하여 읽기 전용 데이터베이스 연결을 사용하고, 비멱등(멱등하지 않은)요청을 해당 서버로 전송하고 계속하여 멱등한 요청을 기본 디버그 서버로 보내십시오. 이렇게 하면 모든 실패한 요청을 캡처할 수 있지만, 추가 서버와 구성이 필요합니다.
완전성을 기하기 위해 옵션 3의 구성을 살펴보고 이전 구성의 변경 사항을 강조 표시해 보겠습니다.
upstream app_server {
server 172.16.0.1;
server 172.16.0.2;
server 172.16.0.3;
}
upstream debug_server {
server 172.16.0.9 max_conns=20;
}
upstream readonly_server {
server 172.16.0.10 max_conns=20;
}
map $request_method $debug_location {
'POST' @readonly;
'LOCK' @readonly;
'PATCH' @readonly;
default @debug;
}
server {
listen *:80;
location / {
proxy_pass http://app_server;
proxy_intercept_errors on;
error_page 500 503 504 $debug_location;
}
location @debug {
proxy_pass http://debug_server;
access_log /var/log/nginx/access_debug_server.log detailed;
error_log /var/log/nginx/error_debug_server.log;
}
location @readonly {
proxy_pass http://readonly_server;
access_log /var/log/nginx/access_readonly_server.log detailed;
error_log /var/log/nginx/error_readonly_server.log;
}
}
새로운 읽기 전용 디버그 서버를 사용하면 map 지시문이 $request_method 변수를 사용하여 요청 메서드의 멱등성에 따라 $debug_location 이라는 새 변수를 설정합니다. error_page 지시문을 만나면 $debug_location 변수를 사용하여 요청에 사용된 HTTP 메서드에 적합한 디버그 서버로 요청 처리를 지시합니다.
일반적으로는 proxy_next_upstream 지시문을 사용하여 NGINX가 upstream 그룹의 남은 서버에서 실패한 요청을 다시 시도하도록 설정합니다(디버그 서버를 시도하기 전). 이 지시문은 주로 네트워크 수준의 에러에서 사용되지만, 5xx 에러에 대해서도 확장해서 사용할 수 있습니다. NGINX Open Source 1.9.13 이상에서는 기본적으로 5xx 에러로 실패한 비멱등 요청은 다시 시도되지 않습니다. 비멱등한 요청을 다시 시도하는 것이 허용되는 경우, proxy_next_upstream 지시문에 non_idempotent 매개변수를 추가하십시오. 이 동작과 새로운 매개변수는 NGINX Plus R9 이상에서도 활성화됩니다.
location / {
proxy_pass http://app_server;
proxy_next_upstream http_500 http_503 http_504 non_idempotent;
proxy_intercept_errors on;
error_page 500 503 504 @debug;
}
4. 결론
500 에러가 발생하는 것은 큰 문제입니다. DevOps 모델을 운영하거나 지속적인 배포(delivery) 실험하거나, 단순히 대규모 업그레이드의 위험을 줄이고자 하는 경우에도 NGINX는 실제 문제에 잘 대응하는 데 도움이 되는 도구를 제공합니다.
자신의 NGINX 환경에서 디버그 서버를 사용해 보려면 지금 무료 30일 평가판을 시작하거나 NGINX SOTRE에 문의하십시오.
아래 뉴스레터를 구독하고 NGINX의 최신 정보들을 빠르게 전달 받아보세요.
댓글을 달려면 로그인해야 합니다.