[내일배움캠프 TIL, Day 61] Prometheus + Grafana로 Spring Boot 메트릭 수집하기
1. 오늘 학습 키워드
- Prometheus - Pull 방식 메트릭 수집, scrape_config
- Grafana - Prometheus datasource, Explore, PromQL
- Spring Boot Actuator + Micrometer - 메트릭 노출 엔드포인트
- PromQL -
rate(), summary vs histogram 차이 - K6 - Docker 기반 부하 테스트, 베이스라인 측정
2. 개념 정리
Prometheus가 메트릭을 수집하는 방식 - Pull
대부분의 모니터링 시스템은 애플리케이션이 데이터를 “밀어넣는(Push)” 방식인데, Prometheus는 반대로 주기적으로 “당겨오는(Pull)” 방식을 쓴다.
[Push 방식]
애플리케이션 → 모니터링 서버 (앱이 직접 전송)
[Pull 방식 - Prometheus]
Prometheus → 애플리케이션의 /actuator/prometheus 호출 (Prometheus가 주기적으로 요청)
Pull 방식의 장점은 Prometheus가 수집을 제어한다는 점이다. 앱은 메트릭을 노출만 하면 되고, 언제 얼마나 자주 수집할지는 scrape_interval로 중앙에서 관리한다.
앱이 죽어도 Prometheus는 그냥 “수집 실패”로 기록할 뿐 자기 설정이 바뀌지 않는다.
Spring Boot가 메트릭을 노출하는 구조
[메트릭 노출 스택]
애플리케이션 코드 (비즈니스 로직)
↓ 자동 계측
Micrometer (측정 추상화 레이어)
↓ Prometheus 포맷으로 변환
Spring Boot Actuator (/actuator/prometheus)
↓ Prometheus가 Pull
Prometheus TSDB (시계열 저장소)
↓ PromQL 조회
Grafana (시각화)
spring-boot-starter-actuator + micrometer-registry-prometheus를 추가하면 HTTP 요청 수, 응답시간, JVM 메모리 등이 자동으로 계측된다.
별도 코드 없이 /actuator/prometheus에서 수백 개 메트릭이 노출된다.
docker-compose에서 Prometheus가 로컬 서비스를 바라보는 방법
Prometheus는 도커 컨테이너 안에서 동작하는데, Spring Boot 서비스는 로컬 호스트에서 실행된다. 컨테이너 내부에서 localhost는 컨테이너 자신을 가리키므로 로컬 서비스에 접근할 수 없다.
# prometheus.yml
static_configs:
- targets:
- host.docker.internal:8002 # 컨테이너 → 호스트 접근
# docker-compose.yml
extra_hosts:
- "host.docker.internal:host-gateway" # Linux에서 host.docker.internal 활성화
host.docker.internal은 컨테이너 → 호스트 머신을 가리키는 특수 호스트명이다. Mac에서는 Docker Desktop이 자동으로 설정해주지만, Linux에서는 extra_hosts로 직접 매핑해야 한다.
summary vs histogram - PromQL에서 삽질한 지점
Grafana에서 p95 응답을 보려고 histogram_quantile(0.95, ...) 쿼리를 쳤는데 “No data”가 뜨며 한참 헤맸다.
[histogram_quantile 사용 조건]
메트릭 타입이 Histogram이어야 함 → _bucket, _sum, _count 세 가지 시리즈 존재
[Spring Boot HTTP 메트릭 실제 타입]
http_server_requests_seconds → Summary 타입
→ _sum, _count만 있고 _bucket이 없음
→ histogram_quantile 사용 불가
Summary 타입에서 평균 응답시간을 구하는 올바른 PromQL:
rate(http_server_requests_seconds_sum{uri="/api/v1/stores/ranking"}[5m])
/
rate(http_server_requests_seconds_count{uri="/api/v1/stores/ranking"}[5m])
rate(sum) / rate(count) = 단위 시간당 누적 응답시간 / 단위 시간당 요청 수 = 평균 응답시간(초)
정확한 p95는 구할 수 없지만 K6가 p95를 직접 측정해주므로, Grafana에서는 “추세와 평균”을 보는 용도로 활용했다.
3. 이번 프로젝트에 적용한 것들
✅ Prometheus + Grafana docker-compose 추가
prometheus:
image: prom/prometheus:v2.53.0
volumes:
- ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
extra_hosts:
- "host.docker.internal:host-gateway"
grafana:
image: grafana/grafana:11.0.0
volumes:
- ./docker/grafana/provisioning:/etc/grafana/provisioning
Grafana는 provisioning 디렉토리를 마운트하면 컨테이너 시작 시 datasource를 자동 등록한다.
매번 UI에서 수동 등록할 필요 없이, datasources.yml 파일 하나로 Prometheus/Loki를 코드로 관리할 수 있다.
✅ 베이스라인 측정 - K6 + Grafana 연동
K6를 Docker로 실행하면서 Grafana Explore로 동시에 메트릭을 관측했다.
export TOKEN="eyJ..."
docker run --rm -i grafana/k6 run --env TOKEN="$TOKEN" - < ranking-load-test.js
✅ 베이스라인 수치 확인
| 엔드포인트 | 조건 | p95 |
|---|---|---|
랭킹 (/api/v1/stores/ranking) |
캐싱 없음 (상위 10건 매 요청 DB 조회) | 27.08s |
검색 (/api/v1/stores) |
Redis 캐시 적중 (USER 기준) | 26.67ms |
랭킹이 27초인 이유는 ZSet으로 순위는 캐싱됐지만, 상위 10건 매장 상세를 매 요청 DB에서 조회하기 때문이다. MVP 구현 테스트에서도 이미 확인한 결과로, 오늘은 Grafana를 활용해 시각화를 해보는 실습 위주로 기준선을 재측정 해보았다. 랭킹 응답 전체를 캐싱해 이 병목을 제거할 예정이다.
4. 학습하며 겪었던 문제점 & 에러
🟠 Prometheus 재시작 시 메트릭 데이터 소실
문제: 도커를 껐다 켜니 이전 K6 테스트 중 수집됐던 메트릭이 사라져 Grafana 그래프가 비어 있음
원인: docker-compose.yml에 Prometheus 데이터 볼륨이 없어 컨테이너 재시작 시 TSDB 초기화
해결 방향: 운영이라면 볼륨 추가가 필요하나, 로컬 개발 환경에서는 K6 수치 자체가 근거가 되므로 현재는 허용
#내일배움캠프 #단기Java #TIL #Kok #Prometheus #Grafana #K6
Leave a comment