dev notes

반년 간의 EC2 Spring 배포 Thread Starvation 장애 해결기

2024-08-1016 min read
공유

개요 및 프로젝트 특징(조건)#

작년 이맘때 샀던 도메인 갱신도 마쳤고, 가운영 반년에 실제 운영 1년이 넘어가는 시점입니다.

Dayner 프로젝트 인프라 구성도

Dayner 프로젝트 인프라 구성도

약 22회의 기능 추가/수정 배포, 30번 이상의 hotfix, 두 번의 대규모 리팩터링을 거친 프로젝트로 (AWS EC2 프리티어), 내가 진행해본 것 중 가장 공을 들이고 여러 기술을 적용하면서 지속적으로 신경 써온 프로젝트입니다. 특징으로는

  1. 최소 비용이 목표라 aws 의 프리티어 ec2를 사용중
  2. 특성상 카페 이용 시간대만 사용자가 몰리고, 트래픽이 증가되는 시간대가 정해져있다. 따라서 AutoScale 을 도입.
  3. 테스트 상으로 5명의 유저가 분당 150 내외의 GET 요청까지는 무리없이 커버한다.

스프링으로 서버를 구축하고 오토 스케일링 그룹을 만들어 유연하게 대처할 수 있도록 구성해두었습니다.

10개월~ 정도는 규모도 작고 이용자도 적어서 잘 이용하고 있었는데 사람이 몰리지 않는 새벽 시간대나 아침 시간대에도 인스턴스 교체(오토 스케일링으로 인한 인스턴스 복제 후에 오래된 인스턴스가 삭제 되는 현상)가 일어나는 문제점이 발생하였습니다.

또한 서버에서 스케일-아웃 될 시기에 스프링 서버에 트랜잭션이 미완료 상태에 있는 경우 이를 고려하지 않은 인프라 구조와, 서버로 인해서 사용에 불편이 예상되었습니다.

  1. 중요 트랜잭션이 실행되고 있는걸 캐치해서 상태를 보여주는 엔드포인트를 만들어 이를 헬스체크에 사용하고 인프라 구조를 블루/그린 방식으로 변경하거나
  2. 최소 인스턴스 개수를 2개로 띄우거나(돈이 2배로!)

여러 방법이 있었는데, ASG에서 스케일-아웃 발생 원인을 모니터링/로깅해두는 기능이 있어서 확인해보았더니

헬스체크 기준이 너무 타이트했다.

EC2 프리티어를 사용하고 있었는데, 헬스체크 설정은 이를 전혀 고려하지 않은 상태였던 것입니다.

EC2 인스턴스가 처음 시작된 뒤 apt 업데이트, Spring 코드 가져오기(CI/CD) 등의 과정에서 적절한 대기 시간이 없다 보니 CPU에 부하가 걸렸고, 경우에 따라서는 Spring 프로젝트 시작까지 5분 이상 걸리는 경우도 있었습니다.

기존 인스턴스 1이 있었고 스케일 업을 통해 새로 만들어진 인스턴스 2가 프로젝트 시작도 채 해보지 않고 healty check 에 걸려서 또다시 중지 처리가 되고 새로운 인스턴스 3가 만들어지는 경우도 존재했습니다.

따라서

AutoScailing Group 스케일-아웃 조건 러프하게 만들기#

를 통해서 스케일-아웃 당시에 인스턴스가 3개,, 4개 발생되는것을 막을수 있었습니다.

관리 패널 내의 EC2>대상그룹>ALB>대상검사 경로로 들어가면 설정이 가능합니다.

EC2 대상그룹 ALB 헬스체크 설정 화면

EC2 대상그룹 ALB 헬스체크 설정 화면

그렇게 한 달 정도는 별탈 없이 사용했습니다.

당시 Prometheus를 다른 VPC의 EC2 위에 올려서 서버를 모니터링하고 있었는데, 메모리도 준수하게 작동되고 있었습니다.

Thread starvation 의 발생#

Thread Starvation 발생 시 로그

Thread Starvation 발생 시 로그

