Incident 2026 09 29 Nginx 연결 고갈 - 100-hours-a-week/KTB4-2nd-Cloud GitHub Wiki

2026-09-29 Nginx 동시 연결 고갈

강사님이 운영 도메인에 의도적으로 부하를 보낸 통제 테스트에서, 약 5분 동안 로그인이 되지 않고 일부 API가 500을 반환했습니다. CPU와 메모리에는 여유가 있었고, Nginx의 동시 연결 수가 먼저 한도에 닿은 게 원인이었습니다.

항목 내용
영향 시간 21:29 ~ 21:34 (KST)
영향 로그인 실패, 일부 API 500
원인 /api/trips/map으로 몰린 요청이 Nginx 연결 슬롯을 모두 사용
감지 Nginx 5xx Alarm → Discord
복구 부하 종료 후 자동 복구, 재시작 없음

감지

21시 31분에 Discord로 Alarm이 왔습니다.

Discord로 전달된 Nginx 5xx Alarm

Alarm의 12:26:00은 UTC 기준 5분 집계 구간의 시작입니다. 21시 26분부터 5분 동안 Nginx 5xx가 41건 쌓여, 구간이 끝난 21시 31분에 알림이 왔습니다.


타임라인

시각 확인한 내용
21:28 TCP 연결 22개, CPU 3.85% (평소 수준)
21:29 TCP 연결 1,521개로 급증, Nginx 연결 부족 오류 시작
21:30 TCP 연결 1,557개, CPU 44.46%
21:31 Nginx 5xx 41건으로 Alarm 발생
21:32 ~ 21:33 TCP 연결 1,550개 안팎 유지, 연결 부족 오류 계속
21:34 TCP 연결 52개로 감소
21:35 TCP 연결 13개, CPU 2.53%로 정상 복귀

EC2 상태 검사는 전체 구간에서 정상이었습니다.


요청이 몰린 곳

Nginx 로그를 경로와 상태 코드로 묶어보니 요청 대부분이 인증 없는 지도 조회였습니다.

요청 상태 Upstream 상태 건수
GET /api/trips/map 401 401 24,356
GET /api/trips/map 500 - 37
GET /api/trips/map 499 - 6

경로와 상태 코드별 요청 수

대부분은 Backend까지 가서 1ms 만에 401로 끝났습니다. 500으로 끝난 요청은 Upstream 상태가 -여서, Backend에 도착하지도 못하고 Nginx에서 끝난 요청이었습니다.

Alarm의 41건은 모든 경로의 5xx이고, 위 37건은 그중 /api/trips/map만 센 값입니다.


원인

같은 구간 Nginx Error 로그에 이 메시지가 반복됐습니다.

worker_connections are not enough
시각 오류 건수
21:29 1,198
21:30 18,264
21:31 22,494
21:32 22,858
21:33 24,156
21:34 8,766

연결 부족 오류의 분당 집계

같은 연결에서 오류가 반복 기록되기 때문에, 이 숫자가 요청 수나 사용자 수를 뜻하지는 않습니다.

당시 Nginx 설정은 이랬습니다.

worker_processes auto;   (App CPU 2개 → Worker 2개)
worker_connections 768;

Worker 두 개의 연결 한도를 합치면 1,536개입니다. 장애 구간의 TCP 연결 약 1,550개와 거의 같고, 연결 부족 오류도 딱 그 구간에만 나왔습니다.

App CPU와 TCP 연결 수

CPU는 최고 44%로 여유가 있었습니다. Backend가 401을 빠르게 돌려줘도 요청마다 연결은 계속 필요했고, 연결 슬롯이 먼저 바닥났습니다.

장애를 키운 조건은 이렇게 정리했습니다.

  • IP별 요청 속도 제한이 없음
  • IP별 동시 연결 제한이 없음
  • Nginx 연결 수와 연결 부족 오류를 보는 Alarm이 없음
  • Access 로그에 Client IP가 없어 요청 출처를 확인할 수 없음
  • Nginx 앞에 CloudFront, ALB, WAF 같은 계층이 없음

worker_connections만 늘리면 늘어난 슬롯도 같은 방식으로 다 쓸 수 있어서, 숫자를 올리는 것만으로는 해결되지 않는다고 봤습니다.


같은 시간 AI 로그

같은 시각 AI 로그에도 요청이 몰린 것처럼 보이는 구간이 있었는데, 확인해보니 이번 부하와 관계없는 로그였습니다.

  • /ready와 /health가 몇 ms 간격으로 찍힌 건 health check 한 번에 내부 확인이 함께 기록된 것
  • 21시 29분 51초 전후로 몰린 요청은 여행 121의 AI 처리 진행률과 결과 전달

공격 요청은 인증 없는 조회라 AI 작업을 만들지 않고, Worker 8000번은 App Security Group에서만 접근할 수 있습니다.


복구 확인

부하가 끝난 뒤 재시작 없이 다음 응답을 확인했습니다.

GET https://yeodam-2gether.com/                     → 307 (로그인 경로로 이동)
GET https://yeodam-2gether.com/api/actuator/health  → 200
GET https://yeodam-2gether.com/api/trips/map        → 401

대응 방향

항목 방향
요청 제한 일반 API에 Nginx limit_req, limit_conn 적용
반복 호출 경로 /api/trips/map, 인증 API에 별도 제한 검토
사진 업로드 일반 API와 다른 제한과 Timeout 적용
연결 한도 OS 파일 디스크립터 확인 후 worker_connections 조정 검토
감지 TCP 연결 급증, 연결 부족 오류 Alarm 추가
출처 확인 Client IP 기록 범위와 보존 기간 결정
앞단 보호 CloudFront 또는 ALB를 도입하는 단계에서 AWS WAF 적용 검토

사진 업로드는 정상 요청도 전송이 몇십 초씩 걸립니다. 일반 API와 같은 강한 제한을 걸면 정상 사용자가 막힐 수 있어서, 조회 API와 업로드 API의 제한은 나눠서 정하고 부하 테스트로 확인한 뒤 적용합니다.

공격 도구와 요청 출처는 로그에 남아 있지 않아 확인하지 못했습니다.


다시 조회하는 방법

경로별 상태 코드 (/yeodam/v1/nginx/access, 2026-09-29 21:26 ~ 21:31 KST)

fields method, normalized_route, status, upstream_status
| filter method = "GET"
| filter normalized_route = "/api/trips/map"
| stats count(*) as requests by status, upstream_status
| sort status asc

연결 부족 오류 (/yeodam/v1/nginx/error, 2026-09-29 21:28 ~ 21:36 KST)

fields @timestamp, @message
| filter @message like /worker_connections are not enough/
| stats count(*) as errors by bin(1m) as minute
| sort minute asc

Host 지표 (2026-09-29 21:26 ~ 21:35 KST, 1분 단위)

  • AWS/EC2: CPUUtilization, NetworkIn, NetworkOut, StatusCheckFailed
  • CWAgent: mem_used_percent, netstat_tcp_established, netstat_tcp_time_wait
⚠️ **GitHub.com Fallback** ⚠️