LabHub
배우기 러닝패스 코스

nginx Incident Response

Create 502 and 504 Yourself and Tell Them Apart by Evidence

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

502 와 504 를 직접 만들어 낸 다음, 상태 코드가 아니라 error.log 의 구절로 원인을 확정할 수 있게 됩니다. 특히 같은 504 인데 걸린 타임아웃이 다른 두 경우를 구분하게 됩니다.

왜 중요한가

장애 대응에서 시간을 가장 많이 잡아먹는 것은 틀린 원인을 확신하는 것입니다. 502 를 "백엔드가 죽었다" 로 단정하고 타임아웃을 올리면 아무 일도 일어나지 않습니다. 연결이 거부된 실패에는 기다릴 시간 자체가 없었기 때문입니다. 반대로 504 라고 해서 proxy_read_timeout 을 올리면 되는 것도 아닙니다. 연결 단계에서 걸렸다면 읽기 타임아웃을 30 초로 올려도 요청은 여전히 2 초 만에 끊깁니다. 이 실습의 5 단계가 정확히 그 장면입니다. 그 차이를 만드는 것은 로그 한 줄의 while 뒤 구절 하나뿐입니다.

단계

  1. /root/ngi/upstream.py 를 저장하고 백그라운드로 띄웁니다. 9101 번에 앱이 뜨고 9109 번은 연결을 받지 않는 포트가 됩니다. 9999 번에는 아무것도 띄우지 않습니다. 확인: 9101 번의 / 가 200, /slow?d=2 가 2초 뒤 200, /bighdr?n=8000 응답에 X-Session-Blob 헤더가 있어야 합니다.
  2. /root/ngi/nginx.conf 를 작성하고 nginx 를 기동합니다. 8088 포트로 listen 하고 세 경로를 둡니다 — /app/ 은 9101 번, /dead/ 는 9999 번, /hang/ 은 9109 번으로 프록시합니다. error_log/root/ngi/logs/error.logwarn 수준으로, 액세스 로그는 /root/ngi/logs/access.log 에 남기고 포맷에 $upstream_response_time, $request_time, $upstream_status 를 모두 넣습니다. proxy_connect_timeout 2s;proxy_read_timeout 3s; 를 설정합니다. 확인: http://127.0.0.1:8088/app/ 가 200 이어야 합니다. (브라우저 미리보기로 열어 볼 수 있습니다.)
  3. http://127.0.0.1:8088/dead/ 를 호출하고 /root/ngi/case-refused.txt 를 만듭니다. 세 줄입니다.
    status=<받은 상태 코드>
    phrase=<error.log 에서 원인을 지목하는 구절 그대로>
    timeout=<실제로 발동한 타임아웃 이름, 없으면 none>
    
  4. http://127.0.0.1:8088/app/slow?d=10 을 호출하고 몇 초 만에 끊기는지 재서 /root/ngi/case-read.txt 를 만듭니다. 네 줄입니다.
    status=<상태 코드>
    phrase=<error.log 의 while 로 시작하는 구절 그대로>
    timeout=<발동한 타임아웃 지시자 이름>
    elapsed=<끊기기까지 걸린 초, 정수>
    
  5. /hang/ 블록 안에 proxy_read_timeout 30s; 를 넣고 reload 한 뒤 http://127.0.0.1:8088/hang/ 를 호출합니다. 읽기에 30 초를 줬는데도 요청이 훨씬 빨리 끊깁니다. 시간을 재서 /root/ngi/case-connect.txt 를 4 단계와 같은 형식으로 만듭니다. 상태 코드는 4 단계와 같지만 timeout 은 달라야 합니다.
  6. http://127.0.0.1:8088/app/bighdr?n=8000 을 호출하면 502 가 납니다. 원인 구절을 확인한 뒤 프록시 버퍼를 키워 200 이 나오게 고칩니다. 지시자 하나만 키우면 nginx 가 뜨지 않으니 emerg 메시지를 읽고 함께 고치세요. /root/ngi/case-header.txt 를 만듭니다. 네 줄입니다.
    before=<고치기 전 코드>
    after=<고친 뒤 코드>
    phrase=<error.log 에서 원인을 지목하는 구절 그대로>
    fix=<헤더 크기를 직접 정하는 지시자 이름>
    
    고친 뒤에도 3~5 단계의 실패는 그대로 재현돼야 합니다.
  7. /root/ngi/verdict.csv 를 만듭니다. 첫 줄은 case,status,fix 이고 네 줄이 이어집니다. caserefused, read-timeout, connect-timeout, big-header 이고 fixnone, proxy_connect_timeout, proxy_read_timeout, proxy_buffer_size 중 하나입니다. 어느 것이 어느 것과 짝인지는 3~6 단계에서 본 것으로 정하세요.
  8. /root/ngi/rca.md 를 작성합니다. ## 현상, ## 증거, ## 원인, ## 조치, ## 재발방지 다섯 개의 h2 제목이 있어야 하고, 4 단계와 5 단계의 elapsed 숫자와 두 타임아웃 지시자 이름이 본문에 그대로 인용돼야 합니다.

