기술

uv 캐시를 끈 게 아니라 실행마다 만들고 있었습니다 — 임시 캐시 34,147개, 650GB

2026.09.269분 읽기

운영 장비 한 대의 디스크가 아침에 차 있었습니다. 926GB 중 903GB 를 쓰고, 남은 공간은 135MB 였습니다. 로그 파일 하나를 새로 만들지 못하는 상태라 무엇이 잘못됐는지 적어 둘 자리부터 없었습니다. 잡은 것은 감시가 아니라 사람이었습니다. '용량이 찬 것 같은데' 한 마디가 시작이었습니다.

감시 스크립트는 그 장비에 있었지만 보는 것은 헬스체크 응답뿐이었습니다. 디스크 남은 공간은 감시 항목이 아니었습니다. 서비스가 200 을 주는 것과 디스크가 비어 있는 것은 별개였습니다.


첫 추정은 그럴듯했고 1분 만에 기각됐습니다

처음 떠올린 범인은 며칠 전 통째로 동기화해 둔 문서 폴더였습니다. 최근에 큰 것을 받았으니 그것이라는 가설은 자연스럽습니다. 지우기 전에 `du` 로 쟀습니다. 문서 동기화 폴더는 11GB, 클라우드 드라이브 동기화 폴더는 8KB 였습니다. 그 폴더부터 지웠다면 11GB 를 벌고 문제는 그대로 남았을 겁니다.


찾은 순서 일곱 단계

이 장비에는 파일이 2,600만 개 있습니다. 전체 `du` 한 번이 10분입니다. 전체는 백그라운드로 돌려 두고, 의심 순으로 하위 트리를 좁혀 쟀습니다. 단계마다 추정과 실측을 나눠 적습니다.

단계

한 것

추정

실측

1

`df -h`

어느 볼륨이 찼나

시스템 볼륨 하나가 903GB

2

`du -xh -d1 ~`

홈이 대부분일 것

홈은 195GB. 차이 700GB 가 홈 밖에 있다

3

`du -xsh /private/var/folders /Library /opt …`

라이브러리나 도커

`/private/var/folders` 가 675GB

4

임시 디렉터리 안에서 접두어별 개수·크기

업로드 작업 폴더일 것

업로드 작업 폴더 901개 23GB. 숨김 `.tmpXXXXXX` 34,147개 약 650GB

5

폴더 하나를 열어봄

정체 불명

`.gitignore`·`.lock`·`CACHEDIR.TAG` 와 파이썬 패키지. uv 캐시 디렉터리 모양

6

`ps -axo pid,ppid,command` 결과에서 `uvx` 검색

누가 만드나

`uvx --no-cache --from <로컬 레포> <MCP 서버>`. 에이전트 프로세스가 띄운 것

7

메인 설정 파일을 열어 `--no-cache` 를 고치고 재시작

이걸로 끝

`ps` 에 옛 인자가 그대로. `grep -rl -- "--no-cache" <설정 디렉터리>` 를 돌리니 프로필별 설정 넷이 같은 값을 따로 들고 있었다

시간을 먹은 것은 셋이었습니다.

`ls *` 는 숨김 항목을 빼서 크기 합이 안 맞습니다. 650GB 가 숨김 폴더였고 `.[!.]*` 까지 세어야 나왔습니다

`Docker.raw` 가 `ls` 로는 926GB 로 보입니다. 희소 파일이라 `du` 로 본 실제 크기는 13GB 였습니다

보이는 것을 다 더했는데 모자라면 안 보이는 곳이 있습니다. 903 에서 195 를 뺀 700 이 방향을 줬습니다

디스크 903GB 가 홈 밖 임시 디렉터리의 숨김 폴더 650GB 로 좁혀지는 네 단계


캐시를 끈 게 아니라 실행마다 만들고 있었습니다

`uvx --no-cache` 를 붙인 이유는 로컬 코드의 최신 상태를 늘 쓰려는 것이었습니다. uv 는 기본값으로 `pyproject.toml` 이 바뀔 때만 다시 빌드하고 `.py` 수정은 보지 않습니다. 캐시를 끄면 매번 새로 빌드하니 안전하겠다고 본 것입니다.

그런데 `--no-cache` 는 '캐시를 안 쓴다'가 아닙니다. 영구 캐시(`~/.cache/uv`)를 안 쓰는 대신 **실행마다 `$TMPDIR` 에 새 캐시 디렉터리를 만듭니다.** 의존성을 풀어 놓은 그 폴더가 하나에 25~90MB 입니다. 그리고 이 환경에서는 프로세스가 죽을 때 그 폴더가 안 지워졌습니다.

곱해지는 자리가 문제였습니다. 그 장비에서는 에이전트 프로세스가 세션을 열 때와 크론 회차마다 MCP 서버 둘을 `uvx` 로 띄웁니다. 한 번에 폴더 둘입니다. 9월 13일 새벽부터 5일 동안 34,147개가 쌓였으니 시간당 280개 남짓, 분마다 네댓 개꼴입니다. 한 번의 비용은 작아도 뜨고 죽는 프로세스의 옵션은 하루 비용으로 곱해집니다.

