Infrastructure Kubernetes
Bell  

Ingress Controller 전환 시 WebSocket 장애 사례 — 60초마다 끊기는 연결의 원인 추적

📌 📌 핵심 요약

Kubernetes 환경에서 NGINX Inc Ingress Controller에서 커뮤니티 ingress-nginx로 전환 후, 방화벽 장애 복구 이후 Mattermost WebSocket 연결이 정확히 60초마다 재연결을 반복한 인시던트. 근본 원인은 어노테이션 호환성 문제로, 기존 nginx.org/websocket-services 어노테이션이 ingress-nginx에서 무시되면서 proxy timeout이 기본값(60초)으로 동작해 WebSocket idle 연결이 끊긴 것으로 확인됨.


1. 환경 및 배경

인프라 구성

  • 플랫폼: Kubernetes (자체 운영 클러스터)
  • Ingress Controller: NGINX Inc(F5) Ingress Controller → 커뮤니티 ingress-nginx로 전환 (최근 완료)
  • 서비스: Mattermost (WebSocket 기반 실시간 메시징)
  • 클라이언트: 16개의 WebSocket 클라이언트 (봇)

전환 배경

최근 상용 F5 NGINX Ingress Controller에서 오픈소스 커뮤니티 ingress-nginx로 마이그레이션을 진행했습니다. 두 프로젝트는 이름은 비슷하지만 완전히 다른 프로젝트로, 관리 주체와 어노테이션 체계가 다릅니다.

전환 작업은 ArgoCD 기반 GitOps 체계로 관리되는 워크로드를 대상으로 전체 검색을 통해 어노테이션을 일괄 전환하는 방식으로 진행되었습니다. 대부분의 워크로드는 이 방식으로 문제없이 전환되었으나, Mattermost는 아직 ArgoCD 관리 체계로 전환되지 않은 상태였습니다. Mattermost Operator CR(Custom Resource)은 여전히 kubectl을 통해 수동으로 배포되고 있었고, 이 사실을 인지하지 못한 상태에서 GitOps 일괄 전환에서 누락된 것이 문제의 시작점이었습니다.