참고

장애 발생기 띄우기

/root/ngi/upstream.py 를 저장하고 백그라운드로 띄웁니다. 9101 번에 앱이 뜨고 9109 번은 연결을 받지 않는 포트가 됩니다. 9999 번에는 아무것도 띄우지 않습니다. 확인: 9101 번의 / 가 200, /slow?d=2 가 2초 뒤 200, /bighdr?n=8000 응답에 X-Session-Blob 헤더가 있어야 합니다.

업스트림 셋이 필요합니다 — 정상적으로 답하는 앱, 연결을 절대 받지 않는 포트, 그리고 아무도 듣지 않는 포트. 앞의 둘은 파이썬 한 파일로 함께 띄웁니다. 기동 뒤에는 앱이 200 을 주는지, 연결을 받지 않는 포트가 정말로 매달리는지 각각 확인하세요.

증거가 남는 프록시 세우기

/root/ngi/nginx.conf 를 작성하고 nginx 를 기동합니다. 8088 포트로 listen 하고 세 경로를 둡니다 — /app/ 은 9101 번, /dead/ 는 9999 번, /hang/ 은 9109 번으로 프록시합니다. error_log/root/ngi/logs/error.logwarn 수준으로, 액세스 로그는 /root/ngi/logs/access.log 에 남기고 포맷에 $upstream_response_time, $request_time, $upstream_status 를 모두 넣습니다. proxy_connect_timeout 2s;proxy_read_timeout 3s; 를 설정합니다. 확인: http://127.0.0.1:8088/app/ 가 200 이어야 합니다. (브라우저 미리보기로 열어 볼 수 있습니다.)

포트는 8088 입니다. 로그 두 개가 이 실습의 전부이니 경로와 수준을 정확히 잡으세요 — error_log 를 error 수준으로 두면 뒤 실습의 경고가 통째로 사라집니다. 액세스 로그 포맷에는 업스트림이 쓴 시간, 전체 시간, 업스트림 상태 코드 세 가지가 들어가야 합니다.

죽은 업스트림 — 502

http://127.0.0.1:8088/dead/ 를 호출하고 /root/ngi/case-refused.txt 를 만듭니다. 세 줄입니다.

status=<받은 상태 코드>
phrase=<error.log 에서 원인을 지목하는 구절 그대로>
timeout=<실제로 발동한 타임아웃 이름, 없으면 none>

죽은 포트로 프록시하면 어떤 코드가 나오는지, 그리고 그때 error.log 첫 낱말이 무엇인지 보세요. 여기서 타임아웃 이야기가 나올 자리가 있는지 스스로에게 물어보면 timeout 칸에 무엇을 적을지 정해집니다.

느린 업스트림 — 504, 읽기 타임아웃

http://127.0.0.1:8088/app/slow?d=10 을 호출하고 몇 초 만에 끊기는지 재서 /root/ngi/case-read.txt 를 만듭니다. 네 줄입니다.