눈으로 바로 보이는 직관적인 문제가 아니라서 원인 파악부터 해야 했습니다.

1. EC2의 시스템 로그 확인하기

EC2 시스템 로그 — 인스턴스 정상 동작 확인

EC2 시스템 로그 — 인스턴스 정상 동작 확인

Spring 프로젝트 셧다운으로 인한 헬스체크 실패

Spring 프로젝트 셧다운으로 인한 헬스체크 실패

EC2 인스턴스 자체는 문제없이 실행 중이었습니다. 다만 Spring 프로젝트가 셧다운되면서 헬스체크에 실패하고 교체가 반복되는 상황이었습니다.

2. CPU, 메모리 사용량 확인하기

EC2와 로드밸런서의 모니터링 서비스를 활용하여 혹시 트래픽이 증가해서 발생했는지 확인해보았습니다. 이 역시 평소와 비슷한 정도의 트래픽이었습니다.

이때 인프라에는 문제가 없다고 판단하고 시선을 Spring 서버로 돌렸습니다. 지금 생각하면 시야가 좀 좁았던 것 같습니다.

3. 스프링 프로젝트의 모니터링 확인

Prometheus/Grafana 모니터링 — Heap·GC 안정 확인

Prometheus/Grafana 모니터링 — Heap·GC 안정 확인

Heap 영역도 안정적으로 이루어지고 GC 또한 균등한 매그니튜드로 실행되고 있음을 확인했습니다.

설계 문제로 인한 메모리 누수는 아니라는 것이 확인됐습니다. 그럼에도 실마리가 잡히지 않았습니다.

4. 스레드 덤프 분석

스레드 덤프 분석 — CloudWatch + jstack + MAT

스레드 덤프 분석 — CloudWatch + jstack + MAT

CPU 사용량이 70%를 넘거나 요청이 5건 이상 들어오는 경우를 CloudWatch로 포착해서, 이벤트 발생 후 10분간 30초 간격으로 jstack을 활용해 스레드 덤프를 자동 수집하고 MAT으로 분석했습니다.

상당한 수작업이었지만, 발생 당시 스레드들은 TIMED_WAITING 또는 WAITING 상태에서 CPU를 거의 점유하지 않고 있었고, BLOCKED 상태가 장기화되는 경우도 없었습니다.

스레드 덤프 자체에는 이상이 없었고, 오히려 덤프 생성이 높은 CPU 사용량이나 스레드 BLOCKED 장기화 이전에 멈춰버렸다는 점이 눈에 들어왔습니다. 다시 애플리케이션 외부에 문제가 있다는 쪽으로 판단이 기울었습니다.

starvation 을 해결하기 위한 다양한 시도#

1. HikariCP 풀사이즈 변경

기본값이 10으로 설정되어 있었는데, EC2 프리티어는 코어 수 2개에 코어당 스레드 수 2개로 총 4개의 스레드에 메모리도 1GB라서 시스템 리소스가 과부하될 수 있겠다고 판단했습니다.

조정하려고 보니 개수 공식이 존재했고, 고려해야 할 상황도 여러 가지였습니다.

다행히도

  • id AUTO 전략을 사용한적은 없었고,
  • 1회의 요청에 2개 이상의 커넥션을 만드는 Lazy 로딩으로 설정된 OneToMany나 ManyToMany 전략을 사용하지 않았고,
  • db 요청에 관한 쿼리는 JPQL 로 fetch join 을 사용하여 1회만에 수행가능하게 만들어놓았다.

HikariCP 커넥션 풀 개수 산출 공식

HikariCP 커넥션 풀 개수 산출 공식

하지만 마진을 두어 Cm을 2로 잡았고 Tn 은 현재 Ec2 의 스펙을 따라서

EC2 t2.micro 인스턴스 사양 (1 vCPU, 1GB RAM)

EC2 t2.micro 인스턴스 사양 (1 vCPU, 1GB RAM)

4로 설정하여 최소 풀의 크기는 5, 여기에 1의 추가 마진을 주어 6으로 변경 해보기도 하였습니다.

