RSS듀오랩스
웹 인프라

docker builder prune 상한: 왜 남의 캐시까지 깎일까?

작성자
듀오랩스 대표·6분 읽기

한 서비스의 개발 배포를 다른 호스트로 옮기는 중이었습니다. 러너를 새로 붙이고, 환경변수 출처를 바꾸고, 이제 워크플로를 켜기만 하면 되는 참이었습니다. 그런데 배포 스크립트 마지막에 있는 한 줄에서 멈췄습니다.

docker builder prune -f --max-used-space 20GB

이 줄은 2년 가까이 아무 문제 없이 돌던 것입니다. 빌드 캐시가 무한히 불어나 디스크를 채운 사고가 있었고, 그 대응으로 들어간 상한이었습니다. 옮겨 가는 호스트에서도 그대로 돌 예정이었습니다.

프로젝트마다 걸린다는 오해

저는 이 줄을 "이 프로젝트의 빌드 캐시를 20GB 이하로 유지한다"로 읽고 있었습니다. 워크플로 안에 있고, 그 워크플로는 한 리포에만 속하니 자연스러운 독해였습니다.

틀렸습니다. docker builder prune 의 대상은 BuildKit 데몬의 캐시 전체입니다. 프로젝트 단위도, compose 프로젝트 단위도 아닙니다. 같은 Docker 데몬을 쓰는 모든 빌드가 하나의 캐시 풀을 공유하고, 상한은 그 풀에 걸립니다.

옮겨 가려던 호스트에는 다른 프로젝트들이 이미 살고 있었습니다. 재보니 이랬습니다.

Images          98.98GB
Build Cache     94.22GB   (reclaimable 48.98GB)
실행 컨테이너    32개

빌드 캐시가 94GB 입니다. 여기서 제 배포가 한 번 돌면 상한 20GB 가 적용되고, 제 것이 아닌 캐시까지 포함해 74GB 가 사라집니다. 배포할 때마다 그렇게 됩니다.

정합성이 아니라 원인이 안 보이는 문제

이게 데이터를 깨뜨리지는 않습니다. 빌드 캐시는 지워져도 다시 만들어지고, 이미지도 컨테이너도 볼륨도 건드리지 않습니다. 상한을 넘겼다고 빌드가 실패하지도 않습니다.

문제는 다른 쪽에 있습니다. 캐시를 잃은 프로젝트에서는 어느 날부터 빌드가 느려졌다로만 나타납니다. 그 프로젝트의 빌드 로그 어디에도 제 서비스는 등장하지 않습니다. 커밋도 그쪽 코드도 그대로인데 시간만 늘어난 상태라, 원인을 찾으려면 같은 기계에 사는 다른 리포의 배포 스크립트를 열어 봐야 합니다. 그럴 이유를 떠올릴 사람은 없습니다.

공유 자원을 조용히 줄이는 쪽이, 시끄럽게 실패하는 쪽보다 고약합니다.

아직 일어나지 않은 사고

여기서 정직하게 적어 둘 것이 있습니다. 저는 이 현상을 겪고 찾아낸 것이 아닙니다. 배포를 켜기 전이었고, 그래서 다른 프로젝트가 느려진 적도 아직 없습니다.

발견한 경위도 제 공이 아닙니다. 이 리포의 환경 구성 문서에 "이 호스트에 개발서버를 두지 않는 이유" 항목이 있었고, 거기 세 줄로 적혀 있었습니다. 그때 실측한 캐시는 44GB 였습니다. 저는 그 결정을 뒤집으려던 참이었고, 뒤집으려면 그 근거를 하나씩 다시 봐야 했습니다. 다시 재보니 44GB 가 94GB 가 되어 있었습니다.

문서가 없었다면 켰을 것입니다. 그리고 몇 주 뒤에 다른 팀에서 빌드가 느리다는 이야기가 나왔을 때, 두 사건을 잇지 못했을 겁니다.

실행 환경에 따라 갈리는 플래그 이름

상한을 올리려고 그 줄을 고치다가, 같은 파일에 이미 적혀 있는 주석을 읽었습니다. docker builder prune 은 실행 환경에 따라 다른 구현으로 떨어집니다.

GitHub Actions 는 잡마다 HOME 을 임시 디렉터리로 덮어씁니다. 그러면 docker 가 ~/.docker/cli-plugins/docker-buildx 를 찾지 못하고 내장 레거시 구현을 씁니다. 두 구현은 받는 플래그가 다릅니다.

실행 환경 구현 상한 플래그
일반 셸 (HOME 정상) buildx 플러그인 --max-used-space
Actions 러너 (HOME 교체) 내장 레거시 --keep-storage

한쪽으로 고정하면 다른 쪽에서 unknown flag 로 죽습니다. 실제로 배포를 한 번 깨뜨린 적이 있다고 주석에 적혀 있었습니다. 그래서 지금 워크플로는 --help 출력을 grep 해서 어느 구현인지 확인한 뒤 플래그를 고릅니다.

더 성가신 것은 --keep-storage 라는 이름이 두 구현에서 같은 뜻이 아니라는 점입니다. 레거시에서는 원래 의미인 상한이고, buildx 경로에서는 하한을 뜻하는 옵션으로 매핑됩니다. 이름이 같은데 의미가 뒤집히는 플래그를 문서만 보고 고르기는 어렵습니다.

80GB 로 올리되 남겨 둔 상한

고친 것은 숫자 하나입니다. 20GB 를 80GB 로 올렸습니다. 그 호스트의 디스크 여유가 166GB 였고, 현재 캐시 94GB 를 담고도 여유가 남는 값입니다.

상한 자체를 없애지는 않았습니다. 애초에 이 줄이 생긴 이유가 캐시 무한 증식으로 디스크를 채운 사고였고, 상한을 없애면 그 사고로 돌아갑니다. 프루닝을 배포에서 떼어 주기 작업으로 옮기는 방법도 있었지만, 그러면 "배포가 자기 뒤처리를 한다"는 성질이 사라집니다. 지금은 숫자만 바꾸는 쪽이 낫다고 판단했습니다.

환경별로 값을 다르게 두는 것도 생각했다가 접었습니다. 상한은 기계의 성질이지 환경의 성질이 아닙니다. 같은 데몬을 쓰는 이상 어느 환경의 배포가 돌든 같은 풀을 깎습니다.

결국 부족해질 80GB

이 조치는 임시입니다. 프로젝트가 하나 더 그 호스트로 들어오면 80GB 도 같은 이유로 부족해집니다. 상한을 계속 올리는 것은 답이 아닙니다.

제대로 고치려면 프루닝의 대상을 좁혀야 합니다. --filter until=… 로 오래된 것만 지우는 방법이 있고, 프로젝트마다 BuildKit 빌더 인스턴스를 나누는 방법도 있습니다. 후자가 구조적으로는 맞지만 빌더를 나누면 캐시 공유의 이점도 함께 사라지고, 그 비용을 아직 재보지 못했습니다.

한 가지는 분명해졌습니다. 공유 호스트에서 도는 정리 명령은 그 명령이 무엇의 범위에서 도는지부터 확인해야 합니다. 워크플로 안에 있다고 해서 그 워크플로의 범위에서 도는 것은 아닙니다. docker system prune, docker image prune, docker volume prune 이 전부 같은 성질을 갖습니다. 저는 그중 하나를 우연히 문서 덕분에 피했을 뿐입니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.