빌드 서버가 처음으로 터진 날

junto·2024년 6월 2일
0

docker

목록 보기
2/2
post-thumbnail

TLDR

  • docker build cache, gitlab-runner cache로 인해 빌드 서버 용량 15GB를 차지하고 있었다. 서버 가용 공간이 없어서 빌드가 계속 실패하는 문제가 발생했다.
  • 해당 빌드 서버를 백엔드 팀과 프론트 팀에서 사용하고 있고, 개인 프로젝트에서도 사용하고 있었다. 빌드 서버를 사용한지 2주만에 벌어진 일이다.
  • 팀원과 거의 동시에 MR을 하게 되었고, 팀원 변경 사항을 반영하고 다시 MR을 하려고 대기 중인 파이프라인을 중단시켰다. 그런데 이때부터 빌드가 안 된 것이다. 수동으로 파이프라인을 종료하면서 설정이 꼬였다고 생각했는데, 애석하게도 이것과는 상관이 없었다.

에러 로그

* What went wrong:
Could not add entry ':bootJar' to cache executionHistory.bin (/home/gitlab-runner/builds/64Gb_ydb/0/cloud_track/class_02/web_project3/team06/backend/.gradle/8.7/executionHistory/executionHistory.bin).
> java.io.IOException: No space left on device
  • 사실, 이때까지도 서버의 공간이 부족하리라는 생각은 절대 못 했다! 굵직한 라이브러리 몇 개와 서버 스왑 영역을 제외하고도 대략 15기가 이상의 여유는 있었기 때문이다.
  • 다시 파이프라인을 동작하니 아래와 같은 에러가 출력되었다. 예상치 않게 작업이 중단된 러너가 해당 프로세스를 잡고 있어서 생긴 문제였다.
Gradle could not start your build.
> Cannot create service of type TaskExecuter using method ProjectExecutionServices.createTaskExecuter() as there is a problem with parameter #9 of type ReservedFileSystemLocationRegistry.
   > Cannot create service of type ReservedFileSystemLocationRegistry using method ProjectExecutionServices.createReservedFileLocationRegistry() as there is a problem with parameter #1 of type List<ReservedFileSystemLocation>.
      > Could not create service of type ExecutionHistoryStore using ExecutionGradleServices.createExecutionHistoryStore().
         > Cannot lock execution history cache (/home/gitlab-runner/builds/64Gb_ydb/0/cloud_track/class_02/web_project3/team06/backend/.gradle/8.7/executionHistory) as it has already been locked by this process.
  • 해당 gradle 프로세스를 찾아 강제로 종료시키면, 다시 공간이 없다는 문제가 나온다.
ps -aux | grep gradle
kill -SIGKILL gradle
  • 서버의 사용 공간을 조회하니 사용 공간이 없다?! 다 어디 갔지,,
~~ubuntu~~@ip-172-31-35-138:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root        29G   29G   0  100% /
  • 어디에서 용량을 많이 쓰는지 검색해 본 결과, docker build cache 용량이 상당하다는 글을 보고 내 빌드 캐시를 찾아보기로 했다.
docker system df
  • docker 빌드 캐시가 13GB 정도 쌓여있었다..

도커 빌드 캐시란 무엇인가?

  • 도커 빌드 캐시란 도커가 여러 번 빌드 할 때 빌드가 빠르게 실행되도록 도와준다. 이를 이해하기 위해선 먼저 도커 레이어를 이해해야 한다.
  • 도커 이미지는 여러 개의 레이어로 구성된다. 레이어는 불변이며 변경되면 새로운 레이어가 생성된다. 각 레이어는 해시값으로 식별된다.
    • 아래 그림을 보면 Dockerfile의 각 줄에 작성한 명령어가 레이어로 구성되며, main.c의 내용이 수정되면 기존에 레이어 캐시가 무효가 되고, 새로운 레이어 캐시를 만들게 된다.

  • Dockerfile의 내용으로 이미지 레이어가 어떻게 구성되어 있는지 알 수 있다. 예를 들어 아래 Dockerfile을 살펴보자.
FROM gradle:jdk17 as builder

WORKDIR /build

COPY . /build

RUN gradle build --exclude-task test

FROM openjdk:17-slim

WORKDIR /app

