웹 서버·WAS 학습 노트 목차

운영 — 무중단 배포와 장애 진단

마지막 편은 평소에 해 두는 일터졌을 때 하는 일이다. 앞의 다섯 편에서 본 값들이 여기서 어떻게 사고로 나타나는지를 따라간다.


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 Gatewaynginx업스트림이 죽었거나, 연결은 됐는데 응답이 깨졌다. 포트 오지정
504 Gateway Timeoutnginx업스트림이 proxy_read_timeout 안에 응답을 못 했다
499nginx 전용클라이언트가 먼저 끊었다 — 서버가 느려서 기다리다 포기한 것
413nginxclient_max_body_size 초과
404대개 업스트림proxy_pass 슬래시로 경로가 어긋난 경우가 많다 (3편)
400nginx헤더가 너무 크거나(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 (헬스체크·배포·모니터링 장)

WAS 커넥터 튜닝 — 스레드는 몇 개가 맞나