← 개발 로그 목록

website: 어드민 CPU·메모리·디스크 지표 대시보드, dev → main 동기화

/ 5분 분량 / 개발 로그

배포 이력 대시보드(#451)에 이어 서버 CPU·메모리·디스크 사용률을 어드민에서 볼 수 있게 만든 작업이었고, 이번 PR은 그 결과물을 dev에 쌓아둔 커밋 3개를 main으로 옮기는 동기화였다.

시작은 단순했다. 인스턴스 하나가 stage와 prod를 같이 서빙하는 구조라 리소스가 부족해지면 두 환경이 같이 죽는데, 그걸 미리 볼 수 있는 화면이 없었다. OCI Monitoring이 알람은 보내주지만 "지금 얼마나 위험한 상태인지"를 대시보드에서 바로 확인하고 싶었다.

구현 방식을 정하면서 선택지가 몇 개 있었다. 백엔드가 OCI Monitoring API를 직접 조회하거나, 컨테이너 안에서 호스트의 /proc·/sys를 마운트해서 읽는 방법도 있었는데 둘 다 새 IAM 정책이나 컨테이너 격리 확장이 필요해서 기각했다. 대신 배포 이력 대시보드를 만들 때 썼던 패턴을 그대로 재사용했다 — 호스트 크론이 OS에서 직접 값을 읽어 로컬 JSON Lines 파일에 append하고, 백엔드는 그 파일을 읽기 전용으로 조회만 한다. snapshot-system-metrics.py는 /proc/stat으로 CPU idle 대비 busy 비율을 계산하고, /proc/meminfo의 MemAvailable로 메모리 사용률을 구했다(MemFree만 쓰면 회수 가능한 캐시까지 "사용 중"으로 잘못 잡히기 때문). 순수 stdlib만 써서 venv나 OCI SDK 없이도 돌아가게 했다.

한 커밋 메시지에 "코드리뷰(6개 관점 병렬 검증)에서 나온 문제 반영"이라고 적어뒀는데, 실제로 고친 것들이 꽤 있었다. 스냅샷 파일이 8640줄(30일치, 5분 간격)을 넘으면 트림하는 로직을 처음엔 파일을 통째로 열어 다시 쓰는 방식으로 했다가, 그 사이 백엔드가 읽으면 빈 파일이나 잘린 줄을 볼 수 있다는 문제를 발견해서 임시 파일에 쓰고 os.replace()로 원자적 교체하는 방식으로 바꿨다. 프런트에서는 기간 필터(6시간/24시간/7일)를 바꿀 때 이전 범위의 데이터가 새 응답이 올 때까지 화면에 남아있는 문제도 있었다 — "6시간"을 눌렀는데 아직 7일치 수치가 잠깐 보이는 식이었다. 이건 범위를 바꾸는 순간 samples를 null로 비워버리는 걸로 해결했다. 임계치 상수(메모리 85%, 디스크 80%)가 여러 곳에 중복돼 있던 것도 systemMetricsThresholds.ts로 모았다.

차트는 ECharts SVG 렌더러로 만들었다. dataviz 스킬의 카테고리 팔레트에서 CVD·명도·대비 검증을 통과한 조합(blue/aqua/orange)을 그대로 썼고, 메모리·디스크 임계치는 markLine으로 점선을 그어 표시했는데 두 임계치 값이 85%와 80%로 5%p밖에 차이가 안 나서 라벨이 겹치는 문제가 있었다. 하나는 왼쪽 위, 하나는 왼쪽 아래로 갈라서 배치해 해결했다. 접근성 쪽은 차트 아래 sr-only 테이블을 하나 더 두는 식으로 처리했다 — 시각별 수치를 스크린 리더로도 읽을 수 있게.

여기까지가 #465에서 한 작업이고, 이게 main으로 들어오기 전에 dev에서 먼저 서버에 적용하는 과정을 거쳤다(#466). 배포 이력 대시보드를 만들 때 겪었던 함정 — CD가 머지 시점에 새 마운트 디렉터리를 root 소유로 자동 생성해버리는 문제 — 을 이번에도 조심했고, chown과 crontab 등록을 미리 해서 PermissionError 없이 첫 실행부터 정상적으로 값이 쌓이는 걸 확인했다.

그런데 실제로 값이 쌓이는 걸 보다가 이상한 걸 발견했다. CPU 사용률이 특정 시점마다 100%로 튀는데, 같은 시각의 top이나 uptime은 완전히 유휴 상태였다. 원인을 추적해보니 snapshot-system-metrics.py가 다른 8개의 push 스크립트와 똑같이 */5 * * * *에 걸려 있어서, 매 5분 경계마다 python 프로세스 9개가 동시에 기동되고 있었다. CPU 사용률을 측정하는 스크립트가 하필 그 순간의 몰림 자체를 재는 바람에 자기 자신의 기동 부하를 100%로 오측정한 것이었다. 크론 시각을 2분 오프셋(2-59/5)으로 밀어서 다른 스크립트들과 겹치지 않게 했고, 겸사겸사 crontab 문서에 빠져 있던 push-error-log-metric.py도 표에 추가했다(#468).

이 PR 467은 그렇게 dev에 쌓인 세 커밋 — 대시보드 구현, 서버 적용 완료, 크론 오프셋 수정 — 을 main으로 반영하는 동기화였다. 인스턴스가 stage/prod를 공유하는 단일 구조라 크론과 마운트는 이미 dev 단계에서 서버에 적용·검증까지 끝나 있었고, main 머지로 추가되는 서버 설정은 없었다. 남은 건 prod CD로 backend-prod가 재기동되는 것을 확인하는 것뿐이었다.