COPY --from=builder /build/build/libs/*-SNAPSHOT.jar app.jar

EXPOSE 8080

USER nobody

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "app.jar"] 
  • 위에서부터 명령어에 따라 도커 이미지 레이어가 형성된다.
  • 도커 빌드를 하고, 또 빌드하게 되면 기존의 이미지 레이어 캐시(즉, 빌드 캐시)를 사용하기 때문에 굉장히 빠르게 되는 걸 볼 수 있다. (초기 빌드 대략 2분, 두 번째는 0.1초)

  • Dockerfile 명령어를 위에서부터 레이어를 만들어 차곡차곡 쌓는다고 볼 수 있다. 변경이 되지 않으면 만들어진 레이어를 사용한다. 그럼, 위의 명령어를 수정하면 어떻게 될까? 즉, 캐시할 수 있는 부분이 없게 된다. 아래와 같이 수정해 보고 빌드 해보자.
FROM gradle:jdk17 as builder

WORKDIR /build3

COPY . /build3

...

COPY --from=builder /build3/build/libs/*-SNAPSHOT.jar app.jar

...
  • WORKDIR을 /build3 이름으로 바꾼것 뿐인데, 빌드를 해보면 2분 걸리는 걸 알 수 있다.
  • 그럼 /build로 수정하고 빌드해보자. 0.1초가 걸리고 다시 /build3으로 수정해도 0.1초가 걸리는 것을 알 수 있다. 즉 도커는 이미지를 한 번 만들면 이를 재사용할 수 있도록 도커 빌드 캐시에 저장한다.

테스트를 통해 새로 알게된 사실

  1. 도커 파일을 작성할 때 변경이 잦은 부분과 그렇지 않은 부분을 나누어 캐시할 수 있는 레이어를 많이 확보하는 게 중요하다. 물론 이미지 크기와 레이어는 적을수록 좋다.
  2. 도커를 빌드하고 나면 Image와 Build Cache가 쌓인다. 위의 예시에선 이미지 크기가 485MB였는데 첫 번째로 빌드할때는 485MB가 디스크에 추가되고, 두 번째로 빌드했을 때 Disk에 추가 된 건 대략 80MB이다. 왜 이런 차이가 발생할까? 기존 레이어를 재활용하기 때문이다. 기반 이미지를 별도의 컨테이너마다 각자 가지고 있는 게 아니라 공통된 부분을 공유한다.
    • 도커 이미지 레이어 캐시와는 별도로 변경이 없다면 같은 레이어 해시값을 가진다. 따라서 빌드 결과물은 공유해서 디스크 공간은 절약하지만, 중간에 빌드 과정을 기록하는 빌드 캐시는 각각 별도로 유지하는 걸 볼 수 있다. 즉 레이어를 얼마나 재활용할 수 있게 설계하느냐에 따라 추가되는 빌드 캐시 공간이 달라진다.

  1. build를 할 때 —-no -cache 옵션을 주어 캐시를 사용하지 않을 수 있다. 이 말을 build cache를 만들지 않는다고 오해할 수 있는데, 도커가 빌드할 때 기존 캐시된 레이어를 사용하지 않겠다는 것으로 빌드 시간이 더 오래 걸린다. 해당 옵션을 주고 진행해도 build cache가 추가되는 걸 확인할 수 있다.
    • 어떤 이미지의 최신 변경 사항을 확인하려고 할 때--no -cache 옵션이 필요할 수 있다. 캐시된 레이어는 최신 변경 사항을 반영한 상태가 아니기 때문이다.
  2. 위의 Dockerfile에서 아래에 위치한 EXPOSE를 8080에서 9090으로 수정해보자. 그렇다면 그 위에 있는 이미지 레이어를 재활용해야 한다고 생각할 수 있다. 하지만, 실제로 해보면 그렇게 동작하지 않는다. 스크립트 시작 부분에 . /build 로 파일을 복사할 때 변경된 Dockerfile이 포함되고, 이에 따라 초기 이미지 레이어를 캐시할 수 없기 때문이다.
    • 공식 문서를 보면, 아래 사진 처럼 파일을 모두 복사하지 말고 필요한 부분만 복사하여 레이어 캐시를 활용하라고 한다. 위의 예시처럼 2단계 멀티스테이징을 적용할 경우 2단계에서 부분적으로 호스트 파일(ex: 도커파일)을 가져올 수 없기 때문에 레이어 캐시를 재활용할 수 없다.

그래서?! crontab

  • 매일 새벽 5시에 빌드 캐시를 삭제하도록 한다. 하루에 빌드를 30번 정도하면 주기를 더 짧게해야 겠지만, 지금은 이정도로 충분해보인다.
crontab -e
0 5 * * * /usr/bin/docker builder prune -f

  • 새벽 5시에 확인해 봤더니, 크론이 돌지 않았다. 서버 시간대를 확인해 보니 기본 설정은 UTC 시간을 쓰고 있다. UTC 시간을 기준으로 크론탭 시간을 변경해도 되지만, 서울 기준으로 시간을 설정해 주었다.

참고 자료

profile
꾸준하게

0개의 댓글