nginx 장애 대응 · 앱 잘못이 아닌 실패 · 실습
앱 잘못이 아닌 실패 — 버퍼·본문 크기·커넥션 재사용
목표
상태 코드가 200 인데 사용자는 느리다고 하는 장애를 nginx 의 버퍼·본문 크기·
커넥션 설정에서 찾아낼 수 있게 됩니다. 그리고 각 설정이 무엇을 대가로
치르는지 숫자로 말할 수 있게 됩니다.
왜 중요한가
5xx 는 대시보드가 알려 줍니다. 정말 오래 끄는 것은 **아무도 잘못한 사람이
없어 보이는 장애**입니다. 개발팀은 "우리는 0.05 초에 응답했다" 고 하고
인프라팀은 "서버는 한가하다" 고 하는데 사용자는 7 초를 기다립니다.
셋 다 사실이고, 빠진 조각은 nginx 안에 있습니다.
응답이 디스크로 샜거나, 요청 본문이 임시 파일로 떨어졌거나, 매 요청마다
새 TCP 커넥션이 열리고 있습니다. 이 셋은 전부 상태 코드에 나타나지 않고,[warn] 수준의 로그 한 줄에만 흔적을 남깁니다.
그래서 error_log 를 warn 으로 열어 두는 것이 이 장애를 볼 수 있는
유일한 방법입니다.
단계
1. /root/ngb/upstream.py 를 저장해 띄우고 /root/ngb/nginx.conf 를 작성해
nginx 를 기동합니다. 8088 포트로 listen 하고 /app/ 을 upstream app
그룹(9101 번)으로 프록시합니다. error_log 는/root/ngb/logs/error.log 에 warn 수준, 액세스 로그는/root/ngb/logs/access.log 에 남기고 포맷에 $upstream_response_time,$request_time, $upstream_status 를 모두 넣습니다.client_max_body_size 는 아직 넣지 않습니다.
확인: http://127.0.0.1:8088/app/ 가 200 이어야 합니다.
(브라우저 미리보기로 열어 볼 수 있습니다.)
2. 두 가지 느린 요청을 각각 한 번씩 보냅니다.
- 업스트림이 느린 경우:
http://127.0.0.1:8088/app/slow?d=4 - 전송이 느린 경우:
http://127.0.0.1:8088/app/big?n=8000000을
curl --limit-rate 1000k 로
두 요청의 액세스 로그 줄을 /root/ngb/attribute.log 로 떠 두고,/root/ngb/attribute.csv 를 만듭니다. 첫 줄은case,upstream_time,request_time,verdict 이고 두 줄이 이어집니다.case 는 slow-upstream 과 slow-transfer, verdict 는upstream 또는 transfer 입니다.
3. 2 단계의 큰 응답이 error.log 에 남긴 경고를 찾아/root/ngb/case-spill.txt 를 만듭니다. 네 줄입니다.
level=<대괄호 안 수준> path=<로그에 적힌 임시 파일 경로 그대로> status=<같은 요청의 액세스 로그 상태 코드> tradeoff=<disk-spill 또는 upstream-held>4. /nobuf/ 경로를 추가합니다. 같은 upstream app 으로 프록시하되
그 블록 안에 proxy_buffering off; 를 넣습니다. reload 한 뒤http://127.0.0.1:8088/nobuf/big?n=8000000 을 같은 --limit-rate 1000k 로
받고 /root/ngb/case-nobuf.txt 를 만듭니다. 네 줄입니다.
upstream_time=<us 칸이 아니라 ut 칸 값 그대로> request_time=<rt 칸 값 그대로> warn=<none 또는 spilled> tradeoff=<disk-spill 또는 upstream-held>5. 3MB 본문을 http://127.0.0.1:8088/app/toobig 로 POST 합니다.
상태 코드, 액세스 로그의 us 칸, 그리고 /root/ngb/upstream.log 에
그 요청이 몇 건 남았는지를 확인하고 /root/ngb/case-413.txt 를 만듭니다.
네 줄입니다.
status=<상태 코드> upstream_status=<액세스 로그 us 칸 값> app_saw=<업스트림 로그에 남은 건수> fix=<이 실패를 푸는 지시자 이름>그런 다음 그 지시자를 10m 으로 설정하고 reload 해서http://127.0.0.1:8088/app/upload 로 같은 3MB 를 POST 하면
200 이 나오게 만듭니다.
6. 500KB 본문을 http://127.0.0.1:8088/app/spill 로 POST 하면error.log 에 임시 파일 경고가 하나 더 나옵니다./root/ngb/case-bodyspill.txt 를 만듭니다. 세 줄입니다.
level=<대괄호 안 수준> path=<로그에 적힌 임시 파일 경로 그대로> fix=<본문을 메모리에 담는 크기를 정하는 지시자 이름>그런 다음 그 지시자를 1m 으로 설정하고 reload 한 뒤http://127.0.0.1:8088/app/nospill 로 같은 500KB 를 POST 해서
이번에는 경고가 나오지 않는 것을 확인합니다.
7. 커넥션 재사용을 실측합니다. 먼저 지금 상태에서http://127.0.0.1:8088/app/ 를 10 회 호출하고 upstream.log 의conn 번호가 몇 종류인지 셉니다. 그다음 upstream app 블록에keepalive 16; 을, /app/ 블록에 proxy_http_version 1.1; 과proxy_set_header Connection ""; 를 넣고 reload 한 뒤 다시 10 회를
호출해 같은 방식으로 셉니다. /root/ngb/keepalive.txt 를 만듭니다.
before=<설정 전 커넥션 수> after=<설정 뒤 커넥션 수>8. /root/ngb/runbook.md 를 작성합니다. ## 증상별 첫 확인, ## 버퍼링,## 본문 크기, ## 커넥션 재사용 네 개의 h2 제목이 있어야 하고,proxy_buffering, client_max_body_size, client_body_buffer_size,keepalive, upstream_response_time 다섯 낱말이 모두 나와야 하며,
2 단계에서 잰 두 시간 값이 숫자로 인용돼야 합니다.
참고
- 기동:
nginx -c /root/ngb/nginx.conf -p /root/ngb - 재적용:
nginx -s reload -c /root/ngb/nginx.conf -p /root/ngb - 느린 클라이언트:
curl -s -o /dev/null --limit-rate 1000k <URL> - 큰 본문 보내기:
head -c 3000000 /dev/zero | curl -s --data-binary @- <URL> - 커넥션 수 세기:
awk '$3=="start"{print $2}' /root/ngb/upstream.log | sort -u | wc -l - 흔한 실수 1: 1 단계에서
client_max_body_size를 미리 넣는 것. - 흔한 실수 2: 커넥션 재사용 3 종 중
proxy_set_header Connection "";를 - 흔한 실수 3: 응답 쪽 임시 파일 경로와 요청 본문 쪽 임시 파일 경로를
(앞 요청이 섞이니 셀 구간을 tail 로 잘라 쓰세요)
5 단계에서 볼 기본값 동작이 사라집니다.
빠뜨리는 것. HTTP/1.1 로 올려도 매 요청마다 커넥션이 끊깁니다.
섞어 적는 것. 서로 다른 디렉터리입니다.
단계 8개
- 측정할 수 있는 프록시 세우기
- 앱이 느린가 전송이 느린가
- 디스크로 새는 응답
- 버퍼링을 끄면 무엇이 바뀌는가
- 애플리케이션이 영영 못 보는 413
- 요청 본문도 디스크로 샌다
- 세 줄이 다 있어야 커넥션이 재사용된다
- 운영 판단 기준서