C++ Observability: Prometheus와 Grafana로 C++ 서버 모니터링 구축하기
이 글의 핵심
C++ 서버에서 메트릭을 노출하고 Prometheus가 수집·Grafana로 시각화하는 파이프라인을 구축하는 방법을 다룹니다. 문제 시나리오, 완전한 예제, 일반적인 에러, 프로덕션 패턴까지 C++ 실전 가이드 시리즈에서 다룹니다.
들어가며: “왜 느려졌는지” 데이터로 보기
지표가 있어야 대응할 수 있다
43-1, 43-2에서 RPC·보안을 다뤘다면, 운영 단계에서는 지표(메트릭)(요청 수, 지연 시간, CPU 사용률 등 측정값)가 필수입니다. Prometheus는 pull 방식(Prometheus 서버가 타겟에 요청해 메트릭을 가져오는 방식)으로 타겟의 HTTP 엔드포인트에서 메트릭을 수집하고, Grafana로 대시보드를 만들어 시각화합니다.
C++ 서버에서 Prometheus 형식으로 메트릭을 노출하려면: Counter·Gauge·Histogram을 정의하고, /metrics 같은 경로에서 텍스트로 내보내면 됩니다. prometheus-cpp 같은 라이브러리를 쓰거나, 간단한 포맷만 직접 구현할 수 있습니다.
이 글에서 다루는 것:
- Prometheus 메트릭 형식: Counter, Gauge, Histogram·라벨
- C++에서 메트릭 노출: 라이브러리·수동 구현·스레드 안전
- Grafana 연동: Prometheus 데이터 소스·대시보드 예시
- 문제 시나리오·일반적인 에러·프로덕션 패턴
실제 문제 시나리오
시나리오 1: 갑자기 느려진 API, 원인을 모를 때
상황: C++ gRPC 서버가 새벽 2시에 응답 지연이 10초로 급증
문제: 로그만으로는 "어디서" 멈췄는지 알 수 없음
결과: Prometheus 메트릭 없음 → 요청 수·지연 분포·에러율 추이를 볼 수 없음
→ 원인 파악에 수 시간 소요, 장애 대응 지연
시나리오 2: 메모리 사용량이 서서히 증가
상황: C++ 서버가 3일째 메모리 사용량이 계속 상승
문제: 힙 사용량·연결 수·큐 길이를 시간별로 추적할 수 없음
결과: Gauge 메트릭 없음 → 메모리 누수 의심만 하고 증거 없음
→ 재시작으로 임시 해결, 근본 원인 미해결
시나리오 3: 특정 엔드포인트만 에러율이 높음
상황: 전체 에러율은 1%인데, /api/payment만 30% 에러
문제: path별 메트릭이 없으면 문제 엔드포인트를 특정할 수 없음
결과: 라벨 없이 전체 Counter만 노출 → 세분화된 분석 불가
→ 잘못된 엔드포인트를 최적화하거나, 문제 경로를 놓침
시나리오 4: 배포 후 성능 회귀 감지
상황: 새 버전 배포 후 "뭔가 느려진 것 같다"는 사용자 피드백
문제: 배포 전후 p99 지연 시간·RPS를 비교할 데이터가 없음
결과: Histogram 없음 → 백분위수 비교 불가
→ 롤백 여부를 데이터 없이 결정, 불필요한 롤백 또는 장애 방치
이 글에서는 위와 같은 문제를 예방하는 Prometheus·Grafana 파이프라인과 완전한 예제를 다룹니다.
목차
- Prometheus 메트릭
- C++에서 메트릭 노출
- Prometheus 설정과 수집
- Grafana 연동
- 완전한 Prometheus·Grafana 예제
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 구현 체크리스트
- 정리
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
1. Prometheus 메트릭
Counter·Gauge·Histogram
- Counter: 단조 증가하는 값(요청 수, 바이트 전송량). rate()로 초당 증가량을 뽑습니다.
- Gauge: 올라갔다 내려갔다 하는 값(연결 수, 큐 길이, 메모리 사용량).
- Histogram: 분포(지연 시간). 버킷과 총합·카운트를 노출하고, Prometheus에서 histogram_quantile로 백분위수를 계산합니다.
- 라벨: 메트릭 이름에 라벨(예: method, path, status)을 붙이면 필터·그룹화가 가능합니다. 라벨 조합이 폭발하지 않도록 카디널리티를 제한하는 것이 좋습니다.
Histogram 버킷 선택 가이드
| 서비스 유형 | 권장 버킷 (초) | 설명 |
|---|---|---|
| 저지연 API | 0.001, 0.005, 0.01, 0.025, 0.05, 0.1 | ms 단위 지연 측정 |
| 일반 API | 0.005, 0.025, 0.1, 0.5, 1.0, 2.5 | REST/gRPC 등 |
| 배치 처리 | 1, 5, 10, 30, 60, 120 | 장시간 작업 |
Prometheus 텍스트 포맷 예시
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/api"} 1234
http_requests_total{method="POST",path="/api"} 567
# HELP http_request_duration_seconds Request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 50
http_request_duration_seconds_bucket{le="0.1"} 100
http_request_duration_seconds_bucket{le="0.5"} 200
http_request_duration_seconds_bucket{le="1.0"} 250
http_request_duration_seconds_bucket{le="+Inf"} 300
http_request_duration_seconds_sum 45.2
http_request_duration_seconds_count 300
메트릭 수집 아키텍처
flowchart LR
subgraph Cpp["C++ 서버"]
M[/metrics 엔드포인트]
end
subgraph Prom["Prometheus"]
S[Scrape]
TS[Time Series DB]
end
subgraph Graf["Grafana"]
D[대시보드]
A[알림]
end
M -->|HTTP GET| S
S --> TS
TS -->|PromQL| D
TS -->|Alert Rules| A
Scrape 시퀀스
sequenceDiagram
participant P as Prometheus
participant C as C++ 서버
loop scrape_interval (예: 15초)
P->>C: GET /metrics
C->>C: export_metrics() 호출
C->>P: 200 OK, text/plain
P->>P: 파싱 후 시계열 DB 저장
end
2. C++에서 메트릭 노출
라이브러리 vs 수동
- prometheus-cpp: Counter/Gauge/Histogram을 등록하고 Serialize로 텍스트를 만듭니다. 다중 스레드에서 접근할 때는 atomic 또는 락으로 보호해야 합니다.
- 수동: std::atomic Counter/Gauge와 버킷별 카운트를 두고, /metrics 핸들러에서 포맷에 맞게 문자열을 조합해 응답합니다. Content-Type: text/plain; charset=utf-8 등 Prometheus가 기대하는 헤더를 붙입니다.
- 위치: 메트릭 엔드포인트는 관리용 포트나 별도 경로로 두고, 인증·네트워크 분리를 고려해 내부에서만 접근 가능하게 하는 것이 보안에 좋습니다.
수동 구현: 최소 동작 예제
request_count는 요청이 올 때마다 fetch_add(1, memory_order_relaxed)로 올리고, export_metrics()는 Prometheus 텍스트 포맷(이름 값\n)으로 문자열을 만들어 반환합니다. /metrics HTTP 핸들러에서 이 문자열을 응답 본문으로 보내면 Prometheus가 주기적으로 pull 해 갑니다. memory_order_relaxed는 이 카운터만 정확하면 될 때 쓰면 되고, 여러 메트릭 간 순서가 중요하면 seq_cst 등을 고려합니다.
#include <atomic>
#include <string>
// 개념적 예: 단일 Counter
std::atomic<uint64_t> request_count{0};
void on_request() {
request_count.fetch_add(1, std::memory_order_relaxed);
}
std::string export_metrics() {
return "http_requests_total " + std::to_string(request_count.load()) + "\n";
}
실전: Counter·Gauge·Histogram 수동 구현
#include <atomic>
#include <string>
#include <sstream>
#include <mutex>
#include <chrono>
// 라벨별 Counter (path, method)
struct Metrics {
std::atomic<uint64_t> requests_total{0};
std::atomic<uint64_t> errors_total{0};
std::atomic<uint64_t> active_connections{0};
std::atomic<uint64_t> queue_length{0};
// Histogram 버킷: 5ms, 25ms, 100ms, 500ms, 1s, +Inf
static constexpr double buckets[] = {0.005, 0.025, 0.1, 0.5, 1.0, -1}; // -1 = +Inf
std::atomic<uint64_t> duration_bucket_5ms{0};
std::atomic<uint64_t> duration_bucket_25ms{0};
std::atomic<uint64_t> duration_bucket_100ms{0};
std::atomic<uint64_t> duration_bucket_500ms{0};
std::atomic<uint64_t> duration_bucket_1s{0};
std::atomic<uint64_t> duration_bucket_inf{0};
std::atomic<double> duration_sum{0};
std::atomic<uint64_t> duration_count{0};
void record_request(bool error, double duration_sec) {
requests_total.fetch_add(1, std::memory_order_relaxed);
if (error) errors_total.fetch_add(1, std::memory_order_relaxed);
// Histogram 버킷 누적
auto add_bucket = [this](std::atomic<uint64_t>& b) {
b.fetch_add(1, std::memory_order_relaxed);
};
if (duration_sec <= 0.005) add_bucket(duration_bucket_5ms);
else if (duration_sec <= 0.025) add_bucket(duration_bucket_25ms);
else if (duration_sec <= 0.1) add_bucket(duration_bucket_100ms);
else if (duration_sec <= 0.5) add_bucket(duration_bucket_500ms);
else if (duration_sec <= 1.0) add_bucket(duration_bucket_1s);
add_bucket(duration_bucket_inf);
double expected;
do {
expected = duration_sum.load(std::memory_order_relaxed);
} while (!duration_sum.compare_exchange_weak(
expected, expected + duration_sec, std::memory_order_relaxed));
duration_count.fetch_add(1, std::memory_order_relaxed);
}
void connection_opened() {
active_connections.fetch_add(1, std::memory_order_relaxed);
}
void connection_closed() {
active_connections.fetch_sub(1, std::memory_order_relaxed);
}
void queue_inc() { queue_length.fetch_add(1, std::memory_order_relaxed); }
void queue_dec() { queue_length.fetch_sub(1, std::memory_order_relaxed); }
std::string export_prometheus() const {
std::ostringstream out;
out << "# HELP http_requests_total Total HTTP requests\n";
out << "# TYPE http_requests_total counter\n";
out << "http_requests_total " << requests_total.load() << "\n";
out << "# HELP http_errors_total Total HTTP errors\n";
out << "# TYPE http_errors_total counter\n";
out << "http_errors_total " << errors_total.load() << "\n";
out << "# HELP http_active_connections Active connections\n";
out << "# TYPE http_active_connections gauge\n";
out << "http_active_connections " << active_connections.load() << "\n";
out << "# HELP http_queue_length Current queue length\n";
out << "# TYPE http_queue_length gauge\n";
out << "http_queue_length " << queue_length.load() << "\n";
out << "# HELP http_request_duration_seconds Request duration\n";
out << "# TYPE http_request_duration_seconds histogram\n";
out << "http_request_duration_seconds_bucket{le=\"0.005\"} " << duration_bucket_5ms.load() << "\n";
out << "http_request_duration_seconds_bucket{le=\"0.025\"} " << duration_bucket_25ms.load() << "\n";
out << "http_request_duration_seconds_bucket{le=\"0.1\"} " << duration_bucket_100ms.load() << "\n";
out << "http_request_duration_seconds_bucket{le=\"0.5\"} " << duration_bucket_500ms.load() << "\n";
out << "http_request_duration_seconds_bucket{le=\"1\"} " << duration_bucket_1s.load() << "\n";
out << "http_request_duration_seconds_bucket{le=\"+Inf\"} " << duration_bucket_inf.load() << "\n";
out << "http_request_duration_seconds_sum " << duration_sum.load() << "\n";
out << "http_request_duration_seconds_count " << duration_count.load() << "\n";
return out.str();
}
};
prometheus-cpp 라이브러리 사용 예제
#include <prometheus/counter.h>
#include <prometheus/gauge.h>
#include <prometheus/histogram.h>
#include <prometheus/registry.h>
#include <prometheus/exposer.h>
#include <memory>
int main() {
// HTTP 서버 8080 포트에서 /metrics 노출
prometheus::Exposer exposer{"127.0.0.1:8080"};
auto registry = std::make_shared<prometheus::Registry>();
// Counter: 라벨로 path, method 구분
auto& request_counter = prometheus::BuildCounter()
.Name("http_requests_total")
.Help("Total HTTP requests")
.Labels({{"service", "cpp-server"}})
.Register(*registry);
auto& get_requests = request_counter.Add({{"method", "GET"}, {"path", "/api"}});
auto& post_requests = request_counter.Add({{"method", "POST"}, {"path", "/api"}});
// Gauge: 활성 연결 수
auto& conn_gauge = prometheus::BuildGauge()
.Name("http_active_connections")
.Help("Active connections")
.Register(*registry);
// Histogram: 지연 시간 (버킷 5ms, 25ms, 100ms, 500ms, 1s)
auto& duration_hist = prometheus::BuildHistogram()
.Name("http_request_duration_seconds")
.Help("Request duration")
.Buckets({0.005, 0.025, 0.1, 0.5, 1.0})
.Register(*registry);
auto& get_duration = duration_hist.Add({{"method", "GET"}}, std::vector<double>{0.005, 0.025, 0.1, 0.5, 1.0});
exposer.RegisterCollectable(registry);
// 요청 처리 시
get_requests.Increment();
conn_gauge.Increment();
auto start = std::chrono::steady_clock::now();
// ... 요청 처리 ...
auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
get_duration.Observe(elapsed);
conn_gauge.Decrement();
return 0;
}
prometheus-cpp 빌드 및 의존성
# vcpkg로 설치 (권장)
vcpkg install prometheus-cpp
# CMakeLists.txt
find_package(prometheus-cpp CONFIG REQUIRED)
target_link_libraries(my_server prometheus::prometheus)
# 또는 FetchContent로 서브모듈 없이 빌드
include(FetchContent)
FetchContent_Declare(
prometheus-cpp
GIT_REPOSITORY https://github.com/jupp0r/prometheus-cpp.git
GIT_TAG v1.2.2
)
FetchContent_MakeAvailable(prometheus-cpp)
target_link_libraries(my_server prometheus::prometheus)
3. Prometheus 설정과 수집
prometheus.yml 기본 설정
global:
scrape_interval: 15s # 기본 수집 주기
evaluation_interval: 15s # 알림 규칙 평가 주기
alerting:
alertmanagers:
- static_configs:
- targets: []
rule_files: []
scrape_configs:
- job_name: 'cpp-server'
scrape_interval: 10s # C++ 서버는 10초마다 수집
scrape_timeout: 5s
static_configs:
- targets: ['localhost:8080']
labels:
env: 'production'
service: 'cpp-api'
동적 타겟 (서비스 디스커버리)
# Kubernetes Pod에서 C++ 서비스 스크래핑
scrape_configs:
- job_name: 'cpp-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: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: ${1}:${2}
target_label: __address__
4. Grafana 연동
데이터 소스 설정
- Prometheus를 Grafana 데이터 소스로 추가하고, PromQL로 쿼리합니다.
- URL:
http://prometheus:9090(Docker/K8s 환경) 또는http://localhost:9090
주요 PromQL 쿼리 예시
# 초당 요청 수 (RPS)
rate(http_requests_total[5m])
# p99 지연 시간 (초)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 에러율 (%)
100 * sum(rate(http_errors_total[5m])) / sum(rate(http_requests_total[5m]))
# 활성 연결 수 (Gauge는 rate 불필요)
http_active_connections
# 큐 길이
http_queue_length
대시보드 패널 구성
- 그래프: RPS, 지연 백분위수(p50, p95, p99), 에러율 시계열
- 싱글 스탯: 현재 연결 수, 큐 길이
- 테이블: path별 요청 수, method별 에러율
- 알림: p99 > 1초, 에러율 > 5% 시 Slack/이메일 알림
추가 PromQL 쿼리 (실전 활용)
# p50, p95, p99 동시 표시
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 평균 지연 시간 (sum/count)
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])
# 인스턴스별 RPS (다중 서버)
sum by (instance) (rate(http_requests_total[5m]))
# 5분간 에러 수
increase(http_errors_total[5m])
Grafana 알림 채널 설정
# Grafana 알림 채널 (Slack 예시)
# Configuration → Alerting → Contact points → New contact point
# Type: Slack
# Webhook URL: https://hooks.slack.com/services/xxx/yyy/zzz
# 채널: #alerts-cpp-server
Grafana 대시보드 변수 (인스턴스별 필터)
# Dashboard Settings → Variables → New variable
# Name: instance
# Type: Query
# Data source: Prometheus
# Query: label_values(http_requests_total, instance)
# Multi-value: Yes
# 패널 쿼리에서 사용: {instance=~"$instance"}
5. 완전한 Prometheus·Grafana 예제
Docker Compose로 전체 스택 실행
# docker-compose.yml
version: '3.8'
services:
cpp-server:
build: .
ports:
- "8080:8080"
environment:
- METRICS_PORT=8080
prometheus:
image: prom/prometheus:v2.47.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
grafana:
image: grafana/grafana:10.2.0
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
depends_on:
- prometheus
volumes:
grafana-data:
C++ 서버 + /metrics 엔드포인트 (Boost.Beast 예시)
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/asio.hpp>
#include <atomic>
#include <chrono>
#include <string>
#include <thread>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
// 전역 메트릭 (실제로는 싱글톤 또는 의존성 주입)
std::atomic<uint64_t> g_requests_total{0};
std::atomic<uint64_t> g_errors_total{0};
std::atomic<uint64_t> g_active_connections{0};
void handle_metrics(http::request<http::string_body> const& req,
http::response<http::string_body>& res) {
res.set(http::field::content_type, "text/plain; charset=utf-8");
res.body() = "# HELP http_requests_total Total requests\n"
"# TYPE http_requests_total counter\n"
"http_requests_total " + std::to_string(g_requests_total.load()) + "\n"
"# HELP http_errors_total Total errors\n"
"# TYPE http_errors_total counter\n"
"http_errors_total " + std::to_string(g_errors_total.load()) + "\n"
"# HELP http_active_connections Active connections\n"
"# TYPE http_active_connections gauge\n"
"http_active_connections " + std::to_string(g_active_connections.load()) + "\n";
res.prepare_payload();
}
// /metrics 요청 시 위 handle_metrics 호출, 그 외 경로는 비즈니스 로직
Grafana 대시보드 JSON (핵심 패널)
{
"panels": [
{
"title": "RPS (초당 요청 수)",
"type": "timeseries",
"targets": [{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{instance}}"
}]
},
{
"title": "p99 지연 시간 (초)",
"type": "timeseries",
"targets": [{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "p99"
}]
},
{
"title": "에러율 (%)",
"type": "timeseries",
"targets": [{
"expr": "100 * sum(rate(http_errors_total[5m])) / sum(rate(http_requests_total[5m]))",
"legendFormat": "error_rate"
}]
},
{
"title": "활성 연결 수",
"type": "stat",
"targets": [{
"expr": "http_active_connections",
"legendFormat": "connections"
}]
}
]
}
6. 자주 발생하는 에러와 해결법
문제 1: Prometheus가 “connection refused” 또는 “context deadline exceeded”
원인: C++ 서버의 /metrics 포트가 닫혀 있거나, 방화벽·네트워크 분리로 접근 불가
해결법:
# C++ 서버 /metrics 응답 확인
curl -v http://localhost:8080/metrics
# Prometheus가 접근 가능한지 (같은 네트워크에서)
docker exec prometheus wget -qO- http://cpp-server:8080/metrics
# prometheus.yml에서 타겟 주소 확인
# Docker: 서비스 이름 사용 (cpp-server:8080)
# K8s: Pod IP 또는 Service 이름
scrape_configs:
- job_name: 'cpp-server'
static_configs:
- targets: ['cpp-server:8080'] # Docker Compose 서비스명
문제 2: “parse error” 또는 “invalid character” in Prometheus
원인: C++에서 내보내는 텍스트 포맷이 Prometheus 규격과 다름
해결법:
# ❌ 잘못된 예: 쉼표, 공백, 이스케이프 오류
http_requests_total 1234,567
http_requests_total{path="/api"} 100 # 따옴표 이스케이프 필요할 수 있음
# ✅ 올바른 예
# HELP http_requests_total Total requests
# TYPE http_requests_total counter
http_requests_total 1234
http_requests_total{path="/api"} 100
- HELP, TYPE 주석을 각 메트릭마다 포함
- 라벨 값에
"또는\가 있으면 이스케이프 - 한 줄에 하나의 메트릭,
이름{라벨} 값또는이름 값
문제 3: Grafana에서 “No data” 또는 빈 그래프
원인: PromQL 오타, 시간 범위 문제, 메트릭 이름 불일치
해결법:
# 메트릭 존재 여부 확인
{__name__=~"http_.*"}
# Counter에 rate() 필수 (누적값만 보면 항상 증가)
rate(http_requests_total[5m])
# Histogram은 _bucket, _sum, _count 사용
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
문제 4: 라벨 카디널리티 폭발로 Prometheus 메모리 급증
원인: path, user_id 등 고유값을 라벨로 사용하면 조합 수가 폭발
해결법:
// ❌ 위험: path가 수천 개면 메트릭 수천 개
request_counter.Add({{"path", user_provided_path}});
// ✅ 안전: path를 그룹화 (예: /api/users/:id → /api/users/)
std::string normalize_path(const std::string& path) {
if (path.find("/api/users/") == 0) return "/api/users/:id";
if (path.find("/api/orders/") == 0) return "/api/orders/:id";
return path;
}
문제 5: C++에서 atomic으로 Histogram 구현 시 race condition
원인: 버킷 누적과 sum/count 업데이트가 원자적이지 않음
해결법: 위 Metrics 예제처럼 각 버킷을 atomic으로 두고, sum은 compare_exchange_weak 루프로 누적. 또는 mutex로 전체 record_request를 보호:
std::mutex metrics_mutex;
void record_request(double duration_sec) {
std::lock_guard<std::mutex> lock(metrics_mutex);
// 버킷, sum, count 업데이트
}
문제 6: /metrics 응답이 너무 느림 (수백 ms)
원인: export 시 동적 할당·문자열 조합이 많거나, 락 경합
해결법:
// ✅ export 결과를 스레드 로컬 버퍼에 캐시, 주기적으로 갱신
thread_local std::string cached_metrics;
std::atomic<uint64_t> last_export_version{0};
std::string get_metrics() {
if (metrics_version.load() != last_export_version.load()) {
cached_metrics = metrics.export_prometheus();
last_export_version = metrics_version.load();
}
return cached_metrics;
}
문제 7: Prometheus “out of order” 또는 “duplicate” 샘플
원인: 서버 재시작 시 타임스탬프가 과거로 돌아가거나, 동일 타겟을 여러 job에서 스크래핑
해결법:
# job_name 중복 확인, 하나의 타겟은 하나의 job에서만
scrape_configs:
- job_name: 'cpp-server'
static_configs:
- targets: ['cpp-server:8080']
# ❌ 같은 타겟을 다른 job에서 또 스크래핑하지 말 것
7. 모범 사례
메트릭 네이밍
- Counter:
_total접미사 (예:http_requests_total) - 단위:
_seconds,_bytes등 (예:http_request_duration_seconds) - 소문자·스네이크:
http_requests_total(camelCase 지양)
라벨 사용 원칙
- 카디널리티 제한: 라벨 조합 수를 수백 이하로 유지
- 고정된 값: env, service, region 등
- 동적 값 주의: user_id, request_id는 라벨로 쓰지 말 것
스크래핑 주기
- 애플리케이션: 10–15초
- 인프라: 30초–1분
- 비용 큰 메트릭: 1–5분
/metrics 보안
- 내부 전용: 관리 네트워크에서만 접근
- 인증: Basic Auth 또는 mTLS
- 별도 포트: 비즈니스 포트와 분리 (예: 8080 API, 9090 metrics)
메트릭 수집 시 성능 영향
| 항목 | 권장 사항 |
|---|---|
| atomic 연산 | memory_order_relaxed 사용 (단, 일관성 필요 시 seq_cst) |
| Histogram Observe | 핫 경로에서 mutex 대신 atomic 버킷 |
| export 빈도 | Prometheus가 10–15초마다 호출하므로, 호출 시 문자열 생성 비용 최소화 |
| 라벨 수 | 5개 이하, 카디널리티 100 이하 유지 |
8. 프로덕션 패턴
패턴 1: 메트릭 포트 분리
// API 서버: 8080
// 메트릭 서버: 9090 (내부망만 바인딩)
void run_metrics_server(const std::string& bind_addr, uint16_t port) {
// 0.0.0.0 대신 127.0.0.1 또는 내부 IP만 바인딩
tcp::acceptor acceptor(ctx, {net::ip::make_address(bind_addr), port});
// ...
}
패턴 2: 시작 시 메트릭 초기화
// main() 시작 시 0으로 초기화하여 "서버 기동" 시점부터 데이터 수집
void init_metrics() {
g_requests_total.store(0);
g_errors_total.store(0);
// ...
}
패턴 3: 요청 처리 래퍼로 자동 메트릭 수집
// RAII로 연결 수·지연 시간 자동 기록 (C++에는 finally 없음)
struct ScopedRequestMetrics {
Metrics& m;
std::chrono::steady_clock::time_point start;
bool error = false;
ScopedRequestMetrics(Metrics& metrics) : m(metrics), start(std::chrono::steady_clock::now()) {
m.connection_opened();
}
~ScopedRequestMetrics() {
auto dur = std::chrono::duration<double>(
std::chrono::steady_clock::now() - start).count();
m.record_request(error, dur);
m.connection_closed();
}
};
// 사용 예
void handle_request() {
ScopedRequestMetrics scope(metrics);
try {
do_work();
} catch (...) {
scope.error = true;
throw;
}
}
패턴 4: Prometheus 알림 규칙
# prometheus/alerts.yml
groups:
- name: cpp-server
rules:
- alert: HighErrorRate
expr: 100 * sum(rate(http_errors_total[5m])) / sum(rate(http_requests_total[5m])) > 5
for: 2m
labels:
severity: critical
annotations:
summary: "C++ 서버 에러율 {{ $value | humanize }}% 초과"
- alert: HighLatency
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "p99 지연 시간 1초 초과"
9. 구현 체크리스트
- C++ 서버에 /metrics 엔드포인트 구현
- Counter (요청 수, 에러 수) 노출
- Gauge (연결 수, 큐 길이) 노출
- Histogram (지연 시간) 노출, 버킷 설정
- Content-Type: text/plain; charset=utf-8 설정
- prometheus.yml에 scrape_config 추가
- Grafana에 Prometheus 데이터 소스 추가
- RPS, p99, 에러율, 연결 수 패널 구성
- 알림 규칙 설정 (에러율, 지연)
- 메트릭 포트/경로 보안 (내부 전용 또는 인증)
- 라벨 카디널리티 검토
10. 정리
| 항목 | 요약 |
|---|---|
| Prometheus | Counter/Gauge/Histogram·라벨·pull·텍스트 포맷 |
| C++ | atomic·라이브러리 또는 수동 직렬화·/metrics 엔드포인트 |
| Grafana | PromQL·대시보드·알림 |
| 프로덕션 | 포트 분리·알림 규칙·라벨 카디널리티 제한 |
43번 시리즈는 gRPC·Protobuf → 보안 코딩·OpenSSL → Observability(Prometheus·Grafana)로 대규모 분산 시스템과 보안·운영까지 다뤘습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- C++ 네트워크 에러 완벽 가이드 | errno·타임아웃·재시도·서킷브레이커 [#28-3]
- C++ 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기 [#41-1]
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
이 글에서 다루는 키워드 (관련 검색어)
Prometheus, Grafana, C++ 모니터링, prometheus-cpp, 메트릭 수집, Observability 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ 서버에서 메트릭을 노출하고 Prometheus가 수집·Grafana로 시각화하는 파이프라인을 구축하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. prometheus-cpp vs 수동 구현, 어떤 걸 쓰나요?
A. prometheus-cpp: 라벨·Histogram 등 풀 기능이 필요하고 의존성 추가가 가능하면 권장. 수동 구현: 의존성 최소화·임베디드·간단한 메트릭만 필요할 때 적합.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. Prometheus 공식 문서, Grafana 문서, prometheus-cpp GitHub를 참고하세요.
한 줄 요약: Prometheus·Grafana로 C++ 서버 메트릭을 수집·시각화할 수 있습니다. 다음으로 C++26 프리뷰(#44-1)를 읽어보면 좋습니다.
이전 글: 실전 도메인 #43-2: 보안 코딩·OpenSSL
다음 글: [C++의 미래 #44-1] C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들
관련 글
- C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전
- C++ 고성능 RPC 시스템: gRPC와 Protocol Buffers를 이용한 마이크로서비스 구축
- C++ constexpr 고급 가이드 | constexpr 컨테이너·알고리즘·문자열·new/delete 실전
- C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]
- C++ 실시간 모니터링 대시보드 | Grafana·Prometheus 통합 [#50-6]