[트러블 슈팅] 가만히 둔 EC2 가 5개월 만에 504 를 뱉은 이유 (JVM 메모리, Swap, CPU 크레딧)

max_409·5일 전

트러블 슈팅

목록 보기
2/2

1. 배경

봉투백서

봉투백서는 사회 초년생을 위해 경조사비를 상황에 맞게 추천해주고, 관리해주는 서비스입니다.
AWS EC2 서비스인 t2.micro(1GB) 인스턴스를 사용했고, docker-compose를 사용해 아래와 같은 컨테이너를 돌렸습니다.

NAMES            설명
bongbaek1        Spring Boot 3.5 / Java 21 API 서버. 8081 포트
bongbaek2        bongbaek1 과 같은 이미지. 8082 포트. 로드밸런싱을 위한 이중화 컨테이너
nginx            리버스 프록시. 80/443 을 받아 두 서버로 least_conn 분배

redis            인증 토큰, 캐시 저장소

prometheus       15초마다 actuator 와 nginx exporter 지표 수집
grafana          Prometheus 대시보드. 
dozzle           컨테이너 로그 웹 뷰어
nginx-exporter   nginx stub_status 를 Prometheus 지표로 변환

1월 배포 인해 변경사항은 없었으나, 6월부터 CPU 크레딧이 고갈되며 모든 요청에 대해 504 time out 에러를 뱉는 문제가 생겼습니다.

2. 문제 상황

  • 1월 6일 배포 후 5개월간 5xx 에러 0건
  • 6월 20일 → Docker 헬스체크 요청이 timeout(10초)에 걸렸고, 이날부터 사용자 요청 모두 504 time out 및 에러 서버 제외 로직 때문에, 503 nginx error까지 발생함

3. 원인 분석

3-1. 무엇이 time out을 유발했는가?

메모리 초과로 스래싱 발생
- EBS 읽기: 5/13 814만/일 → 7월 초 2,700만/일
- 스래싱으로 인해 CPU 사용량이 5월 초 8.6% -> 6월 초 평균 10.5%로 증가
- 6월 20일에 남은 크레딧 모두 소진된 이후 요청 처리 속도가 감소

스래싱으로 CPU 사용량 증가 -> 상한선을 넘어 CPU 크레딧 고갈
- 1gb로 돌리기 힘든 8개 컨테이너를 운용하기 위해 Swap Memory 전략을 사용
- t2.migro는 CPU 사용량 10% 이상부터 크레딧 소진이 발생

3-2. 가만히 둔 서버인데 왜 메모리 초과가 발생했는가?

  • 프로젝트를 마무리하면서 메트릭 기록을 백업해 두지 않아, 얼만큼 메모리를 사용했는지 찾아볼 수 없었습니다.. 그래서 추청된 원인을 나열해 보려고 합니다.

Swap Memory로도 해결할 수 없는 상시 사용 메모리

  • Swap Memory 부족한 램의 해결책이 아닙니다
    OOM과 같은 메모리 사용률이 불규칙 할 때 서버의 다운 등을 해결하는데는 용이하지만,
    상시 가용해야하는 컨테이너를 돌리기 위해서는 인스턴스의 램 크기(t2.micro=1gb)내에서 동작 해야합니다

JVM 컨테이너의 메모리 사용량 증가

  • 8개의 컨테이너를 동시에 작동시키기에는 t2.micro는 부족한 메모리 환경이었다. JVM 컨테이너(bongbaek1, bongbaek2)는 기본 상한인 ram 한도 25%까지 자랄 수 있습니다
    문제를 조치하는 시점에서 메모리 사용률이 약 390MB이었다 -> Spring boot 컨테이너 가동 시 처음 메모리 사용량은 평균 200MB로 추정

두 컨테이너로만 800MB를 사용했기에 버틸 수 있는 메모리 수준을 넘어섰고, 사용자 요청, healthcheck와 같은 상시 요청 처리가 disk로 넘아간 것이 주된 원인이라고 생각합니다

4. 해결 방법 검토

1. JVM 옵션 설정

  • 컨테이너 설정으로 제한
