macOS에서 스크립트로 띄운 Chrome을 강제 종료하면 실행마다 2.1GB가 남습니다

설치된 Google Chrome을 QA 스크립트로 띄웠다가 SIGTERM이나 SIGKILL로 끝내면 앱 번들 사본이 하나씩 남습니다. 3주 동안 138개, 약 289GB가 쌓여 디스크가 찼습니다. 원인과 종료 방식별 측정, 막는 법입니다.

Mac 한 대의 디스크가 갑자기 가득 찼습니다. 찾아 보니 임시 폴더 아래에 Chrome 앱 번들 사본이 138개, 합쳐서 약 289GB가 쌓여 있었습니다. QA·스모크 테스트 스크립트가 설치된 Google Chrome을 띄웠다가 프로세스를 죽여서 끝낼 때마다 사본이 하나씩 남은 것이었습니다. 9월 27일부터 10월 18일까지 3주 동안 쌓였습니다.

약 2.1GB강제 종료 1회마다 남는 사본
138개 · 약 289GB3주 동안 쌓인 양
0기능을 끄고 띄웠을 때 누수

증상

Chrome을 띄울 때마다 아래 위치에 code_sign_clone.XXXXXX 디렉터리가 하나씩 생기고, 강제 종료하면 지워지지 않습니다.

$(dirname "$TMPDIR")/X/com.google.Chrome.code_sign_clone/code_sign_clone.XXXXXX/

스크립트를 돌리기 전후로 개수를 세어 보면 새는지 알 수 있습니다. 정상 종료 직후에는 정리 프로세스가 지우는 중이라 잠깐 하나 늘어 보일 수 있으니, 몇 초 뒤에 다시 셉니다.

ls "$(dirname "$TMPDIR")/X/com.google.Chrome.code_sign_clone" | wc -l

원인

Chromium 소스의 chrome/browser/mac/code_sign_clone_manager에 따르면 다음과 같습니다. 이 내용은 소스 페이지 요약으로 확인했고, 원문 전체를 읽은 것은 아닙니다.

  • Chrome은 시작할 때마다 앱 번들의 copy-on-write 사본을 만듭니다. 실행 중에 업데이트로 번들이 바뀌어도 실행 중인 파일이 유효하게 남고 코드 서명이 일관되게 유지되도록 하려는 것입니다. 기본으로 켜져 있고(kMacAppCodeSignClone), Chrome for Testing 빌드는 이 과정을 건너뜁니다.
  • 사본은 클론 관리자의 소멸자가 --type=code-sign-clone-cleanup 도우미 프로세스를 띄워 지웁니다. 부모 프로세스가 끝난 뒤에 지우는 방식이라, 소멸자가 돌지 않으면 사본이 남습니다.
  • 시작할 때 예전 사본을 쓸어 내는 코드는 없습니다. 소스에는 그런 정리를 하겠다는 TODO만 있습니다.

Chrome 쪽에서 이 문제를 다룬 보고는 찾지 못했습니다. 같은 이름 규칙으로 OpenAI Codex 데스크톱 앱의 com.openai.codex.code_sign_clone이 실행마다 약 1GB씩 쌓인다는 글은 있었습니다.

종료 방식별 측정

Chrome 154.0.8037.57에서 종료 방식만 바꿔 확인했습니다.

종료 방식 결과
CDP Browser.close 0.1~1.7초에 끝나고 사본이 지워짐. 바쁜 Mac에서는 10초 넘게 걸린 적도 있음
SIGTERM 0.0~0.1초 만에 프로세스만 끝나고 사본이 남음 (3회 중 3회)
SIGKILL 사본이 남음
--disable-features=MacAppCodeSignClone 실행 중에도 사본이 생기지 않음. SIGKILL로 끝내도 누수 0

SIGTERM으로도 남는다는 점이 중요합니다. 스크립트에서 흔히 쓰는 "끝나면 프로세스에 SIGTERM" 방식으로는 막을 수 없습니다.

