라이켓은 다양한 문화생활 정보를 공유하고 나만의 문화생활 기록을 남길 수 있는 서비스를 제공하고 있습니다.
태그별, 지역별로 관심있는 정보들만 골라보고 쉽게 문화생활을 즐겨보세요.
안녕하세요. LIKET팀의 백엔드 개발자 민경찬입니다. 배포 과정에서 발생한 EC2 인스턴스 과부하 문제를 분석하고 해결한 경험을 공유해보려고 합니다.
저희는 3개의 서버 컨테이너를 실행해야했기에 T3.small
이라는 인스턴스 유형을 사용하고 있습니다.
서버 운영 중, 총 3번 인스턴스가 멈춘 적이 있습니다.
이 세 번의 멈춤 사건에는 공통점이 존재했습니다.
배포 과정에서 CPU 사용량이 과도해져 문제가 발생한 것으로 보였습니다.
하지만 마지막 멈춤 사건 때, 멈췄던 인스턴스를 재시작하고
docker restart
명령어를 통해 컨테이너를 다시 재시작하는 과정에서 인스턴스가 다시 한 번 멈추게 되었고 컨테이너 실행 과정이 문제라는 것을 더욱 더 확신하게 되었습니다.
배포 싸이클동안 프로덕션 인스턴스에 실행되는 명령어는 다음과 같습니다.
docker pull image
: 배포할 이미지를 가져옴docker run -d image
: 배포할 이미지를 실행함user-server
와 admin-server
, batch-server
를 따로 관리하고 있습니다.
이를 모노레포로 관리하고 있으며 모두 t3.small
인스턴스에 배포하고 있습니다.
이 3개의 서버 모두 Docker
를 통해 배포하고 있으며 Github Action
을 통해 빌드하고 배포하고 있습니다.
따라서 공용 라이브러리가 변경되었을 경우, 3개의 서버 모두 다시 배포가 됩니다.
docker pull
과 docker run
과정이 cpu를 얼마나 사용하는지 분석해보려고 합니다.
실험 과정: 리눅스 컨테이너를 실행하여 컨테이너 내부에서
docker
를 사용하여 이미지를pull
,run
하였습니다. 그 후, 커맨드 실행 과정을cAdvisor
,Prometheus
,Grafana
를 사용하여 분석하였습니다.
우선, user-server
를 기준으로 기존 코드를 docker build하였고 실험할 컨테이너에서 pull
과 run
명령어를 실행하였습니다.
놀랍게도 docker pull
과 docker run
명령어 모두 대단히 높은 cpu 사용률을 보여주고 있습니다.
이미지 용량이 1.81GB
라는 점을 고려했을 때 docker pull
작업이 무거울 수 있다고 생각했습니다. 그런데 docker run
은 아니였죠.
docker run
작업이 1.81GB
도커 이미지를 pull하는 것 보다 더 많은 cpu를 먹을 순 없다! 라고 생각하였고 분석을 해봤습니다.
제가 생각한 docker run
은 그저 빌드한 코드를 실행하는 것에 불과했습니다.
Dockerfile
에도 실행 스테이지에서 npm run start
만을 하고 있었으니까요.
그러나 npm run start
가 문제였습니다.
package.json
"scripts": {
"start": "nest start",
...
npm run start
를 하게 될 경우 nest start
명령어를 실행하게 됩니다.
nest start
는 TypeScript를 JavaScript로 변환하고 번들링하는 과정이 포함되어 있어 상당한 CPU 리소스를 소모합니다. 이런 작업은 배포 전에 미리 수행하는 것이 효율적이겠죠!
실제 nest-cli
코드를 보면 nest start
할 때 사용되는 StartAction
클래스는 BuildAction
를 상속받아 build
액션을 실행하도록 되어있습니다.
export class StartAction extends BuildAction {
public async handle(commandInputs: Input[], commandOptions: Input[]) {
// do something...
await this.runBuild(
commandInputs,
commandOptions,
isWatchEnabled,
isWatchAssetsEnabled,
!!debugFlag,
onSuccess,
);
}
}
이제와서 생각해보면 nest start
에서 Typescript 컴파일 과정이 생략될 수가 없는데 말입니다...
정말 초보적인 실수였습니다😭
nestjs/nest-cli
레포에서 nest-cli에 대한 모든 코드를 확인할 수 있습니다.
레포로 이동하기
그래서 실행 스테이지에서 npm run start
가 아닌 빌드한 파일을 node
로 실행할 수 있도록 Dockerfile
을 수정하였습니다.
변경 후, 59%
의 cpu 사용률은 1.87%
로 감소하였습니다.
결과 정리
nest start
CPU 사용률: 59%
node dist/.../main.js
CPU 사용률: 1.87%
이 정도로 대단히 큰 차이가 발생하다니 놀랐습니다.
Typescript 컴파일과 번들링 작업은 제 생각보다도 더더 무거운 작업인가봅니다.
docker run
명령어는 59%
사용률에서 1.87%
의 사용률로 개선되었지만 여전히 docker pull
은 cpu를 많이 사용하고 있습니다.
도커라는 배포 환경을 포기하지 않는 이상 docker pull
은 필수적이였고 도커 이미지 용량을 최적화하는 방법이 떠올랐습니다.
배포 인프라는 정말 자주 바뀌었습니다. 트래픽을 처리해 줄 컴퓨팅 파워와 팀 주머니 사정이라는 현실 속에서 적절히 타협을 봐야만 했거든요. 다양한 시도를 해봤고 다양한 시도를 하기에 도커라는 도구는 정말 유용했습니다.
그래서 도커를 포기하지는 않았습니다.
두 가지를 변경해보았습니다.
dist
와 node_modules
를 제외한 코드 파일은 제외알파인 리눅스는 경량화된 베이스 이미지로, 기존 리눅스 컨테이너 대비 이미지 크기를 크게 줄일 수 있습니다.
그래서 실행 스테이지에서는 알파인 리눅스와 정말 필요한 js 코드 파일만을 남겨놨습니다.
그 결과, 이미지 용량이 유의미하게 낮아진 것을 확인할 수 있었습니다.
1.81GB
-> 487MB
CPU 사용률도 비교해봐야겠지요.
50.7%
에서 40.0%
에서 조금은 유의미하게 cpu사용률이 낮아졌습니다.
결과 정리
도커 이미지 용량 변화: 1.81GB
-> 487MB
CPU 사용률 변화: 50.7%
-> 40.0%
AWS ECR 비용 중 스토리지 비용은 이미지 용량에 비례합니다. 교차 리전 트래픽 비용을 고려하지 않는다면 이미지 용량이 줄어든 만큼 비용도 비례하여 감소합니다.
유의미한 변화라는 것이죠!
작업 | 기존 CPU 사용률 | 변경 후 CPU 사용률 |
---|---|---|
nest start | 59% | 1.87% |
docker pull | 50.7% | 40.0% |
이번 경험을 통해 컴파일과 빌드 작업은 반드시 배포 전 로컬에서 완료하고, 경량화된 이미지를 사용하는 것이 중요하다는 점을 확인했습니다.
CPU 과부하를 예방하고 비용 절감까지 이루었으니 말이죠!
읽어주셔서 감사드립니다.