# docker-compose.yml

services:
  bongbaek1:
    container_name: bongbaek1
    image: bongbaek-server:latest
    restart: unless-stopped            # OOM 으로 죽으면 다시 띄움
    mem_limit: 450m                    # 컨테이너 램 상한. 넘으면 즉시 OOM kill
    memswap_limit: 450m                # 램+스왑 합산 상한. mem_limit 과 같게 → 스왑 사용 금지
    environment:
      JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=50 -XX:+ExitOnOutOfMemoryError"
                                       # MaxRAMPercentage=50 : 힙 상한 = 컨테이너 한도의 50% (225MB)
                                       # ExitOnOutOfMemoryError : OOM 시 즉시 종료 → restart 로 재기동
  • 빌드 설정으로 제한
// build.gradle
tasks.named('bootBuildImage') {
    imageName = 'bongbaek-server'
    environment = [
        'BP_JVM_VERSION'      : '21',   // 이미지에 넣을 JDK 버전
        'BPL_JVM_THREAD_COUNT': '50',   // 메모리 계산에 쓸 스레드 수. 기본 250, 줄이면 힙에 더 배정
        'BPL_JVM_HEAD_ROOM'   : '10'    // 한도 중 JVM 이 비워둘 비율(%). glibc 등 계산 밖 메모리 여유분
    ]
}

2. 컨테이너 단일화

  • 이중화 전략(무중단 배포와 가용성을 위해 컨테이너 이중화를 선택)을 위해 2개의 API 컨테이너를 상시 가동했으나, 1gb로는 어려운 구조였던 거 같습니다
  • 배포 때에만 2개를 띄어 배포 안정성과 무중단을 구현하는 방향이 좋았을 거 같다

3. 모니터링 스택을 클라우드로 변경
dozzle, grafana, prometheus, nginx-exporter 컨테이너를 모두 합치면 메모리 사용량이 300~350MB 입니다.
해당 스택을 grafana cloud로 변경한다면, 위 스택들로 이루고자 하는 nginx, api 서버 메트릭 관리와 로그 저장까지 가능합니다.
무료 버전 한도는 14일 보관, 로그 50GB/월, 대시보드 1,000 수준으로 14일 이내로 로그 메트릭 저장하는 파이프라인만 구성한다면 기존과 동일한 수준에서 처리가 가능합니다

5. 회고

- 백업 데이터 소실

  • 인스턴스를 삭제 하면서, 로그는 백업했지만 메트릭에 관한 정보를 추출하지 않았습니다.
    이로인해 언제부터 크레딧이 왜 고갈됐으며, 어떤 문제가 메모리 스래싱을 야기했는지 파악할 수 없었습니다.

- observerbility 부족

# nginx.conf

upstream backend {
    least_conn;
    server bongbaek1:8080 max_fails=3 fail_timeout=10s;
    server bongbaek2:8080 max_fails=3 fail_timeout=10s;
}
proxy_connect_timeout 3s;
proxy_read_timeout    20s;
proxy_next_upstream   error timeout http_502 http_503 http_504;
  • 서버 요청 장애에 대한 로드밸런싱 전략은 존재했으나 해당 문제가 발생 시 책임자가 볼 수 있는 alert 기능이 부재했습니다

  • 또한, 지속적인 healthcheck 기능은 있었으나 문제 발생 시 최종 재시작하는 대응책이 없었습니다

[출처]
Swap Memory
https://docs.cloud.google.com/kubernetes-engine/docs/how-to/node-memory-swap?hl=ko
https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/container-swap.html

JVM Memory 증가
https://mainmethod0126.github.io/java/Analyzing-JAVA-Memory-Leak-Issues

2개의 댓글

안녕하세요! 배포 변경 없이 504가 난 원인을 메모리 부족에서 스래싱으로 그리고 CPU 크레딧 고갈로 이어서 보여 주신 게 흥미로웠습니다. 조치는 컨테이너별 힙 상한으로 하셨나요? 저도 Velog에 백엔드 트러블슈팅 기록 쓰고 있어요. 팔로우해 두고 다음 글도 볼게요

1개의 답글