항목 NGINX Inc (F5) 커뮤니티 ingress-nginx
GitHub 저장소 nginxinc/kubernetes-ingress kubernetes/ingress-nginx
관리 주체 F5 NGINX Kubernetes 커뮤니티
어노테이션 prefix nginx.org/* nginx.ingress.kubernetes.io/*
WebSocket 설정 nginx.org/websocket-services timeout 어노테이션으로 대체
문서 위치 docs.nginx.com kubernetes.github.io

2. 인시던트 타임라인

2.1. 초기 장애 발생

  • KST 07:00: 방화벽 장애 발생
  • KST 07:00~14:00: 약 7시간 동안 네트워크 단절
  • KST 14:00: 방화벽 복구 완료

2.2. 부분 복구와 잔존 문제

  • 아웃바운드 HTTP API: 방화벽 복구 즉시 정상 동작
  • 인바운드 WebSocket: 정확히 60초마다 연결 끊김 반복
  • 모니터링 결과: 16개 WebSocket 클라이언트에서 총 1,163건+ 재연결 이벤트 관측

2.3. 증상 패턴

[14:05:23] WebSocket connected
[14:06:23] WebSocket disconnected (exactly 60s later)
[14:06:23] WebSocket reconnecting...
[14:06:24] WebSocket connected
[14:07:24] WebSocket disconnected (exactly 60s later)
...

핵심 관측: 연결 종료 시점이 정확히 60초 간격으로 발생. 이는 하드코딩된 timeout 설정을 강하게 시사합니다.


3. 원인 분석 과정

3.1. 초기 가설 — 방화벽 설정?

방화벽 복구 후에도 WebSocket만 문제가 지속되었으므로, 방화벽의 idle timeout 설정을 의심했으나:
– HTTP API는 정상 동작
– 방화벽 로그에 특이사항 없음
– 60초는 방화벽의 일반적인 기본 timeout 값과 불일치

방화벽은 원인이 아닌 것으로 추정

3.2. 두 번째 가설 — NGINX timeout 설정

WebSocket은 long-lived connection이므로, reverse proxy의 timeout 설정에 민감합니다. NGINX의 기본 proxy timeout은?

NGINX 공식 문서
proxy_read_timeout: 업스트림 서버로부터 응답을 읽을 때의 timeout (기본값: 60초)
출처: nginx.org/docs/http/ngx_http_proxy_module.html#proxy_read_timeout

60초 패턴과 정확히 일치!

3.3. Ingress 설정 확인

기존 Ingress 리소스의 어노테이션을 확인한 결과:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mattermost-ingress
  annotations:
    nginx.org/websocket-services: "mattermost-svc"  # ← 문제의 어노테이션
spec:
  rules:
  - host: chat.example.corp
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: mattermost-svc
            port:
              number: 8065

3.4. 근본 원인 확인

핵심 문제: nginx.org/websocket-servicesNGINX Inc Ingress Controller 전용 어노테이션입니다.

NGINX Inc 문서 (F5 상용 Ingress Controller 전용)
nginx.org/websocket-services: WebSocket 서비스 목록을 지정하여 해당 서비스에 대해 WebSocket 프록시 설정을 활성화
출처: docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-annotations/

커뮤니티 ingress-nginx는 이 어노테이션을 인식하지 못하고 무시합니다. 따라서:
1. WebSocket에 대한 특수 처리가 비활성화됨
2. 일반 HTTP proxy 설정 적용
3. proxy_read_timeout 기본값 60초 적용
4. WebSocket idle 상태에서 60초 경과 시 연결 종료

ingress-nginx 공식 문서 (Miscellaneous – WebSockets)
“Support for websockets is provided by NGINX out of the box. No special configuration required.
The only requirement to avoid the close of connections is the increase of the values of proxy-read-timeout and proxy-send-timeout.
The default value of these settings is 60 seconds.
A more adequate value to support websockets is a value higher than one hour (3600).”
출처: kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets


4. 해결 방법

4.1. 적용한 어노테이션

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mattermost-ingress
  annotations:
    # 커뮤니티 ingress-nginx 어노테이션으로 변경
    nginx.ingress.kubernetes.io/proxy-body-size: "1000m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
  rules:
  - host: chat.example.corp
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: mattermost-svc
            port:
              number: 8065

4.2. 각 어노테이션 상세 분석

nginx.ingress.kubernetes.io/proxy-read-timeout

항목 내용
의미 업스트림 서버로부터 연속된 두 read 연산 사이의 최대 대기 시간. WebSocket의 경우 클라이언트/서버가 메시지를 보내지 않는 idle 시간에 해당
기본값 nginx: 60초 / ingress-nginx ConfigMap: 60초 (출처)
적용값 3600초 (1시간)
선택 근거 WebSocket은 실시간 메시징에서 사용자가 1시간 이상 메시지를 보내지 않을 수 있으므로, 충분한 여유를 둠. ingress-nginx 공식 문서 권장값.
미설정 시 문제 60초 동안 메시지가 없으면 NGINX가 연결을 일방적으로 종료. WebSocket은 양방향 통신이므로 클라이언트는 즉시 재연결 시도 → 60초마다 reconnect 반복
이게 진짜 문제인 이유 WebSocket은 persistent connection이 핵심. 60초는 실시간 메시징에서 너무 짧음. 사용자가 잠깐 자리를 비우거나, 봇이 이벤트 대기 중일 때도 연결이 유지되어야 함.

nginx.ingress.kubernetes.io/proxy-send-timeout

항목 내용
의미 업스트림 서버로 연속된 두 write 연산 사이의 최대 대기 시간. 업스트림이 요청을 받는 속도가 느릴 때 영향
기본값 nginx: 60초 / ingress-nginx ConfigMap: 60초 (출처)
적용값 3600초 (1시간)
선택 근거 proxy-read-timeout과 대칭적으로 설정하여 양방향 통신 모두 안정성 확보
미설정 시 문제 업스트림 서버가 느리게 응답받을 경우 전송 timeout 발생 가능. WebSocket의 경우 큰 메시지 전송 시 문제 가능성 있음

nginx.ingress.kubernetes.io/proxy-body-size

항목 내용
의미 클라이언트 요청 본문의 최대 크기. client_max_body_size nginx 지시문에 매핑됨
기본값 ingress-nginx ConfigMap: 1m (1MB) (출처)
적용값 1000m (1000MB = ~1GB)
선택 근거 Mattermost는 파일 업로드 기능이 있으므로, 큰 파일 전송을 위해 여유있게 설정
WebSocket과의 관계 WebSocket 자체와는 직접 관련 없지만, 같은 Ingress에서 HTTP API도 처리하므로 함께 설정

4.3. proxy-connect-timeout은 왜 설정하지 않았는가?

항목 내용
의미 업스트림 서버와 최초 연결을 수립할 때의 timeout
기본값 ingress-nginx ConfigMap: 5초 (출처)
설정 여부 설정하지 않음 (기본값 유지)
이유 연결 수립은 일반적으로 5초 이내 완료. 이 값이 60초 reconnect 문제와 무관함. 오히려 너무 높게 설정하면 unhealthy upstream에 대한 failover가 지연될 수 있음

5. Timeout 값별 WebSocket 동작 비교

실제 테스트를 통해 확인한 결과:

proxy-read-timeout proxy-send-timeout WebSocket 동작 재연결 빈도 비고
60초 (기본값) 60초 (기본값) ❌ 60초마다 끊김 매우 높음 (시간당 60회) 실시간 서비스에 부적합
300초 (5분) 300초 (5분) ⚠️ 5분마다 끊김 높음 (시간당 12회) 짧은 대화에는 괜찮으나 idle 시 문제
3600초 (1시간) 3600초 (1시간) ✅ 안정적 유지 매우 낮음 ingress-nginx 공식 권장값
86400초 (24시간) 86400초 (24시간) ✅ 안정적 유지 거의 없음 과도하게 긴 설정 (리소스 낭비 가능성)
0 (무제한) 0 (무제한) 설정 불가 N/A nginx는 0을 무제한으로 해석하지 않음

권장값 선택 기준

  • 3600초 (1시간): 대부분의 실시간 애플리케이션에 적합. ingress-nginx 공식 문서 권장
  • 더 긴 값이 필요한 경우: 장시간 연결 유지가 필요한 IoT, 모니터링 시스템 등
  • 더 짧은 값이 허용되는 경우: 클라이언트가 주기적으로 ping/pong heartbeat를 구현한 경우 (아래 대안 참고)

⚠️ Timeout 값 변경 시 트레이드오프

NGINX의 proxy_read_timeoutproxy_send_timeout이 기본값 60초로 설정된 데에는 이유가 있습니다. 이 값을 변경할 때 발생할 수 있는 긍정적/부정적 영향을 반드시 이해해야 합니다.

값을 늘릴 때 (60초 → 3600초)

영향 내용
✅ 긍정적 WebSocket 등 long-lived connection이 안정적으로 유지됨. idle 상태에서도 연결이 끊기지 않아 재연결 오버헤드 제거
❌ 좀비 연결 증가 비정상 종료된 클라이언트(네트워크 단절, 브라우저 강제 종료 등)의 연결이 최대 3600초 동안 NGINX 워커에 남아있음. 연결당 메모리(수 KB~수십 KB)와 파일 디스크립터를 점유하므로, 대규모 클라이언트 환경에서는 worker_connections 고갈 가능성
❌ 장애 감지 지연 업스트림 서버가 응답 불능 상태에 빠져도 NGINX는 최대 timeout 시간만큼 대기한 후에야 502/504를 반환. 기본값 60초면 1분 내 감지되지만, 3600초면 최대 1시간 후에야 감지
❌ 리소스 낭비 의도하지 않은 long-lived HTTP 요청(예: 느린 클라이언트, 잘못된 API 호출)도 3600초 동안 연결이 유지되어 서버 리소스를 불필요하게 점유

값을 줄일 때 (예: 60초 → 10초)

영향 내용
✅ 긍정적 좀비 연결이 빠르게 정리되어 리소스 회수가 빠름. 업스트림 장애를 빠르게 감지
❌ 정상 연결 끊김 처리 시간이 긴 API 요청이나 대용량 파일 다운로드가 중간에 끊길 수 있음
❌ WebSocket 완전 사용 불가 10초 동안 메시지가 없으면 연결이 끊기므로 실시간 서비스 운영 자체가 불가능

권장 완화 전략

Timeout을 늘려야 하는 경우, 다음과 같은 완화 전략을 함께 적용하는 것을 권장합니다:

  • 서비스별 개별 설정: ConfigMap(전역)이 아닌 Ingress 어노테이션(개별)으로 설정하여, WebSocket이 필요한 서비스만 높은 timeout을 적용. 이번 사례에서 채택한 방법
  • 클라이언트 Ping/Pong 구현: 애플리케이션 레벨에서 heartbeat를 구현하면 timeout을 보수적(300~600초)으로 설정 가능
  • NGINX keepalive 설정 병행: upstream-keepalive-timeout, upstream-keepalive-connections 등으로 업스트림 연결 풀 관리
  • 모니터링 강화: 활성 연결 수(nginx_ingress_controller_nginx_process_connections) 메트릭을 모니터링하여 비정상 증가 조기 감지

6. 검증 및 결과

6.1. 적용 절차

# Ingress 리소스 수정
kubectl apply -f mattermost-ingress.yaml

# NGINX 설정 즉시 반영 확인
kubectl exec -n ingress-nginx <controller-pod> -- cat /etc/nginx/nginx.conf | grep proxy_read_timeout
# 출력: proxy_read_timeout 3600s;

6.2. 적용 후 결과

  • 즉시 효과 확인: 어노테이션 적용 후 재연결 이벤트가 즉시 멈춤
  • 안정성 검증: 이후 24시간 동안 1,163건+ → 0건으로 감소
  • 서비스 정상화: 16개 WebSocket 클라이언트 모두 안정적 연결 유지

6.3. 왜 즉시 멈췄는가?

  1. Ingress 어노테이션 변경 시 ingress-nginx controller가 NGINX 설정 파일을 즉시 재생성
  2. NGINX reload 수행 (graceful reload로 기존 연결은 유지)
  3. 새로운 WebSocket 연결은 3600초 timeout으로 처리
  4. 기존 60초 timeout이 적용된 연결은 마지막 재연결 후 새 설정으로 유지

7. 대안 분석 — 다른 방법은 없었을까?

7.1. WebSocket Ping/Pong Heartbeat 구현

개념

WebSocket 프로토콜은 RFC 6455 §5.5 (Control Frames)에서 Ping(§5.5.2)/Pong(§5.5.3) frame을 정의합니다. 클라이언트나 서버가 주기적으로 ping을 보내면, 상대방은 pong으로 응답하여 “연결이 살아있음”을 증명합니다.

구현 예시 (Node.js ws 라이브러리 기준)

아래 코드는 Node.js의 ws npm 패키지 API를 사용합니다. 브라우저의 표준 WebSocket API에서는 ws.ping()/ws.on('pong')을 직접 사용할 수 없으며, 애플리케이션 레벨 메시지로 heartbeat를 구현해야 합니다.

const ws = new WebSocket('wss://chat.example.corp/api/v4/websocket');

// 30초마다 ping 전송
const pingInterval = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.ping(); // WebSocket ping frame 전송
  }
}, 30000);

ws.on('pong', () => {
  console.log('Received pong from server');
});

장단점 비교

접근 방법 장점 단점 적용 난이도
Timeout 증가 (채택) 인프라 레벨 해결, 코드 변경 불필요, 즉시 적용 가능 좀비 연결이 오래 유지될 가능성 ⭐ 낮음
Ping/Pong Heartbeat 연결 상태 능동 확인, 네트워크 장애 즉시 감지 클라이언트/서버 코드 수정 필요, 트래픽 증가 ⭐⭐⭐ 높음
Timeout 증가 + Heartbeat 최고의 안정성 구현 복잡도 높음 ⭐⭐⭐⭐ 매우 높음

결론

  • 단기 해결: Timeout 증가 (본 사례의 선택)
  • 장기 개선: 클라이언트 코드에 Heartbeat 추가 후 timeout을 좀 더 보수적 값(예: 300초)으로 조정 고려 가능

7.2. Global ConfigMap 설정 vs 개별 Ingress 어노테이션

ConfigMap 설정 (전역)

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  proxy-read-timeout: "3600"
  proxy-send-timeout: "3600"
방법 적용 범위 우선순위 권장 사용처
Global ConfigMap 모든 Ingress에 기본값 적용 낮음 (어노테이션에 의해 덮어씌워짐) 클러스터 전체 정책
Ingress 어노테이션 (채택) 특정 Ingress만 적용 높음 (ConfigMap 덮어씀) 서비스별 커스터마이징

선택 이유

  • Mattermost 외 다른 서비스는 짧은 timeout이 적합할 수 있음
  • Blast radius 최소화: 하나의 서비스만 영향받도록 격리
  • 명시적 문서화: Ingress YAML을 보면 WebSocket 설정임을 즉시 알 수 있음

8. 어노테이션 호환성 매핑 테이블

Ingress Controller 전환 시 반드시 확인해야 할 주요 어노테이션 매핑:

기능 NGINX Inc (F5) 커뮤니티 ingress-nginx 비고
WebSocket 지원 nginx.org/websocket-services: "svc1,svc2" nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
호환 없음 — 수동 전환 필수
Client 최대 body size nginx.org/client-max-body-size: "10m" nginx.ingress.kubernetes.io/proxy-body-size: "10m" 키 이름 다름
Proxy timeout nginx.org/proxy-read-timeout: "30s" nginx.ingress.kubernetes.io/proxy-read-timeout: "30" 단위 표기법 다름 (s 유무)
SSL redirect nginx.org/redirect-to-https: "true" nginx.ingress.kubernetes.io/ssl-redirect: "true" 키 이름 다름
Rewrite target nginx.org/rewrites: "..." nginx.ingress.kubernetes.io/rewrite-target: "/..." 구조 완전히 다름

⚠️ 전환 시 주의사항

  1. 자동 변환 없음: 두 프로젝트 간 어노테이션은 자동으로 변환되지 않음
  2. 무시됨, 오류 없음: 잘못된 어노테이션은 조용히 무시됨 (로그에 경고만 출력)
  3. 문서 확인 필수:
  4. NGINX Inc: docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-annotations/
  5. ingress-nginx: kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/

9. 교훈 및 Ingress Controller 전환 체크리스트

9.1. 핵심 교훈

1️⃣ “비슷한 이름 ≠ 같은 프로젝트”

  • ingress-nginxNGINX Ingress Controller완전히 다른 프로젝트
  • 관리 주체, 저장소, 문서, 어노테이션이 모두 다름
  • 전환 시 “이름만 바꾸면 되겠지”라는 가정은 위험

2️⃣ “조용히 실패하는 설정이 가장 위험하다”

  • 잘못된 어노테이션은 오류 없이 무시됨
  • 테스트 환경에서는 발견하기 어려움 (짧은 세션에서는 60초 timeout이 문제 안 됨)
  • 실제 프로덕션 부하에서만 드러나는 문제

3️⃣ “방화벽 장애가 숨겨진 설정 문제를 드러냈다”

  • 전환 후 정상 동작하는 것처럼 보였으나, 실제로는 잠재된 문제였음
  • 방화벽 장애로 대량 재연결 발생 → 60초 패턴이 명확히 관측됨
  • 장애는 때로 더 큰 문제를 발견하는 기회

9.2. Ingress Controller 전환 시 체크리스트

전환 전 (Pre-migration)

  • [ ] 현재 사용 중인 모든 어노테이션 목록 작성
  • [ ] 새 Ingress Controller의 어노테이션 문서 확인
  • [ ] 어노테이션 매핑 테이블 작성 (1:1 매핑 불가능한 것 체크)
  • [ ] WebSocket 사용 서비스 목록 확인
  • [ ] Staging/개발 환경에서 먼저 테스트

전환 중 (Migration)

  • [ ] Ingress YAML의 모든 어노테이션 prefix 변경
  • [ ] 기능별 동등성 확인 (특히 timeout, body-size, rewrite 등)
  • [ ] NGINX 설정 파일 diff 확인 (kubectl exec ... cat /etc/nginx/nginx.conf)
  • [ ] 배포 전 kubectl apply --dry-run=client로 검증

전환 후 (Post-migration)

  • [ ] WebSocket 서비스 장시간 연결 테스트 (최소 5분 이상)
  • [ ] 모니터링: 재연결 빈도, 502/504 오류율 추적
  • [ ] NGINX access log에서 upstream_response_time 확인
  • [ ] 클라이언트 로그에서 unexpected disconnection 패턴 검색
  • [ ] 롤백 계획 준비 (기존 Ingress Controller를 parallel로 유지 권장)

9.3. WebSocket 서비스 운영 체크리스트

  • [ ] proxy-read-timeout 최소 3600초 설정 (ingress-nginx 공식 권장)
  • [ ] proxy-send-timeout도 동일하게 설정 (대칭성 유지)
  • [ ] 클라이언트 재연결 로직 구현 (네트워크 일시 장애 대비)
  • [ ] 선택사항: Ping/Pong heartbeat 구현 (능동적 연결 확인)
  • [ ] 모니터링: WebSocket 연결 수명(lifetime) 메트릭 추적

10. 참고 자료

공식 문서

  1. ingress-nginx 어노테이션 문서
    https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
  2. ingress-nginx ConfigMap 옵션
    https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/
  3. ingress-nginx WebSocket 공식 가이드
    https://kubernetes.github.io/ingress-nginx/user-guide/miscellaneous/#websockets
  4. NGINX proxy_read_timeout 공식 문서
    https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
  5. NGINX WebSocket 프록시 가이드
    https://nginx.org/en/docs/http/websocket.html
  6. NGINX Inc vs Community ingress-nginx 비교
    https://docs.nginx.com/nginx-ingress-controller/install/migrate-ingress-nginx/

소스 코드

  1. ingress-nginx 기본값 설정 코드
    GitHub: kubernetes/ingress-nginx/internal/ingress/controller/config/config.go
    (관련 라인: ProxyReadTimeout: 60, ProxySendTimeout: 60)

RFC

  1. WebSocket Protocol (RFC 6455)
    https://datatracker.ietf.org/doc/html/rfc6455

마치며

이번 인시던트는 “이름이 비슷하다”는 이유로 간과하기 쉬운 어노테이션 호환성 문제가 프로덕션 장애로 이어진 사례였습니다. 특히 WebSocket처럼 long-lived connection을 사용하는 서비스는 timeout 설정에 매우 민감하므로, Ingress Controller 전환 시 반드시 꼼꼼한 검증이 필요합니다.

핵심 요약

  • 사실: proxy_read_timeout 기본값 60초는 공식 문서로 확인됨
  • 사실: ingress-nginx는 nginx.org/* 어노테이션을 무시함
  • 사실: 어노테이션 적용 즉시 재연결 문제 해결됨
  • ⚠️ 추정: 방화벽 장애가 없었다면 문제가 한동안 발견되지 않았을 가능성 높음
  • ⚠️ 한계: 다른 서비스에서 동일 문제가 숨어있을 가능성 (전수 조사 필요)

마지막으로, 문서를 믿되, 검증하라는 원칙을 다시 한번 되새기게 된 사례였습니다.

조회수: 1

댓글 남기기