개요
정상적으로 동작하던 배포 파이프라인에 난데 없이 마주치게 된 에러 문구
에러의 내용은 현재 VM 머신의 디스크에 공간이 부족하다는 에러였다.
이게 무슨소리지??? 싶어 VM 인스턴스에 접속하여 현재 디스크 상태를 확인했다.
그런데 맙소사 현재 10GB짜리 머신에서 99%가량의 디스크 공간을 Docker가 사용하는 것을 확인할 수 있었다.
이를 개선하는 짧은 기술 챌린지를 가져보고자 한다.
현재
현재 아맞당의 배포 파이프라인은 다음과 같은 구조를 가지고 있다.
Gihub Action
을 통해 운영 브랜치인 main에 코드가 병합되는 순간을 기점으로 파이프라인이 동작- 파이프라인에선 제일 먼저 Ubuntu, Docker, Gradle을 build하는데 필요한 정보들을 환경변수로 받고 이를 바탕으로 배포를 준비
- gradle build를 실행한 후 현재 도커 이미지 저장소로 사용되고 있는 GCP의 Artifact Registry에 이미지 빌드 결과를 Push한다.
위 까지가 일반적인 CI(Continuous Intergartion)과정이다.
- 이후 GCP VM에 SSH로 접속하여 기존의 도커 이미지를 중단, 삭제
- 이후 Docker Image를 Pull 받아 새롭게 애플리케이션을 실행시키는 경량화 파이프라인을 보유하고있다.
여기까지가 CD(Continuous Delivery)를 구현한 내용이다.
여기서 아맞당 서버팀이 겪은 문제는 쌓이는 도커 이미지에 대한 처리 방식이었다.
현재 파이프라인 상 새로운 버전의 애플리케이션이 배포될 때 기존 Docker에 대한 Stop, rm 작업은 해주고 있었다.
하지만 왜 디스크에 메모리가 부족했던 것일까?
Trouble Shooting
1. Swap Memory?
맨 처음 생각한 키워드는 가상 메모리였다.
이런 상황을 AWS 인프라를 활용할 때도 겪었기에 Swap 메모리를 통한 머신의 여유공간을 확보하고자 생각했다.
현재 아맞당의 서버 운영 전략상 Docker를 통해 애플리케이션을 실행시키기에 VM의 디스크와 메모리를 전부 사용하게 된다.
하지만 현재 에러는 no space left on device
즉 디스크 영역의 용량이 부족하다는 뜻이다.
또한 도커가 메모리를 사용하는 영역은 애플리케이션 영역 그러니까, JVM이나 공유 메모리 영역에만 사용하고 현재 문제가 되는 부분은 이미지 레이어가 저장되는 정적 리소스 위치인 /var/lib/docker/overlay2
이 경로에 이미지가 VM의 디스크 크기보다 물리적으로 많이 저장되어 발생한 문제였기에 이는 해결책이 아니었다.
2. Prune
Docker 관련작업하여 Prune을 빼놓고 얘기할 순 없을것이다.
Prune
은 컨테이너 뿐만 아니라, 이미지, 볼륨, 네트워크, 캐시 등의 도커와 관련된 메타 데이터를 정리한다.
🤔 아 그럼 그냥 스크립트에 prune을 추가하면 되겠네
“과연 옳바른 설계일까?”라는 생각을 가장 먼저해야한다.
매 배포 때마다 Docker 메타데이터를 쓸어버리는 건 증상 가리기용 임시방편에 가깝다. 성능도 깎고, 불안정성만 키운다.
매 스크립트 마다 실행시 발생하는 Side Effect
- 성능 저하: 매번 이미지/캐시를 지우면 다음 배포에서 다시 받아야 해서 pull 시간, 네트워크 IO, 디스크 IO가 그대로 비용으로 돌아온다.
- 오버헤드: 불필요한 데이터를 지울 시 롤백 비용 발생
- 근본 해결 X: 로그 폭주, 과도한 보존, 비대한 이미지 같은 근본 원인은 그대로고 매 배포마다 임시 방편처리하는 꼴
😡 그래서 뭐해야하는데?
자 우선 도커에서 비용(cost)이라 칭할 애들을 찾아보자
나는 VM의 디스크를 과도하게 차지하는 애들을 기준으로 개선작업을 수행하고자 한다.
1. 로그 롤링
우선 도커의 로그 부분이다.
밑의 사진은 docker image prune 을 수행한 직후 애플리케이션을 새롭게 띄운후에 로그 크기를 측정한 커맨드이다. 직후에 실행했음에도 3MB나 차지하는 모습이 보인다.
도커 로그를 통해서 애플리케이션의 실행시 발생한 에러 파악이나 서버의 HealthCheck용으로 많이 사용되기에 필요한 데이터이다.(버릴 수는 없다)
그렇기 떄문에 만약에 LTS(프로덕션 레벨)이 릴리스 된 상태라면 로그는 계속해서 쌓이게 되는 것이다.
그렇기 때문에 로그 롤링은 반드시 필요한 작업이다.
그렇기에 아맞당 서버의 특성상 로그가 많은 서비스는 아니고 분산 서버가 고려되는 서비스는 아니기에 아래 계산식을 활용하여 로그의 최대 저장시간을 구했다.
보존시간(분) ≈ (max-size × max-file) / (평균 로그 속도(MB/분))
이를 구하기 위해서 로그의 발생속도 또한 측정하였다. 1분을 기준으로 측정한다면 정확한? 수치를 뽑을 수 있을거라고 기대했기에 1분을 기준으로 설정하였다.
LOG="/var/lib/docker/containers/7e510cdf7715fc7e41efa9095c1432e270b008f56dbaa14eb3f4e8622534a920/7e510cdf7715fc7e41efa9095c1432e270b008f56dbaa14eb3f4e8622534a920-json.log"
if ! sudo test -f "$LOG"; then
echo "로그 파일을 없음 !!!!!: $LOG"
exit 1
fi
S1=$(sudo stat -c%s "$LOG" 2>/dev/null || echo 0)
sleep 60
S2=$(sudo stat -c%s "$LOG" 2>/dev/null || echo 0)
if [ "$S2" -ge "$S1" ]; then
BYTES=$((S2 - S1))
else
BYTES=$S2
fi
awk -v b="$BYTES" 'BEGIN { printf("로그 증가 속도: %.1f MB/분\n", b/1024/1024) }'
해당 작업을 통해 우선 100MB 정도의 롤링 사이즈를 지정하게되었다. (최대 100분간 로그보존)
2. 조건부 Prune
결국 이미지나 도커에 관한 데이터를 정리하는 것은 필수적인 작업이다.
그중에서 가장 간편하게 커맨드로서 제공되는 Prune을 사용할 것이다.
그렇다면 이를 일정한 조건을 기준으로 실행시킨다면 기존의 파이프라인에서 Disk 부족문제와 성능저하 문제 또한 잡을 수 있을 것이다.
그렇다면 기준은?
현재 아맞당 서버가 운용되고있는 GCP의 VM은 10GB이고 SpringBoot 도커 이미지 크기는 대략 6~700MB이다.
그렇다면 여유있게 배포 1회정도 여유를 가질 수 있도록 Disk 사용량이 85%이상이라면 Prune을 실행시키도록 구현하였다.
조건부 prune
if [ "$USED" -ge 85 ]; then
echo "Disk >= 85% → light prune (images/builder cache)"
docker image prune -af --filter "until=168h" || true
docker builder prune -af || true
fi
weekly 주기 prune
weekly-prune:
name: Weekly prune on Wed 03:00 KST
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: SSH & Prune
uses: google-github-actions/ssh-compute@v1
with:
project_id: ${{env.PROJECT_ID}}
zone: ${{ env.VM_ZONE }}
instance_name: ${{ env.VM_NAME }}
user: ubuntu
ssh_private_key: ${{ secrets.GCP_VM_SSH_PRIVATE_KEY }}
command: |
set -euo pipefail
echo "Weekly prune start..."
docker image prune -af --filter "until=168h" || true
docker builder prune -af || true
sudo journalctl --vacuum-time=7d || true
echo "Weekly prune done."
결론
현재 아맞당의 프로덕트는 단일 서버 기준으로 운영될 것을 고려하여 로그 롤링과, 디스크 85% 조건부 정리를 넣으면서 배포 파이프라인이 훨씬 건강해졌다.
세줄 요약
- 로그는 필요한 데이터다 👉 측정 후(100분 보존) 정책으로 관리
- 디스크 정리는 필요하다 👉 상황(85%)에서만 가볍게 실행
- 무지성 청소는 성능·롤백 비용만 키운다 👉 보존/제한/조건부로 운영
이렇게 배포 파이프라인 구성경험을 통해 디테일한 정책을 통해 사용자 중심의 서비스를 계속해서 운영해나가고자한다.
결국 프로덕트는 사용자가 사용하고 사용자의 피드백이 개발자의 결실이 된다. 그러니 사용자 불편함을 최소화하는 서비스를 창출하기 위해 노력해나가보자