운영 — 무중단 배포와 장애 진단
마지막 편은 평소에 해 두는 일과 터졌을 때 하는 일이다. 앞의 다섯 편에서 본 값들이 여기서 어떻게 사고로 나타나는지를 따라간다.
1. 무중단 배포 — 요청을 흘리지 않는 순서
-
빠뜨리면 안 되는 두 지점
-
① 신버전은 '준비될 때까지' 트래픽을 받으면 안 된다
- 기동 직후에는 커넥션 풀·캐시가 비어 있고 JIT 도 안 데워져 있다
- 준비 상태(readiness)와 살아있음(liveness)을 구분한다
- 준비 전에 트래픽이 들어오면 배포 직후에만 5xx·지연이 튄다
-
② 구버전은 '제외된 뒤에' 수용을 멈춰야 한다
- 헬스체크 실패 → LB 가 알아채기까지 시차가 있다
- 그 시차 동안 새 요청이 계속 오는데 이미 닫았다면 전부 실패한다
- 대기 시간 ≥ 헬스체크 주기 × 실패 임계 + 여유
헬스체크를 얕게 만들지, 깊게 만들지
얕은 헬스체크 프로세스가 살아 있는지만 확인 (200 을 그냥 반환)
장점 의존 자원 장애가 전체 인스턴스를 죽이지 않는다
단점 DB 가 죽어도 '정상' 으로 보인다
깊은 헬스체크 DB·캐시 연결까지 확인
장점 진짜 서비스 가능 여부를 반영한다
단점 DB 가 잠깐 흔들리면 '모든' 인스턴스가 동시에 제외된다
→ 트래픽 받을 곳이 사라져 장애가 증폭된다
실무 기본형
liveness 얕게 (재시작 판단 — 자기 자신 문제만)
readiness 중간 (트래픽 수용 판단 — 필수 의존만, 선택 의존은 제외)
깊은 점검은 별도 엔드포인트로 두고 모니터링만 시킨다
깊은 헬스체크로 전멸시키는 사고가 실제로 흔하다. DB 순단에 모든 파드가 동시에 NotReady 가 되고, 복구된 뒤에도 한꺼번에 붙어 다시 DB 를 무너뜨린다.
앞단 리로드로 하는 배포
# 업스트림 목록만 바꿔서 무중단으로 전환
nginx -t && nginx -s reload
- 2편에서 본 대로 검증 실패면 아무것도 안 바뀐다
- 배포 스크립트는 반드시 -t 를 앞에 둔다
- 성공 후에도 access log 의 상태 코드 분포를 잠시 지켜본다
2. 로그 — 무엇을 남길 것인가
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time urt=$upstream_response_time '
'uct=$upstream_connect_time uht=$upstream_header_time '
'ua=$upstream_addr us=$upstream_status '
'rid=$request_id';
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
-
$upstream_addr — 어느 백엔드로 갔는지 — 특정 인스턴스만 느린 것을 잡는다
-
$upstream_status — 업스트림이 준 코드 — nginx 가 만든 502 와 구분된다
-
$request_id — 요청마다 고유값. 앞단↔애플리케이션 로그를 잇는다
- proxy_set_header X-Request-ID $request_id; 로 함께 넘긴다
-
buffer/flush — 매 요청 디스크 쓰기를 줄인다. 대신 최근 몇 초가 유실될 수 있다
로그 회전
2편에서 본 그대로 — mv 만 하면 워커는 옛 디스크립터에 계속 쓴다
/etc/logrotate.d/nginx postrotate
- [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid) endscript
컨테이너라면 파일 대신 표준출력으로 보내고 수집기에 맡기는 편이 단순하다 access_log /dev/stdout main;
- error_log — /dev/stderr warn;
3. 상태 코드로 원인을 좁힌다
앞단이 내는 코드는 어디가 문제인지를 꽤 정확히 가리킨다.
| 코드 | 누가 냈나 | 흔한 원인 |
|---|---|---|
| 502 Bad Gateway | nginx | 업스트림이 죽었거나, 연결은 됐는데 응답이 깨졌다. 포트 오지정 |
| 504 Gateway Timeout | nginx | 업스트림이 proxy_read_timeout 안에 응답을 못 했다 |
| 499 | nginx 전용 | 클라이언트가 먼저 끊었다 — 서버가 느려서 기다리다 포기한 것 |
| 413 | nginx | client_max_body_size 초과 |
| 404 | 대개 업스트림 | proxy_pass 슬래시로 경로가 어긋난 경우가 많다 (3편) |
| 400 | nginx | 헤더가 너무 크거나(large_client_header_buffers) 요청이 깨졌다 |
502 와 504 의 차이가 진단의 출발점이다
- 502 — 연결은 시도했는데 실패했다 → 프로세스·포트·업스트림 헬스 확인
- 504 — 연결은 됐는데 늦다 → 애플리케이션·DB·타임아웃 값 확인
499 가 늘어난다 = 우리가 느려졌다는 뜻이다
- 5xx 만 보고 있으면 이 신호를 놓친다
4. 자주 나는 사고 다섯
① 업로드가 413 으로 실패한다
계층마다 한도가 있다
- CDN → LB → nginx(client_max_body_size 1m 기본) → WAS
하나만 낮아도 거기서 막힌다 확인 순서: 어느 계층이 413 을 냈는지 응답 헤더의 Server 값으로 가른다
② https 인데 http 로 리다이렉트된다
- 원인 — X-Forwarded-Proto 를 안 넘겼다 (1편)
- 애플리케이션이 자기가 http 로 서비스되는 줄 알고 절대 URL 을 만든다
- 증상 — 무한 리다이렉트 · 혼합 콘텐츠 차단
- 확인 — curl -I 로 Location 헤더를 본다
③ 배포했는데 옛 화면이 보인다
- 원인 — HTML 이 캐시됐다 (3편)
- 해시 붙은 자산은 immutable, HTML 은 no-cache 여야 하는데 뒤집혔다
- 확인 — 응답의 Cache-Control · Age 헤더
- CDN 이면 엣지 캐시 무효화가 필요하다
④ 특정 시간대에만 502 가 튄다
후보
-
업스트림 keepalive 미설정 → 매 요청 새 연결 → 포트 고갈 (3편)
-
배포 시각과 겹친다 → graceful shutdown 순서 문제 (5편)
-
파일 디스크립터 한도 → error log 의 "Too many open files" (2편)
-
확인 — ss -s 로 TIME_WAIT 수, /proc/<pid>/limits 로 한도
⑤ 디스크가 찼는데 로그 파일은 작다
- 원인 — 로그 회전 후 USR1 을 안 보냈다 (2편)
- 삭제된 파일을 프로세스가 계속 들고 있으면 공간이 안 돌아온다
- 확인 — lsof | grep deleted
- 해결 — nginx -s reopen (또는 프로세스 재시작)
5. 진단 명령 모음
# 설정
nginx -t # 문법
nginx -T # include 를 펼친 최종 설정
nginx -V # 빌드 옵션 (HTTP/3·모듈 포함 여부)
# 연결 상태
ss -s # 소켓 요약 (TIME_WAIT 등)
ss -lnt # LISTEN 소켓 · Recv-Q(대기) · Send-Q(백로그)
ss -tn state established | wc -l
# 한도
ulimit -n
cat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep 'open files'
# 실시간
tail -f /var/log/nginx/error.log
awk '{print $9}' access.log | sort | uniq -c | sort -rn # 상태 코드 분포
# 응답 확인
curl -sv -o /dev/null https://example.com 2>&1 | grep -E '^< (HTTP|Location|Server)'
curl -w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
-o /dev/null -s https://example.com
마지막
curl -w한 줄이 어느 구간이 느린지를 바로 가른다. DNS·TCP·TLS·서버 처리(TTFB)·전송이 각각 분리돼 나온다.
6. 평소에 해 둘 것
□ nginx -t 를 배포 파이프라인에 넣는다 □ upstream_*_time 을 access log 에 남긴다 □ $request_id 를 애플리케이션까지 전달한다 □ 인증서 만료 D-30 알림 □ 파일 디스크립터 한도를 워커 설정과 함께 올린다 □ 로그 회전에 USR1 을 넣는다 (또는 stdout 으로 보낸다) □ 헬스체크를 liveness/readiness 로 나눈다 □ graceful shutdown 대기 시간 ≥ 헬스체크 주기 × 실패 임계 + 여유 □ 타임아웃을 계층별로 적어 두고 '안쪽이 더 짧은지' 확인한다 □ 컨테이너 정지 시그널을 확인한다 (nginx 는 SIGQUIT 가 우아한 종료)
한눈에 정리
- 무중단 배포 — 준비되기 전에 받지 않기 · 제외된 뒤에 닫기. 둘 다 빠지기 쉽다
- 헬스체크 — liveness 얕게 · readiness 중간. 깊게 만들면 동시 전멸한다
- 로그 — upstream_addr · upstream_status · request_id 를 남긴다
- 상태 코드 — 502 연결 실패 · 504 응답 지연 · 499 클라이언트가 포기
- 499 증가는 '우리가 느려졌다' 는 신호다
- 사고 다섯 — 413 계층 한도 · XFP 누락 · HTML 캐시 · keepalive 미설정 · 로그 회전
- 진단 — nginx -T · ss -lnt · lsof | grep deleted · curl -w 구간 분해
출처 — nginx docs:
ngx_http_log_module·ngx_http_core_module· "Controlling nginx" · Apache Tomcat 11.0.24 Config Reference · Google SRE — Site Reliability Engineering (헬스체크·배포·모니터링 장)