status=<상태 코드>
phrase=<error.log 의 while 로 시작하는 구절 그대로>
timeout=<발동한 타임아웃 지시자 이름>
elapsed=<끊기기까지 걸린 초, 정수>

응답까지 10 초 걸리는 경로를 호출하되, 몇 초 만에 끊기는지 시간을 재세요. 그 초 값은 설정 파일 어딘가에 그대로 적혀 있습니다. error.log 의 구절에서 while 뒤를 읽으면 어느 타임아웃인지 확정됩니다.

올린 타임아웃은 발동한 타임아웃이 아니었다

/hang/ 블록 안에 proxy_read_timeout 30s; 를 넣고 reload 한 뒤 http://127.0.0.1:8088/hang/ 를 호출합니다. 읽기에 30 초를 줬는데도 요청이 훨씬 빨리 끊깁니다. 시간을 재서 /root/ngi/case-connect.txt 를 4 단계와 같은 형식으로 만듭니다. 상태 코드는 4 단계와 같지만 timeout 은 달라야 합니다.

먼저 /hang/ 블록 안에 proxy_read_timeout 30s 를 넣고 reload 하세요. 읽기에 30 초를 주겠다고 선언한 겁니다. 그런데도 요청이 몇 초 만에 끊기는지 재 보고, error.log 의 while 뒤 구절이 4 단계와 어떻게 다른지 보세요. 상태 코드와 오류 번호는 같습니다.

서버는 멀쩡한데 특정 사용자만 502

http://127.0.0.1:8088/app/bighdr?n=8000 을 호출하면 502 가 납니다. 원인 구절을 확인한 뒤 프록시 버퍼를 키워 200 이 나오게 고칩니다. 지시자 하나만 키우면 nginx 가 뜨지 않으니 emerg 메시지를 읽고 함께 고치세요. /root/ngi/case-header.txt 를 만듭니다. 네 줄입니다.

before=<고치기 전 코드>
after=<고친 뒤 코드>
phrase=<error.log 에서 원인을 지목하는 구절 그대로>
fix=<헤더 크기를 직접 정하는 지시자 이름>

고친 뒤에도 3~5 단계의 실패는 그대로 재현돼야 합니다.

응답 헤더를 8000 바이트로 부풀리는 경로가 있습니다. 헤더가 커졌을 뿐인데 코드가 어떻게 나오는지 보세요. 고칠 때 버퍼 지시자를 하나만 키우면 nginx 가 아예 뜨지 않습니다 — 그 emerg 메시지가 무엇을 함께 키우라고 하는지 읽으세요.

네 줄짜리 판정표

/root/ngi/verdict.csv 를 만듭니다. 첫 줄은 case,status,fix 이고 네 줄이 이어집니다. caserefused, read-timeout, connect-timeout, big-header 이고 fixnone, proxy_connect_timeout, proxy_read_timeout, proxy_buffer_size 중 하나입니다. 어느 것이 어느 것과 짝인지는 3~6 단계에서 본 것으로 정하세요.

네 가지 실패를 한 표로 모읍니다. fix 칸에는 nginx 설정에서 이 실패를 실제로 푸는 지시자 이름을 적되, nginx 설정으로는 풀 수 없는 것이 하나 있습니다 — 그 줄에는 none 을 적습니다.

숫자가 들어간 장애 보고서

/root/ngi/rca.md 를 작성합니다. ## 현상, ## 증거, ## 원인, ## 조치, ## 재발방지 다섯 개의 h2 제목이 있어야 하고, 4 단계와 5 단계의 elapsed 숫자와 두 타임아웃 지시자 이름이 본문에 그대로 인용돼야 합니다.

보고서의 가치는 숫자에 있습니다. 앞 단계에서 재 둔 초 값을 그대로 인용하세요. 재발방지에는 '주의한다' 대신 어떤 지시자를 어떤 값으로 두고 어떤 로그 문구에 알림을 걸 것인지를 적습니다.