커넥션 풀 크기 조정 후에도 셧다운 재발

커넥션 풀 크기 조정 후에도 셧다운 재발

결과는 아쉽게도 셧다운 문제가 해결되지 않았습니다.

2. EC2 의 스왑 메모리 적용

사용 중인 EC2의 메모리가 총 1GB라 Spring에서 사용할 수 있는 메모리가 부족한 것 같다는 판단이 들었습니다.

메모리가 더 큰 EC2 인스턴스로 교체하는 방법도 있었지만, 최소 비용 운영이 목표였기 때문에 인스턴스 교체는 최후의 수단으로 미뤄두었습니다.

EC2 인스턴스 스펙 업그레이드 비용 비교

EC2 인스턴스 스펙 업그레이드 비용 비교

간헐적 끊김 현상을 막겠다고 운영비를 두 배로 늘리는 건 정말 최후의 수단입니다.

다행히도, EC2는 PC의 가상 메모리처럼 디스크용량을 메모리로 스왑하여 사용이 가능합니다.

EC2 스왑 메모리 설정 과정

EC2 스왑 메모리 설정 과정

스왑 메모리 할당 확인

스왑 메모리 할당 확인

그렇다면 해결이 되었을까?

결론부터 말하자면 스왑 메모리의 도입으로 Stravation으로 인한 셧다운 문제는 해결되었다.

스왑 메모리 도입 후 셧다운 문제 해결 확인

스왑 메모리 도입 후 셧다운 문제 해결 확인

OOM 의 발생

하지만 얼마 지나지 않아 OOM(Out Of Memory)가 발생하게 되었는데..

스왑 메모리 할당으로 인한 디스크 공간 부족#

OOM, 즉 메모리 부족입니다.

스왑 메모리로 용량도 늘렸고 모니터링 내역도 특이점이 없었는데 이상했습니다. 혹시 디스크 공간은 충분한가 싶어서 인스턴스 상태를 확인해보았더니

디스크 사용량 99% 확인

디스크 사용량 99% 확인

Wa! 99%!

여유 디스크 공간을 스왑 메모리로 할당해버리면서 디스크 공간이 거의 꽉 차버린 것이었습니다.

왜 사전에 확인을 못했을까?

프리티어 EC2의 기본 디스크 공간은 8GB인데, 초반에 Java와 JDK, AWS 클라이언트 설치만 했을 때 4GB를 넘지 않았기 때문에 최대 30GB까지 추가할 수 있었지만 따로 설정을 변경하지 않았습니다.

EC2 프리티어 EBS 디스크 용량 — 최대 30GB 무료

EC2 프리티어 EBS 디스크 용량 — 최대 30GB 무료

30기가 까지는 무료로 제공됩니다.

남아 있던 4GB는 이후 CI/CD를 구축하면서 CodeDeploy 관련 파일 설치, 운영 중 쌓이는 로그 파일들, 그리고 3GB를 스왑 메모리로 할당하면서 모두 소진된 것이었습니다.

추가 디스크 공간 할당

바로 EC2 내리고 볼륨 관리자 EBS에서 시원하게 12기가를 추가해서 20기가를 맞춰주었습니다.

EBS 볼륨 12GB 추가 할당 (8GB → 20GB)

EBS 볼륨 12GB 추가 할당 (8GB → 20GB)

디스크 확장 후 안정화된 인스턴스 상태

디스크 확장 후 안정화된 인스턴스 상태


(0902추가)

현재 거의 한 달이 지났고, Thread Starvation 문제는 해결된 상태입니다.

오래 운영해오기도 했고 새 디자인도 나온 만큼 서버 안정성이 뒷받침되어야 한다고 생각하던 차에 여러 번의 서버 이슈가 터졌습니다. 매번 다른 오류를 뱉어내서 마음고생도 있었지만, 그 과정에서 JVM 구조를 직접 들여다보고 OOM을 여러 형태로 경험하고 스레드 덤프 분석도 해보면서 꽤 밀도 있는 경험을 했습니다.

Connected Notes