디스크가 차면 다른 서비스도 흔들립니다

이 Mac에서는 디스크가 찬 영향이 Chrome을 띄운 작업 밖으로 번졌습니다. 새벽에 디스크 부족으로 Docker VM(OrbStack)이 재시작됐고, 컨테이너는 다시 떴지만 그 뒤로 LAN에 닿지 못했습니다. 인터넷은 되는데 같은 네트워크의 GPU 서버에 붙지 못해, 그 서버를 쓰는 서비스의 생성 시험이 연결 시간 초과로 실패했습니다. 사본을 지우고 컨테이너를 다시 시작하자 LAN 접속이 돌아왔습니다.

디스크가 찼다가 풀린 뒤에는 컨테이너가 다시 떴는지만 보지 말고, 컨테이너 안에서 바깥 네트워크에 닿는지까지 확인해야 합니다.

누가 띄웠는지 찾기

사본 디렉터리는 Chrome 주 프로세스가 시작될 때 생기므로, 디렉터리 생성 시각이 곧 Chrome 실행 시각입니다.

stat -f %B <사본 디렉터리>

이 시각을 각 에이전트 세션 기록의 도구 호출 시각과 맞춰 보니 범인이 바로 나왔습니다. 10월 18일에 생긴 13개는 한 세션이 QA 스크립트를 몇 분 사이에 여러 번 반복 실행한 시각과 정확히 맞았고, 9월 30일의 76개도 같은 저장소의 QA 반복 실행이었습니다. 그 밖에 다른 프로젝트의 스모크 테스트와, 릴리스 확인용으로 Chrome을 직접 한 번 띄운 기록이었습니다.

처음에는 다른 에이전트를 의심했습니다. 추측으로 좁히는 것보다 생성 시각과 실행 기록을 맞춰 보는 쪽이 훨씬 빨랐습니다.

막는 법

두 겹으로 막습니다.

  1. Chrome을 --disable-features=MacAppCodeSignClone으로 띄웁니다. 사본 자체가 생기지 않으므로 어떤 방식으로 끝나도 남는 것이 없습니다. 다만 Chrome이 이 기능 이름을 바꾸면 플래그는 아무 오류 없이 무시될 수 있어서, 2번이 함께 필요합니다.
  2. 종료를 단계적으로 합니다. CDP Browser.close를 보내고 충분히(30초) 기다린 뒤, 그래도 남아 있으면 SIGTERM, 마지막에만 SIGKILL을 보냅니다. 주 프로세스가 끝난 뒤에도 정리 도우미가 일을 마칠 때까지 기다려야 합니다. 일찍 SIGKILL을 보내면 사본이 남습니다. 어느 단계에서 끝났는지 로그에 남깁니다.

몇 가지를 더 챙겼습니다.

  • Browser.close 요청에도 시간 제한을 둡니다. 멈춘 Chrome은 이 요청에 응답하지 않아서, 제한이 없으면 다음 단계로 넘어가지 못합니다.
  • 스크립트 자체가 SIGINT, SIGTERM, SIGHUP을 받았을 때도 같은 종료 단계를 거쳐 Chrome을 닫습니다.

이미 쌓인 사본 정리

스크립트를 고쳐도 이미 쌓인 사본은 따로 지워야 합니다. 이때 지금 떠 있는 Chrome이 쓰는 사본 하나는 남겨야 합니다. 평소에 쓰는 Chrome도 같은 폴더에 사본을 두고 실행 중에 사용합니다. Chrome 주 프로세스의 시작 시각(ps -o lstart -p <PID>)과 생성 시각이 같은 디렉터리가 그것입니다.

이 문제는 macOS에서 설치된 Google Chrome을 자동화 도구로 띄우는 경우에 해당합니다. 테스트 전용 빌드인 Chrome for Testing은 소스상 이 사본을 만들지 않습니다.