AWS EC2 무중단 Scale-up 적용기

,

🔥 메모리 부족, 서버 중단

레벨 3 방학이 시작된 다음 날 토요일 아침, 리뷰미 서비스가 중단되었었다. 아침 7시 남짓부터 접속이 불가능했고, 그 이유는 리눅스 로그를 담는 syslog에서 확인할 수 있었다. 메모리 부족으로 인해 oom-killer가 java 프로세스를 중단하게 되었다.

2024-08-23T22:28:02.426195+00:00 kernel: Out of memory: Killed process 69224 (java) total-vm:2967296kB, anon-rss:326152kB, file-rss:1644kB, shmem-rss:0kB, UID:0 pgtables:1160kB oom_score_adj:0

그리고 그 메모리 부족을 일으켰던 건 nginx였다.

2024-08-23T22:28:02.185357+00:00 kernel: nginx invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
2024-08-23T22:28:02.377758+00:00 kernel: CPU: 1 PID: 72271 Comm: nginx Not tainted 6.8.0-1009-aws #9-Ubuntu

당시에 스왑 메모리가 존재하지 않았던 것이 원인이었겠지만, 인스턴스 한 개가 WAS / WS를 모두 담당하는 것은 책임이 과하다고 보았다. 같은 인스턴스에 존재하므로 부작용이 서로 간섭될 가능성이 존재했다 (실제로 nginx의 활동으로 인해 어플리케이션이 종료되기도 했으니…). 이를 제외하고서라도 스왑 메모리가 필요하다는 의미였으며, 이는 SSD를 활용해 RAM의 역할을 하게 되므로 성능상 손해를 볼 수 있다고 생각했다.

📦 무중단 인스턴스 스케일 업!

팀 내 회의를 통해 Nginx와 Tomcat 인스턴스를 분리하자는 결론이 났고, 추가로 프로덕션에서 사용할 인스턴스를 t4g.micro에서 t4g.small로 스케일업 하기로 했다. 나름 도전적인 과제로, 이를 모두 무중단으로 진행해보면 어떨까? 라는 생각을 했다. 실제로 사용자가 최대한 불편함을 느끼지 않도록 하기 위함이다.

기존 인스턴스 구조와 변경하고자 하는 구조는 아래 그림과 같다. 처음 Https를 적용하기 위해 Nginx를 도입했으며, 이를 분리해 오른쪽 구조와 같이 만들고자 했다. Docker-nginx에 certbot을 자동 갱신하는 건 이 레포지토리를 보고 진행했었는데, 머릿속으로 이해가 가지 않기도 하고, 더 쉬운 방법이 존재해서 acme.sh를 사용하기로 했다.

(지금 글을 쓰면서 드는 생각인데, 우리가 겪은 스케일 업 방식보다 훨씬 더 간단한 방법이 존재했다. 이 내용은 뒤쪽에 작성하려고 한다. 왜 뱅뱅 돌아갔을까… 🥲)

우리의 계획은 다음과 같았다.

  1. Nginx 인스턴스를 생성한다. acme.sh를 활용해 인증서를 자동 갱신하도록 설정한다.
  2. 기존 운영 서버 인스턴스의 이미지를 생성하고, 이미지를 활용해 T4g.small 인스턴스를 생성한다.
  3. 기존 운영 서버의 nginx에 새로운 서버 인스턴스 로드밸런싱을 적용한다. api.review-me.page는 양쪽으로 통신하게 된다.
  4. 새로운 nginx 인스턴스도 nginx.review-me.page로 들어오는 트래픽을 기존 운영 서버와 새로운 운영 서버로 나누어 로드밸런싱을 적용한다.
  5. DNS 설정을 변경한다. api.review-me.page의 IP를 새로운 nginx 인스턴스를 바라보도록 수정한다.
  6. DNS가 변경된 것을 확인한 뒤, nginx 설정에서 로드밸런싱 설정을 제거한다.
  7. 기존 T4g.micro 운영 서버 인스턴스를 중단한다.

