들어가며
"서버가 느린 것 같은데 원인을 모르겠어요", "장애가 발생했는데 언제부터인지 파악이 안 돼요 API Gateway 패턴의 Circuit Breaker와 장애 대응" - 모니터링이 없는 서비스에서 자주 듣는 말입니다. Prometheus와 Grafana는 오픈소스 모니터링의 사실상 표준(de facto standard)으로, CNCF 졸업 프로젝트이기도 합니다. 2026 백엔드 기술 스택에서의 모니터링 선택 이번 글에서는 Prometheus의 아키텍처와 PromQL, Grafana 대시보드 구축, Spring Boot Micrometer 연동, 커스텀 메트릭 작성, AlertManager 알림 설정까지 실전 수준으로 다루겠습니다.
1. Prometheus 아키텍처
Pull 모델
Prometheus는 모니터링 대상이 메트릭을 Push하는 것이 아니라, Prometheus 서버가 주기적으로 대상의 엔드포인트를 Pull(스크레이핑)하는 방식입니다.
┌──────────────────────────────────────────────────┐
│ Prometheus Server │
│ ┌───────────┐ ┌────────┐ ┌──────────────────┐ │
│ │ Retrieval │ │ TSDB │ │ HTTP Server │ │
│ │ (Scraper) │→ │(Storage)│→ │ (PromQL API) │ │
│ └─────┬─────┘ └────────┘ └────────┬─────────┘ │
│ │ │ │
└────────┼──────────────────────────────┼───────────┘
│ Pull (HTTP GET /metrics) │ PromQL 쿼리
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Spring Boot │ │ Grafana │
│ /actuator/ │ │ 대시보드 │
│ prometheus │ └──────────────┘
└─────────────┘ │
▼
┌──────────────┐
│ AlertManager │
│ (알림 전송) │
└──────────────┘
│ │ │
Slack Email PagerDuty
Pull vs Push 모델 비교
| 항목 | Pull (Prometheus) | Push (InfluxDB, Datadog) |
|---|---|---|
| 데이터 수집 | Prometheus가 대상을 스크레이핑 | 애플리케이션이 서버로 전송 |
| 장점 | 대상 상태 확인 가능, 중앙 제어 | 방화벽 뒤 대상 수집 가능 |
| 단점 | 방화벽 뒤 대상 수집 어려움 | 대상 장애 감지 어려움 |
| 서비스 디스커버리 | 필수 (자동 대상 감지) | 불필요 (대상이 직접 전송) |
TSDB (Time Series Database)
Prometheus는 자체 TSDB에 시계열 데이터를 저장합니다. 각 메트릭은 이름과 레이블의 조합으로 식별됩니다.
# 메트릭 형식: metric_name{label1="value1", label2="value2"} value timestamp
http_requests_total{method="GET", path="/api/users", status="200"} 1527 1617183600
http_requests_total{method="POST", path="/api/orders", status="201"} 342 1617183600
http_request_duration_seconds{method="GET", path="/api/users", quantile="0.99"} 0.45
메트릭 타입
- Counter: 단조 증가 값 (요청 수, 에러 수). 절대 감소하지 않음. rate()와 함께 사용.
- Gauge: 증감 가능한 값 (현재 온도, 메모리 사용량, 활성 스레드 수)
- Histogram: 관측값의 분포 (응답 시간, 요청 크기). 버킷별 카운트 + 합계
- Summary: 클라이언트에서 계산된 분위수. Histogram과 유사하나 서버측 집계 불가
2. PromQL 핵심 문법
PromQL은 Prometheus의 쿼리 언어로, 시계열 데이터를 필터링하고 집계하는 데 사용됩니다.
기본 쿼리
# 특정 메트릭의 현재 값
http_requests_total
# 레이블 필터링
http_requests_total{method="GET"}
http_requests_total{status=~"5.."} # 정규식: 5xx 에러
http_requests_total{path!="/health"} # 제외
# 시간 범위 선택 (Range Vector)
http_requests_total[5m] # 최근 5분간의 데이터
주요 함수
# rate(): Counter의 초당 변화율 (가장 많이 쓰는 함수)
rate(http_requests_total[5m])
# 결과: 초당 요청 수 (RPS)
# irate(): 마지막 두 데이터 포인트 기반 순간 변화율
irate(http_requests_total[5m])
# 순간적인 스파이크 감지에 유리
# increase(): 기간 내 총 증가량
increase(http_requests_total[1h])
# 결과: 1시간 동안의 총 요청 수
# histogram_quantile(): Histogram에서 분위수 계산
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 결과: P99 응답 시간
# sum(), avg(), max(), min(): 집계 함수
sum(rate(http_requests_total[5m])) by (method)
# 결과: HTTP 메서드별 초당 요청 수
avg(rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])) by (path)
# 결과: 경로별 평균 응답 시간
실전 유용 쿼리 모음
# 1. 전체 RPS (Requests Per Second)
sum(rate(http_server_requests_seconds_count[5m]))
# 2. 경로별 P99 응답 시간
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri)
)
# 3. 5xx 에러율 (%)
100 * sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
# 4. JVM 힙 메모리 사용률 (%)
100 * jvm_memory_used_bytes{area="heap"}
/ jvm_memory_max_bytes{area="heap"}
# 5. HikariCP 커넥션 풀 사용률
hikaricp_connections_active / hikaricp_connections_max
# 6. GC 일시 정지 시간 (초/분)
rate(jvm_gc_pause_seconds_sum[5m])
# 7. 시스템 CPU 사용률
system_cpu_usage
process_cpu_usage
3. Spring Boot Micrometer 연동
의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}
application.yml 설정
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
http.server.requests: true # 응답 시간 히스토그램 활성화
sla:
http.server.requests: 100ms, 300ms, 500ms, 1s # SLA 버킷
spring:
application:
name: order-service
기본 제공 메트릭 확인
# Spring Boot Actuator Prometheus 엔드포인트 Spring Boot Actuator 완벽 활용으로 메트릭 노출하기
$ curl http://localhost:8080/actuator/prometheus
# 출력 예시
# HELP http_server_requests_seconds Duration of HTTP server request handling
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{method="GET",uri="/api/products",status="200",le="0.1"} 450
http_server_requests_seconds_bucket{method="GET",uri="/api/products",status="200",le="0.3"} 520
http_server_requests_seconds_bucket{method="GET",uri="/api/products",status="200",le="0.5"} 535
http_server_requests_seconds_bucket{method="GET",uri="/api/products",status="200",le="+Inf"} 540
http_server_requests_seconds_count{method="GET",uri="/api/products",status="200"} 540
http_server_requests_seconds_sum{method="GET",uri="/api/products",status="200"} 42.35
# JVM 메트릭
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 52428800
jvm_threads_live_threads 45
jvm_gc_pause_seconds_count{action="end of minor GC",cause="G1 Evacuation Pause"} 12
# HikariCP 메트릭
hikaricp_connections_active{pool="HikariPool-1"} 3
hikaricp_connections_idle{pool="HikariPool-1"} 7
hikaricp_connections_max{pool="HikariPool-1"} 10
4. 커스텀 메트릭 작성
@Counted, @Timed 어노테이션
@Configuration
public class MetricsConfig {
// @Timed 어노테이션 활성화
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
// @Counted 어노테이션 활성화
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MeterRegistry meterRegistry;
// @Timed: 메서드 실행 시간 자동 측정
@Timed(
value = "order.create.duration",
description = "주문 생성 소요 시간",
percentiles = {0.5, 0.95, 0.99}
)
@Counted(
value = "order.create.count",
description = "주문 생성 횟수"
)
public OrderResponse createOrder(CreateOrderRequest request) {
Order order = Order.create(request);
orderRepository.save(order);
// 주문 금액 메트릭 (커스텀)
meterRegistry.counter("order.amount.total",
"payment_method", request.paymentMethod()
).increment(order.getTotalAmount());
return OrderResponse.from(order);
}
}
프로그래밍 방식 커스텀 메트릭
@Component
@RequiredArgsConstructor
public class BusinessMetrics {
private final MeterRegistry meterRegistry;
private final AtomicInteger activeOrders = new AtomicInteger(0);
@PostConstruct
public void init() {
// Gauge: 현재 처리 중인 주문 수
Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
.description("현재 처리 중인 주문 수")
.register(meterRegistry);
// Gauge: 시스템 상태 (1=UP, 0=DOWN)
Gauge.builder("app.health", this, self -> self.isHealthy() ? 1.0 : 0.0)
.description("애플리케이션 상태")
.register(meterRegistry);
}
// Counter: 비즈니스 이벤트 카운팅
public void recordPaymentSuccess(String method, double amount) {
meterRegistry.counter("payment.success",
"method", method
).increment();
// Distribution Summary: 결제 금액 분포
DistributionSummary.builder("payment.amount")
.description("결제 금액 분포")
.baseUnit("won")
.tag("method", method)
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
.record(amount);
}
public void recordPaymentFailure(String method, String reason) {
meterRegistry.counter("payment.failure",
"method", method,
"reason", reason
).increment();
}
// Timer: 외부 API 호출 시간 측정
public T measureExternalApi(String apiName, Supplier action) {
Timer timer = Timer.builder("external.api.duration")
.description("외부 API 호출 시간")
.tag("api", apiName)
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
return timer.record(action::get);
}
public void incrementActiveOrders() { activeOrders.incrementAndGet(); }
public void decrementActiveOrders() { activeOrders.decrementAndGet(); }
private boolean isHealthy() { return true; }
}
5. Prometheus 서버 설정
prometheus.yml
global:
scrape_interval: 15s # 스크레이핑 간격
evaluation_interval: 15s # 알림 규칙 평가 간격
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
# Prometheus 자체 모니터링
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Spring Boot 애플리케이션
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets: ['app1:8080', 'app2:8080']
labels:
env: 'production'
# Kubernetes 서비스 디스커버리
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# Node Exporter (서버 시스템 메트릭)
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
6. Grafana 대시보드 구축
주요 대시보드 패널
Spring Boot 모니터링 대시보드에 꼭 포함해야 할 패널입니다.
// Grafana 대시보드 JSON 패널 예시 (RPS 패널)
{
"title": "Requests Per Second",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) by (uri)",
"legendFormat": "{{uri}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps"
}
}
}
추천 대시보드 구성
| 행 | 패널 | PromQL |
|---|---|---|
| 요약 | 총 RPS | sum(rate(http_server_requests_seconds_count[5m])) |
| 요약 | 에러율 (%) | 100 * sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) |
| 요약 | P99 응답시간 | histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le)) |
| HTTP | 경로별 RPS | sum(rate(http_server_requests_seconds_count[5m])) by (uri) |
| HTTP | 상태코드 분포 | sum(rate(http_server_requests_seconds_count[5m])) by (status) |
| JVM | 힙 메모리 | jvm_memory_used_bytes{area="heap"} |
| JVM | GC 일시정지 | rate(jvm_gc_pause_seconds_sum[5m]) |
| DB | 커넥션 풀 | hikaricp_connections_active |
Grafana 프로비저닝 (코드로 대시보드 관리)
# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
7. AlertManager 알림 설정
알림 규칙 정의 (alert_rules.yml)
groups:
- name: spring-boot-alerts
rules:
# 1. 높은 에러율 경고
- alert: HighErrorRate
expr: |
100 * sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) > 5
for: 2m
labels:
severity: critical
annotations:
summary: "높은 에러율 감지 ({{ $value | printf \"%.1f\" }}%)"
description: "5xx 에러율이 5%를 초과했습니다. 현재 {{ $value | printf \"%.1f\" }}%"
# 2. 느린 응답 시간
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (le)
) > 2
for: 3m
labels:
severity: warning
annotations:
summary: "P99 응답 시간 초과 ({{ $value | printf \"%.2f\" }}s)"
description: "P99 응답 시간이 2초를 초과했습니다."
# 3. 높은 메모리 사용률
- alert: HighHeapUsage
expr: |
100 * jvm_memory_used_bytes{area="heap"}
/ jvm_memory_max_bytes{area="heap"} > 85
for: 5m
labels:
severity: warning
annotations:
summary: "JVM 힙 메모리 사용률 높음 ({{ $value | printf \"%.0f\" }}%)"
# 4. 커넥션 풀 고갈 위험
- alert: ConnectionPoolExhaustion
expr: |
hikaricp_connections_active
/ hikaricp_connections_max > 0.8
for: 2m
labels:
severity: warning
annotations:
summary: "DB 커넥션 풀 사용률 80% 초과"
# 5. 인스턴스 다운
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "인스턴스 다운: {{ $labels.instance }}"
description: "{{ $labels.job }}의 {{ $labels.instance }}가 1분 이상 응답하지 않습니다."
AlertManager 설정 (alertmanager.yml)
global:
slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
route:
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'slack-critical'
routes:
- match:
severity: critical
receiver: 'slack-critical'
- match:
severity: warning
receiver: 'slack-warning'
receivers:
- name: 'slack-critical'
slack_configs:
- channel: '#alerts-critical'
title: '{{ .GroupLabels.alertname }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
{{ end }}
send_resolved: true
- name: 'slack-warning'
slack_configs:
- channel: '#alerts-warning'
title: '{{ .GroupLabels.alertname }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
{{ end }}
send_resolved: true
8. Docker Compose로 모니터링 스택 구축 Docker 실전 가이드로 컨테이너 이해하기
# docker-compose.monitoring.yml
services:
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle' # 설정 핫 리로드
restart: unless-stopped
grafana:
image: grafana/grafana:10.4.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
depends_on:
- prometheus
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.27.0
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
실행 및 확인
# 모니터링 스택 시작
$ docker-compose -f docker-compose.monitoring.yml up -d
# 상태 확인
$ docker-compose -f docker-compose.monitoring.yml ps
# Prometheus UI: http://localhost:9090
# - Status > Targets 에서 스크레이핑 대상 상태 확인
# - Graph 탭에서 PromQL 쿼리 테스트
# Grafana UI: http://localhost:3000 (admin/admin123)
# - Data Sources에서 Prometheus 연결 확인
# - Dashboard > Import > ID 4701 (JVM Micrometer)
# - Dashboard > Import > ID 11378 (Spring Boot Statistics)
# AlertManager UI: http://localhost:9093
# - 활성 알림 확인
# Prometheus 설정 리로드 (변경 시)
$ curl -X POST http://localhost:9090/-/reload
추천 Grafana 대시보드 ID
- 4701: JVM (Micrometer) - JVM 상세 모니터링
- 11378: Spring Boot Statistics - HTTP, DB, 캐시 통합
- 1860: Node Exporter Full - 서버 시스템 모니터링
- 13587: Spring Boot Observability - 최신 대시보드
마치며
Prometheus + Grafana 모니터링 스택은 서비스 안정성의 근간입니다. MSA 관측 가능성 완벽 가이드 핵심 포인트를 정리합니다.
- Prometheus Pull 모델: 서버가 대상을 스크레이핑하는 방식으로, 대상의 상태(UP/DOWN)도 자동으로 감지할 수 있습니다.
- PromQL:
rate(),histogram_quantile(),sum() by ()세 가지만 잘 사용하면 대부분의 모니터링 쿼리를 작성할 수 있습니다. - Micrometer: Spring Boot의 메트릭 파사드로,
@Timed,@Counted어노테이션으로 간편하게 메트릭을 수집합니다. - 커스텀 메트릭: 비즈니스 메트릭(주문 수, 결제 금액 등)을 추가하면 기술 메트릭만으로는 보이지 않는 인사이트를 얻을 수 있습니다.
- AlertManager: 임계값 기반 알림으로 장애를 조기에 감지합니다. severity 기반 라우팅으로 알림 피로를 줄이세요.
- Docker Compose: 전체 모니터링 스택을 코드로 관리하면 환경 구축이 쉽고 재현 가능합니다.
모니터링은 장애 발생 후가 아니라 서비스 출시 전에 구축해야 합니다. "측정하지 않으면 개선할 수 없다"는 격언을 기억하며, 모니터링 체계를 먼저 갖추고 서비스를 운영하시길 권장합니다.
'DevOps' 카테고리의 다른 글
| Git 고급 전략 - 브랜치 전략부터 위기 탈출까지 (1) | 2026.04.15 |
|---|---|
| Docker Compose 실전 - Spring Boot + DB + Redis + Kafka 개발 환경 구축 (0) | 2026.04.14 |
| Terraform으로 인프라 코드화 - AWS 실전 예제로 배우는 IaC (0) | 2026.04.07 |
| Kubernetes 실전 운영 - Helm, Ingress, HPA 오토스케일링 (0) | 2026.04.07 |
| Kubernetes 입문 - Pod, Service, Deployment 핵심 개념 총정리 (0) | 2026.04.06 |