MeetBack 2026.08.28 5 min read

Prometheus 설정은 맞는데 Target이 없었던 이유

Exporter는 정상인데 Prometheus Target이 없었던 원인을 Bind Mount inode와 설정 hash로 찾아 container를 재생성한 과정을 정리했다.

프로젝트 기간: 2026-08-18 ~ 2026-09-02
글 범위: Prometheus·Grafana·Loki·Alloy 모니터링

MeetBack의 monitoring stack은 Prometheus, Grafana, Alertmanager, Loki, Alloy로 구성했다. 처음 목표는 dashboard를 예쁘게 만드는 것보다 node, database, storage, container 중 어느 계층이 문제인지 빠르게 구분할 수 있는 정보를 모으는 것이었다.

모든 component는 monitoring server의 compose.yaml로 관리했다. 이 환경에서는 파일명이 docker-compose.yml이 아니라 compose.yaml이므로 운영 명령과 문서도 같은 이름으로 통일했다.

어떤 지표를 수집했는가

수집 대상을 역할별로 나눴다.

node_exporter
  → CPU, memory, disk, filesystem, network, systemd/NFS 상태

mysqld_exporter
  → MySQL up, connection, replication, read-only, process 상태

cAdvisor
  → container CPU, memory, network, filesystem

Prometheus self metrics
  → scrape, rule evaluation, storage 상태

node_exporter는 Ubuntu node 전체에 설치했다. DB server는 private network address로 scrape했고, network migration이 끝난 뒤 이전 service address target은 제거했다. 같은 server가 두 주소로 보이면 dashboard에 가짜 downtime이 남거나 role이 중복될 수 있기 때문이다.

Exporter는 모두 열리는데 Target 목록은 비어 있었다

각 node의 /metrics endpoint를 직접 curl했을 때는 모두 응답했다. promtool check config도 성공했고 reload API는 HTTP 200을 반환했다. 그런데 Prometheus API에서 node-exporter job을 조회하면 결과가 비어 있었다.

처음에는 YAML indentation이나 label 구조를 의심했다. 실제로 target과 label을 한 줄에 붙이는 식의 잘못된 YAML을 작성한 적도 있어 파일을 다시 정리했다. 하지만 host 파일 syntax는 정상인데 loaded config에는 job이 없었다.

이때 검증을 세 단계로 나눴다.

1. Exporter endpoint가 응답하는가
2. Prometheus가 어떤 configuration을 실제로 읽었는가
3. 그 configuration의 target scrape가 healthy인가

직접 endpoint 성공은 1번만 증명한다. 2번과 3번은 별도 API로 확인해야 한다.

curl -fsS http://127.0.0.1:9090/api/v1/status/config
curl -fsS http://127.0.0.1:9090/api/v1/targets

Host와 Container의 같은 경로가 다른 파일이었다

Prometheus container에는 host의 설정 파일을 bind mount했다. host 파일에는 새 target이 있었지만 container 내부 file에서 grep하면 아무것도 나오지 않았다. 두 파일의 SHA-256 hash도 달랐다.

sha256sum /opt/meetback-monitoring/prometheus/prometheus.yml
docker exec <prometheus> sha256sum /etc/prometheus/prometheus.yml

원인은 editor의 저장 방식과 bind mount의 file inode였다. 일부 editor는 기존 file 내용을 직접 덮어쓰지 않고 새 file을 만든 뒤 rename해 교체한다. 실행 중인 container는 교체 전 inode를 계속 참조할 수 있다. host에서는 같은 path에 새 내용이 보이지만 container는 오래된 file을 보고 있었다.

/-/reload는 container가 현재 보고 있는 file을 다시 읽을 뿐, bind mount를 새 inode에 다시 연결하지 않는다. HTTP 200은 reload 요청 성공이지 host의 최신 내용 반영을 보장하지 않았다.

해결은 단순 restart가 아니라 container 재생성이었다.

docker compose -f compose.yaml up -d --force-recreate prometheus

재생성 뒤 host와 container hash가 같아졌고 loaded config에 job이 나타났으며 전체 target이 UP으로 들어왔다.

설정 변경 절차를 고정했다

이후 Prometheus 변경 순서를 다음처럼 정했다.

1. 기존 설정 backup
2. YAML 수정
3. promtool check config
4. host와 container file hash 비교
5. hash가 같으면 lifecycle reload
6. 다르면 container recreate
7. loaded config 확인
8. active target과 lastError 확인
9. alert rule health 확인

syntax, mount, loaded configuration, runtime target을 서로 다른 검증으로 본다.

Grafana dashboard는 장애 확인 순서에 맞췄다

대시보드를 구성할 때 graph 수보다 읽는 순서를 중요하게 봤다.

첫 줄에는 전체 target과 node 상태를 두고, 다음에는 CPU·memory·filesystem을 배치했다. MySQL dashboard에서는 Primary/Replica 역할, IO/SQL thread, lag, connection, read-only 상태를 같이 보게 했다. NFS dashboard는 service process와 실제 /srv/nfs mount, read-only 여부, 용량과 inode를 분리했다.

NFS service가 active여도 data volume이 mount되지 않을 수 있다. 이 상태에서 application이 mountpoint의 local directory에 쓰면 storage 장애가 조용히 data 위치 변경으로 이어진다. 그래서 process up과 mount present를 별도 alert로 다뤘다.

Loki와 Alloy를 함께 둔 이유

Prometheus는 숫자로 집계할 수 있는 metric에 적합하고 Loki는 log 검색을 담당한다. Alloy는 host와 container에서 telemetry를 수집해 필요한 backend로 전달하는 agent 역할을 맡는다.

현재 Loki와 Alloy service 자체는 실행됐지만 실제 Spring Boot application log 연결은 application 배포 후 작업이다. stack component가 UP인 것과 business log가 end-to-end로 수집되는 것을 같은 완료 항목으로 보지 않았다.

현재 상태와 남은 검증

구축 시점 snapshot에서 Prometheus는 23개 target을 모두 UP으로 수집했고 alert rule 21개는 health=ok였다. 이 숫자는 특정 시점의 검증 결과이며 앞으로 target과 application service가 추가되면 바뀐다.

남은 일은 application metric과 log, request latency와 error rate, 실제 user flow를 연결하는 것이다. infrastructure metric만으로 server 상태는 알 수 있지만 사용자가 정상적으로 약속을 만들 수 있는지는 알 수 없다.

이 문제를 해결하며 가장 크게 배운 것은 “설정 파일이 맞다”와 “실행 중인 process가 그 설정을 읽었다”가 다르다는 점이었다. reload HTTP 200, exporter endpoint 200, target UP도 각각 다른 사실을 증명한다. monitoring 자체도 관찰 가능한 검증 단계가 필요했다.