🥹 서브넷을 잘못 설정했어요

다 잘 마무리되었는데, 한 가지 걸리는 게 있었다. 이제는 모든 트래픽이 nginx를 통해서 왔다갔다하니 EC2 인스턴스가 Public subnet에 존재할 이유가 없었다. nginx를 통해 내부망으로 통신할 수 있으니 외부로 IP를 노출할 필요가 없었다. 하지만 AWS에서는 생성한 인스턴스의 서브넷을 변경할 수 없었기에… 아래와 같은 과정을 통해 Private subnet으로 옮기게 되었다.

  1. 새로 만든 T4g.small 인스턴스의 이미지를 생성한다. (기존 micro 복제본에서 nginx 등의 설정을 제거해 그대로 사용하면 되었다)
  2. 동일한 인스턴스를 Private subnet에 생성한다.
  3. Nginx가 Public/Private subnet에 존재하는 두 서버를 로드밸런싱하도록 설정한다.
  4. 정상 작동을 확인하고 Public subnet 로드밸런싱 설정을 삭제, 인스턴스를 중단한다.

이 친구도 잘 옮겨졌다. Public subnet에 IP를 할당할 필요가 없다고 느껴서 할당하지 않았는데, 인터넷에 연결되지 않는 당연하지만 당황스러운 (ㅋㅋ) 일이 일어났었다. Private subnet에는 Public subnet에 존재하는 NAT을 제공하고 있었기에, 추가적인 IP 할당 없이도 외부 통신이 가능했다.

👨🏻‍🔧 더 쉽고 안전한 방법

글을 작성하면서, 로드밸런싱이 필요없겠다는 생각이 들었다. 처음에 로드밸런싱을 하려고 했던 이유는 새로 올라가는 운영 서버가 잘 돌아가는지를 확인하기 위함이었기 때문이었는데, ‘잘 돌아가는지 확인’을 실제 운영 nginx에 올려 확인하는 것은 안일한 생각이었다… 단순히 새로운 nginx와 운영 서버를 연결해 테스트하고, DNS 설정만 변경하면 될 일이었다.

결국 위에서 적었던 단계들을 아래와 같이 개선할 수 있다.

  1. Nginx 인스턴스를 생성한다. acme.sh를 활용해 인증서를 자동 갱신하도록 설정한다.
  2. 기존 운영 서버 인스턴스의 이미지를 생성하고, 이미지를 활용해 T4g.small 인스턴스를 생성한다.
  3. 기존 운영 서버의 nginx에 새로운 서버 인스턴스 로드밸런싱을 적용한다. api.review-me.page는 양쪽으로 통신하게 된다.
  4. 새로운 nginx 인스턴스도 nginx.review-me.page로 들어오는 트래픽을 기존 운영 서버와 새로운 운영 서버로 나누어 로드밸런싱을 적용한다. 새로운 서버 인스턴스를 바라보도록 한다. 이 과정에서 새 nginx를 통한 테스트가 가능하다.
  5. DNS 설정을 변경한다. api.review-me.page의 IP를 새로운 nginx 인스턴스를 바라보도록 수정한다.
  6. DNS가 변경된 것을 확인한 뒤, nginx 설정에서 로드밸런싱 설정을 제거한다.
  7. 기존 T4g.micro 운영 서버 인스턴스를 중단한다.

추가로, 서브넷을 옮길 때에도 로드밸런싱을 사용하지 않고 upstream 주소만 변경하면 되었다고 생각한다. ‘잘 동작하는지 테스트’가 목적이었다면, 이를 nginx에 붙이면 안 됐다… 🥴

이런저런 방법을 찾아내는 건 참 재미있다. 문제는 이게 그때 떠올랐어야하는데.. 라는 아쉬움이 크다. 다음에는 이런 방법도 알아냈으니 다양한 선택지에서 고르는 훈련을 할 수 있겠지!

기존 T4g.micro Swap 적용 전 🔥
T4g.micro Swap 적용 후
T4g.small Swap 적용 후 😄

Categories