[AWS] 서버가 갑자기 다운되는 문제 (CPU 점유율 100%, Memeory 부족)
2024.11.13- -
해당 문제는 AWS EC2에 올려 운영 배포 중이던 서버가 중간중간 죽는 문제를 겪게 되면서 이를 해결하기 위해 시도했던 방식들을 바탕으로 글을 작성해보았습니다. 아직도 정확한 원인은 파악하지 못해 계속해서 찾아보고 있는 중이며, 관련된 내용에 대해 의심이 가는 부분이 있다면 댓글로 남겨주시면 감사하겠습니다 :)
1. 문제 상황
AWS EC2에 클라우드 환경을 구축하여 NestJS, NextJS, MySQL, Nginx를 Docker를 통해 컨테이너로 띄워 서비스를 운영을 하던 와중이었습니다.
실제 사용자를 받아 운영 중이던 서비스였고 사용자를 정식으로 받기 전부터 3명 정도가 미리 사용해보면서 문제가 되는 부분을 수정하는 느낌으로 미리 QA도 진행하여 성공적으로 서비스 런칭을 하게 되었습니다.
서버 스펙은 다음과 같았습니다.
- t3.medium
- disk: 20GB
- RAM: 4GB
- vCPU2(2코어)
저도 실제 사용자를 받아보는 상황은 처음이었기에 이러한 스펙의 서버로 운영이 가능할지 가늠이 되지 않았지만 얼추 테스트해 보았을 때는 적어도 CPU나 disk에서 문제가 될 일은 없어 보였습니다.
클라이언트 요청 사항 중 1년에 서버 비용이 50만원이 넘지 않았으면 좋겠다라는 말이 있었기 때문에 그 선에서 제일 쓸만한 서버가 t3.medium이기도 했었습니다.
등록된 사용자는 약 600명 정도 였으며, 일일 활성 사용자 수는 평균 100명 정도였습니다. 서버를 사용자들에게 오픈하고 몇 주 동안은 별 문제 없이 흘러가다가 어느 순간부터 갑자기 서버가 중간에 죽는다는 연락이 오기 시작하였습니다...
배포 및 운영 자동화가 구축되어 있지 않았던 상황이라 그런 상황이 올 때마다 직접 ssh로 서버에 접속하여 docker 컨테이너들을 재시작하거나 ec2 서버 자체를 리부팅 해주었습니다.
그러다가 정말 심할 때는 서비스에 접속이 안되는 것도 안되는 건데 심지어 ssh 접속 조차도 되지 않을 때가 있어 빨리 문제의 원인을 찾아야 했습니다. 서버를 다시 껐다가 켜도 임시적으로 해결이 된 것일뿐 또 수 시간 내지 수 십분이 지나게 되면 다시 또 서버가 죽는 현상이 반복되었습니다.
2. 서버가 죽었던 이유
EC2 콘솔을 통해 확인해 본 결과 그 전까지 괜찮던 CPU 사용률에서 어느 순간 100%를 찌르더니 서버가 완전히 불완전해진 것인데요. 저 순간마다 서버가 다운되기를 반복했던 것이었습니다.
서버가 죽었다는 연락을 받으면서 같이 들려왔던 몇 가지 말들이 있었는데 '강의를 듣는 페이지'가 안 들어가진다와 '관리자 퀴즈 관리 페이지'가 너무 느리다라는 것이었습니다. 실제로 제가 들어가서 확인해보니 강의를 듣는 페이지를 들어가기만 하면 서버가 죽거나 죽기전 발버둥을 치는 것인지 엄청 느려지는 것을 확인할 수 있었습니다. 퀴즈 관리 페이지도 마찬가지로 API를 호출하는 데 굉장히 오래 걸리고 여기서도 운이 좋지 않으면 또 서버가 내려가는 상황이 발생하였습니다.(개복치...?)
그러던 중 터미널 명령어 중에서 리소스를 실시간 모니터링할 수 있는 'htop'이라는 명령어를 알게 되어 이를 확인해 보았습니다.
CPU 사용량이 평상시에도 조금 이상한 행태를 보였는데 위에서 언급한 두 페이지를 들어가보면서 확인해본 결과 위 사진과 같이 CPU 점유율이 거의 한계치에 다다르는 모습을 볼 수 있었습니다. 이는 역시나 API 서버를 의미하는 커맨드인 'node dist/main'에서 갑자기 CPU 사용량이 126% 이상으로 오르기 시작한 것입니다.
3. 예상되는 문제 - 코드 레벨
우선 서버가 내려가면 클라이언트 측에서 압박이 올 수 있었기 때문에 docker compose 파일에서 띄워질 컨테이너들에 대해 리소스 자원에 limit을 걸어 구동시켜 임시방편으로 서버가 죽을만큼의 리소스 사용량이 되더라도 다시 살아나도록 처리해 놓았습니다.
하지만 이는 임시방편일 뿐 되도록 빨리 문제를 해결해야 했는데요.
가장 먼저 생각이 드는 문제는 API가 너무 성능이 좋지 않아 특정 API를 호출할 때마다 병목현상이 발생하여 서버가 버티지 못했던 것이 아닌가라는 생각에 코드 레벨에서 수정을 해보려고 했습니다.
하지만 현 시점에서 생각해 보았을 때 이 문제도 물론 영향이 있긴 했겠지만 아무리 그래도 사용자 1명의 요청인데 t3.medium으로 되지 않는다고?라는 생각이 계속 들었지만 당장에 할 수 있는 게 이것밖에는 없었기 때문에 API 성능을 조금 개선해보기로 했습니다.
일단 문제가 되는 API를 다음과 같이 리스트업 해보았습니다.
- 강의 듣기에서 요청되는 api
- 퀴즈 관리에서 요청되는 api
퀴즈 관리에서의 문제
클라이언트 요구 사항 중에는 사용자들이 제출한 퀴즈를 볼 수 있고 채점할 수 있는 기능이 있었고 해당 부분에는 위와 같이 정렬 및 이름 검색 기능도 포함이 되어야 했습니다. 저는 tanstack-table(현 react-table) 라이브러리에서 데이터를 테이블 형식으로 보여주면서도 정렬과 검색기능을 포함하는 기능이 있어 해당 라이브러리를 사용했었는데 이 라이브러리에는 client-side 데이터와 server-side 데이터에 따라 테이블 구현 방식이 완전히 달라지게 됩니다.
client-side 방식은 먼저 서버로부터 관련 데이터 전부를 가져오면 이를 react-table에서 알아서 페이지네이션과 정렬, 필터링을 알아서 해주게 됩니다. 그래서 페이지를 넘기거나 필터링을 바꿀 때마다 API 요청이 호출되는 것이 아니기도 하고 그렇기 때문에 화면이 부드럽게 이어지게 됩니다.
반면, server-side 방식은 테이블에서 보여줄만큼의 데이터를 계속해서 요청해서 사용하는 방식으로 이 경우에 페이지네이션, 정렬, 필터링 모두를 server의 요청과 연동을 해주어야 합니다.
얼핏보면 client-side가 굉장히 편해보이는데, 데이터가 굉장히 많아졌을 경우에 초기 데이터를 불러오는 단계에서 굉장히 오랜 시간을 기다려야 될 수 있다는 문제가 있게 됩니다. 저도 마찬가지의 이유로 초기에 client-side로 구현을 진행해 react-table의 기능을 잘 사용해오다가 데이터가 많아지는 순간부터 이 부분에 병목이 생겨 API 호출이 느려지게 되었던 것으로 판단이 됩니다.
조금만 생각해보면 위와 같이 한 페이지 당 10개씩 134페이지가 있다는 것은 총 1340개의 데이터를 불러온다는 것인데 보기만 해도 굉장히 비효율적이라는 것을 알 수 있습니다.
강의 듣기 페이지에서의 문제
그럼 이번엔 강의 듣기 API를 확인해볼까요?
제가 구성했던 mysql 테이블들에는 인덱스를 걸어주지 않았던 상태였습니다.
강의 듣기에서 요청되는 API에서는 아래 사진과 같이 엄청난 relations가 있는 것을 볼 수 있는데 여기서 quizSubmits는 바로 앞에서 봤던 것처럼 데이터가 1300개 이상이 존재합니다.
이는 곧 저 테이블들 간의 join 연산이 이루어진다는 것을 의미하기도 하는데요. 위 연산이 join이 되었을 경우 탐색해야 될 경우를 단순 계산해보게 되면
- course x lectures x l.q x l.q.qA x l.q.qS x category
- 5 x 60 x 108 x 265 x 1782 = 15,300,252,000(153억)
최악의 경우 약 153억번의 연산이 이루어지게 되는 것으로 예상이 됩니다....
이를 해결하려면 join을 풀고 각각의 쿼리를 날리는 식으로 서비스 로직을 바꾸는 것을 생각해볼 수 있을 것 같습니다.
4. 로컬에서 테스트
운영 단계에서 문제가 생겼던 것을 로컬에서 확인해 보고 싶었는데 컴퓨터 사양이 너무 좋아서 그런지 똑같이 재현해 내기는 어려움이 있었습니다.
우선 최대한 환경을 맞추기 위해 운영 DB를 그대로 복제하여 로컬에 가져와 개발을 진행하였고 API의 성능을 측정해본 결과 아래와 같이 나오게 되었습니다.
- 가벼운 API
- 5ms / 5.88KB
- 강의 듣기에서 요청되는 api
- /courses/myLectures/${courseId} - 300ms / 67.48KB
- /courses/${courseId}/lectures/progress - 554ms / 1.5KB
- /courses/lectures/${lectureId} - 80ms / 422B
- 퀴즈 관리에서 요청되는 api
- /admin/quizzes - 284ms / 424.46KB
가장 오래 걸린 API에 대한 코드를 본 결과 역시 레코드가 많은 테이블들이 포함되어 join 연산이 부대끼고 있는 것을 볼 수 있었습니다.
맥북은 성능이 좋은 편인데도 이렇게 쓰레기 같이 코드를 작성하면 500ms의 응답속도를 보일 수 있다는 것을 깨닫게 되었습니다...
위에서 relations로 엮은 테이블은 각각 14,000개, 1300개 가량의 레코드가 존재하는 테이블들 입니다.
그래서 우선 join 연산을 최소화하였고 서비스 로직에서 쿼리들을 분리하여 호출하도록 수정해서 어느 정도 성능을 꽤 많이 개선할 수 있었습니다.
- 300~500ms 정도 걸리던 시간이 30~50ms 까지 줄어들었습니다.
물론 쿼리를 이렇게 막 분리하면 또 다른 문제가 있는지는 찾아봐야 할 것 같고, 레코드가 많은 테이블에 조만간 인덱스도 걸어주어야 할 것 같습니다.
5. 궁금한 점
그런데 사실 그 전부터도 데이터가 많기는 했던지라 갑지가 왜 어느 순간부터 CPU가 저렇게 튀기 시작한 건지 이해가 되지 않았습니다.
이렇게 성능을 개선하고 서버를 재부팅한 다음 서버를 다시 띄워서 테스트를 해보았는데 별 문제 없이 동작하는 듯 하다가 갑자기 어느 순간에 다시 CPU 사용량이 이번에는 50%를 찍더니 그대로 유지하는 모습을 보여주었습니다.
중간 중간 0% 가깝게 떨어졌던 부분은 서버를 리부트 시켜 그런 것이었는데 다시금 40% 이상으로 사용되고 있는 것을 볼 수 있습니다.
이는 docker 컨테이너에 리소스 limit을 걸어서 100%는 찍지 않은 것 같았지만 여전히 문제가 남아있다는 것을 의미한다고 생각해 htop으로 다시 한 번 리소스 모니터링을 확인해보았습니다.
여기서부터 정말 신기한 일들이 발생하기 시작합니다.
위 사진을 보면 0번의 코어에서 100%를 잡고 있는 것을 볼 수 있습니다. 그러나 아래 어떤 프로세스가 해당 CPU를 사용하고 있는지를 확인하고 싶었지만 제일 많이 사용하는 것이 0.7%로 나오는 것을 볼 수 있습니다.
그래서 docker 컨테이너 자체를 일단 다 내려 모든 애플리케이션을 전부 다운시킨 다음 ec2 서버를 재부팅 시켜서 CPU 점유율이 서버가 켜지는 순간부터 4~50%가 되는 것인지 확인해보려고 했습니다.
그 결과 서버를 딱 다시 켰을 때는 예상대로 0% 가깝게 유지되는 것을 볼 수 있었습니다. 그러나 놀랍게도 특정 순간에 다시 40%로 올라가게 되는 것을 확인할 수 있습니다.
위 사진에서 보면 알 수 있듯이 재부팅하고 나서는 해당 클라우드에 돌아가고 있는 서버라든가 서비스는 전혀 없는 상태입니다. 그렇기에 당연히 CPU 점유율이 1% 남짓을 기웃거리다가 갑자기 새벽 4시부터 저렇게 다시 또 40%로 튀더니 내려갈 생각도 없이 그대로 유지가 되는 것을 보고는 정말 경악을 금치 못했습니다.
해당 문제를 같이 도와주던 분도 도저히 이유를 모르겠다고 하여 현업 개발자분께 질문을 드렸지만 그 분도 결국 신기해 하시곤 원인을 찾지는 못했습니다.
또 의미있게 볼만한 지표 중에 하나로 네트워크 입력을 보면 해당 시간대에 0.5MB 정도의 네트워크 입력이 들어온 것으로 보이지만 이것과 관련이 있는지는 모르겠습니다.
디도스를 의심해보고 싶기도 했지만 서비스가 띄워지지도 않았는데 어디로 어떻게 요청을 할 수 있을까 해서 그럴 확률은 적다고 생각하여 배제하였습니다.
6. 그럼 뭐가 문제지...
sysstat이라는 패키지의 perf top이라는 명령어를 알게되어 확인해봤는데 kernel에서 46%를 사용하고 있다고 나오는 것을 볼 수 있었습니다.
이걸 처음에 딱 보자마자 이 문제였구나...! 라고 생각했지만 문제가 없는 현 시점에서도 저 명령어를 똑같이 실행해보면 저렇게 kernel의 점유율이 굉장히 높게 나오는 건 똑같기 때문에 htop에서 실제 사용률과는 관계가 없다는 것을 확인하였습니다.
그러다가 서버를 재부팅해봤을 때 원래는 메모리가 2GB를 넘어섰었는데 갑자기 200MB 대로 들어와 편안한 상태를 보여주는 것을 보고선 정말 더 미궁으로 빠지는 것 같았습니다.
그래서 우선 원인을 알지 못한채 그냥 서버 자체가 이건 저주를 받았다라고 생각하고 ec2 인스턴스 자체를 새로 생성해서 좀 더 보안에 신경을 써서 구축하기로 했습니다.
7. 지금 상황
현재 도메인도 새로 구입하였으며 SSL 인증서도 서버 내에서 발급받도록 한 다음 https를 적용하여 서비스를 다시 오픈한 상태입니다.
오픈 이후 CPU가 비정상적으로 올라가는 문제는 더 이상 보이지 않았으며 심지어는 docker build 할 때를 제외하고 5%를 넘어가는 경우도 없었습니다.
글을 쓰는 현재까지도 마음을 졸이며 수시로 확인하고 있습니다.
8. 뭐가 문제였을까?
우선 관련 내용을 찾아보거나 주변 개발자 분들께 물어봤을 때 가장 얘기가 많이 나왔던 것은 메모리 사용 과다로 인한 문제였습니다.
이는 메모리 스왑 영역을 생성함으로써 해결하면 서버가 죽는 문제는 생기지 않겠지만 결국 그것도 디스크를 메모리 대신 사용하는 것이기 때문에 성능 측면에서 문제가 생길 여지가 있을 수 있습니다.
또한 이러한 문제를 겪었던 사례를 보면 EC2 프리티어 인스턴스를 사용하시는 분들이 대부분이었는데 그 경우에 1GB의 메모리가 문제가 되기에는 충분한 이유가 될 수 있지만 저의 경우 4GB인데 그렇게까지 메모리를 차지할 일이 있었나 싶습니다.
이와 관련하여 스왑 메모리 영역 확보도 분명 문제 해결책 중에 하나가 되기 때문에 관련하여 보고 싶으신 분들은 아래 링크를 참고해주시면 됩니다.
보통 대부분 이 문제를 겪었던 분들이 입을 모아 메모리를 말하는 것과 제 상황도 메모리가 꽉차있는 경우가 있는 것을 미루어 봤을 때 메모리 문제가 우선 가장 가능성있는 원인인 것 같습니다.
다만 이 메모리가 부족한 것이 제가 띄운 서비스에 비해 메모리 용량(4GB)이 너무 낮아서 그런 것인지 아님 다른 문제가 있었는지는 모르겠지만 이러한 문제가 생겼던 시기를 미루어봤을 때 jenkins와 연관이 있을 수도 있을 것이라는 생각이 듭니다. 이전 서버에서부터 CI/CD를 하기 위해 jenkins를 도입하려고 했었는데 서버가 자꾸 이상해져 적용하지 못해 jenkins 서비스 docker를 내린 채로 두었었습니다.
현재 서비스에서는 jenkins로 CI/CD를 하고 있는 상태인데 docker에 띄워진 jenkins 서비스를 보면 메모리를 굉장히 많이 잡아먹고 있는 것을 볼 수 있는데 그 전 서버에서도 아마 이것 때문에 그러한 문제가 생긴 것이 아닐까 하는 생각이 드는 것이 현재까지의 결론입니다.
관련된 경험이 있으시거나 예상되는 원인이 있으시다면 댓글로 달아주시면 감사하겠습니다...!
'CS 지식' 카테고리의 다른 글
[CS 지식] GraphQL이 뜨는 이유(feat. RESTful API 와 다른 점은?) (6) | 2024.11.07 |
---|---|
JWT는 안전할까? (1) | 2024.10.05 |
[CS 지식] 컴퓨터 공학 필수 지식 A부터 Z까지 (0) | 2023.04.14 |
소중한 공감 감사합니다