no-cache 옵션이 캐시를 없애는 게 아니라 실행마다 임시 폴더에 만드는 흐름의 좌우 대비


지우기, 막기, 그리고 안 간 더 깔끔한 길

지우기부터 했습니다. `.tmp*` 를 전부 지우면 되는데, 살아 있는 MCP 서버 둘이 쥐고 있는 폴더가 있습니다. 그 발밑을 빼면 그 세션이 죽습니다. `lsof -p <pid>` 로 확인한 폴더 2개만 빼고, 1시간 넘은 업로드 작업 폴더와 함께 `nohup … &` 로 떼어 지웠습니다. 남은 공간이 105MB 에서 677GB 로 돌아왔고 사용률은 26% 가 됐습니다.

막는 쪽은 세 갈래였습니다.

갈래

무엇

판단

`--no-cache` 유지 + 청소 크론

누수를 그대로 두고 치우기만 한다

안 고름

`--refresh`

영구 캐시를 쓰되 시작할 때 로컬 코드 변경을 다시 확인한다. 임시 폴더가 안 생긴다

고름

플래그 없이 `[tool.uv] cache-keys`

`.py` 가 바뀌면 다시 빌드한다. 시작할 때 네트워크를 안 본다

가장 깔끔하지만 레포 둘을 손봐야 해서 뒤로

`--refresh` 로 바꾸고 에이전트의 서비스 둘을 `launchctl kickstart -k` 로 재시작했습니다. 고아로 남은 옛 `uvx --no-cache` 프로세스는 따로 죽였습니다. 그 뒤 40분간 새 임시 캐시는 0개였습니다.

여기서 한 번 되돌아갔습니다. 메인 설정 파일을 고치고 재시작한 뒤 '고쳤다'고 했는데 `ps` 에 옛 인자가 계속 떴습니다. 그제야 설정 디렉터리 전체를 `grep` 했고, 프로필별 설정 파일 넷이 같은 값을 따로 들고 있었습니다. '고쳤다'의 근거를 파일이 아니라 실제 실행 인자로 삼고 나서야 넷이 잡혔습니다.

더 깔끔한 길은 MCP 서버 레포 둘의 `pyproject.toml` 에 아래를 넣는 것입니다.

[tool.uv]
cache-keys = [{ file = "pyproject.toml" }, { file = "**/*.py" }]

이러면 플래그 없이도 `.py` 가 바뀔 때 다시 빌드합니다. `--refresh` 는 시작할 때마다 캐시 항목의 최신 여부를 확인하므로 원격 의존성이 있으면 인덱스를 다시 봅니다. 네트워크가 없는 환경이면 이쪽이 답입니다.

임시 캐시를 막는 세 갈래 — no-cache 유지와 청소, refresh, cache-keys 의 비교


같은 임시 폴더에서 하나 더 나왔습니다

같은 임시 디렉터리에 업로드 작업 폴더가 901개, 23GB 있었습니다. 그 장비에서 도는 문서 색인 서비스가 `tempfile.mkdtemp` 로 만든 작업 폴더를 색인이 끝난 뒤 안 지우고 있었습니다. 전날 다른 색인 경로 하나를 `finally` 에서 `shutil.rmtree` 하도록 고쳤는데, 옆의 업로드 경로는 그대로였습니다. 하나를 고쳤으면 같은 패턴을 `grep` 했어야 했습니다. 한 줄짜리 수정을 할 일에 올렸고, 이 글 시점에는 아직 안 고쳐져 있습니다.


남긴 것 셋

임시 폴더를 한 번 봅니다. '끈다'는 옵션은 대개 '어딘가에 임시로 만든다'입니다. 없어지는 게 아니라 위치가 바뀌니, 그 위치가 어디고 누가 지우는지를 봅니다

설정은 한 파일이 아닙니다. 프로필·오버라이드·환경별 설정이 같은 값을 따로 들면 한 곳만 고친 것은 안 고친 것입니다. 고친 뒤에는 `ps` 로 실제 인자를 봅니다

디스크 남은 공간을 감시 항목에 넣습니다. 디스크가 100% 면 로그도 못 쓰니 사후 분석 재료까지 같이 사라집니다. 90% 를 넘으면 알리는 것을 할 일로 남겼습니다

`--no-cache` 가 필요한 자리는 있습니다. 일회성 실행·격리 빌드·재현 확인에서는 영구 캐시를 오염시키지 않는 것이 맞습니다. 문제는 플래그가 아니라 매번 뜨는 프로세스에 붙어 있고 죽을 때 안 지운다는 조합이었습니다. '안 지운다'도 이 환경의 관측입니다. 확정한 것은 34,147개가 남아 있었다는 것과 바꾼 뒤 40분간 0개였다는 것뿐입니다.

이틀 전에는 같은 감시 스크립트가 헬스체크를 잘못 읽었고, 이번에는 디스크를 아예 안 봤습니다. 감시가 '살아있냐'만 물을 때 무엇이 빠지는지는 아래 글에 있습니다. 이번 사고는 사람이 잡았고, 다음 것도 아직은 사람이 잡아야 합니다.

#uv#uvx#캐시#디스크풀#임시파일누수#MCP서버#에이전트운영#디스크감시#사고회고