← 개발 로그 목록

website: 시스템 지표 크론 오프셋 + crontab 문서 실측 갱신

/ 3분 분량 / 개발 로그

어드민 대시보드용 시스템 지표(#451)를 서버에 적용하고 값을 확인하던 중, cpuPercent가 100.0%로 4틱 연속 찍히는 걸 발견했다.

문제는 서버 부하가 실제로 있는지부터 확인하는 거였다. 같은 시각에 top과 uptime을 직접 돌려보니 load average가 0.03으로 완전 유휴 상태였다. 스크립트가 재는 값과 실제 서버 상태가 너무 다르니, 스크립트 자체의 측정 로직을 의심할 수밖에 없었다.

crontab을 다시 들여다봤다. snapshot-system-metrics.py가 다른 8개 스크립트(disk, git-drift, email-failure×2, email-success×2, error-log×2)와 똑같이 */5 * * * *로 등록돼 있었다. 이 서버는 nproc=2인 소형 인스턴스라, 매 5분 경계(:00, :05, :10...)마다 python 프로세스 9개가 한꺼번에 기동하는 셈이었다. 나머지 8개는 이메일 실패건수나 git 드리프트처럼 카운트/존재 체크만 하는 스크립트라 그 몰림이 로직에 영향을 안 준다. 하지만 snapshot-system-metrics.py는 그 순간의 CPU 사용률을 /proc/stat 기반으로 1초 샘플링해서 재는 스크립트다. 즉 프로세스 9개가 동시에 뜨는 그 1초 구간을 그대로 "부하"로 측정해버린 거였다. 자기 자신이 만든 컨텐션을 자기가 측정 오염으로 되돌려 받은 셈이다.

해결은 단순했다. snapshot-system-metrics.py만 다른 8개와 겹치지 않게 2-59/5 * * * *로 2분 오프셋을 줬다. :02, :07, :12...처럼 다른 스크립트들의 기동 시점을 피해서 CPU를 재게 만든 것이다. 나머지 8개는 타이밍이 로직에 얽혀 있지 않으니 그대로 뒀다 — 전부 흩뿌릴 필요 없이, "그 순간을 실측하는" 스크립트만 골라서 옮기면 되는 문제였다.

오프셋은 서버에 crontab -e로 먼저 적용해뒀고, 새 tick에서 cpuPercent가 유휴 수준의 정상값으로 찍히는지는 다음 확인 과제로 남겨뒀다(테스트 플랜에도 체크박스로 남아 있다). PR 자체는 문서만 갱신하는 커밋이라 병합은 바로 처리됐다.

겸사겸사 crontab을 실측하다가 infra/CLAUDE.md의 크론 표에서 push-error-log-metric.py(prod/stage) 두 줄이 누락된 걸 발견해서 같이 추가했다. 문서가 실제 서버 상태를 따라가지 못하고 있었던 셈인데, 이번처럼 크론을 직접 들여다볼 일이 생겼을 때 같이 바로잡는 게 맞다고 판단했다. infra/docs/observability.md에도 오프셋을 준 이유와 실측 근거(4틱 연속 100.0, 같은 시각 load average 0.03)를 남겨서, 나중에 이 스크립트만 왜 다른 스케줄을 쓰는지 다시 설명하지 않아도 되게 해뒀다.

마지막으로 pm/docs/learnings.md에 이번에 배운 걸 기록했다. 핵심은 "같은 스케줄로 여러 크론을 몰아넣으면, 그 순간을 직접 측정하는 스크립트는 자기가 만든 컨텐션을 부하로 오인한다"는 것. CPU/메모리처럼 그 순간의 실측값을 재는 크론과, 이벤트 카운트/존재 체크형 크론은 같은 취급을 하면 안 된다는 걸 이번에 실제 오탐을 통해 확인했다. 비슷한 지표 스크립트를 크론에 새로 등록할 때는 이제 이 구분부터 먼저 따져보게 될 것 같다.