C++ Observability: Prometheus와 Grafana로 C++ 서버 모니터링 구축하기

C++ Observability: Prometheus와 Grafana로 C++ 서버 모니터링 구축하기

이 글의 핵심

C++ 서버에서 메트릭을 노출하고 Prometheus가 수집·Grafana로 시각화하는 파이프라인을 구축하는 방법을 다룹니다. 문제 시나리오, 완전한 예제, 일반적인 에러, 프로덕션 패턴까지 C++ 실전 가이드 시리즈에서 다룹니다.

들어가며: “왜 느려졌는지” 데이터로 보기

지표가 있어야 대응할 수 있다

43-1, 43-2에서 RPC·보안을 다뤘다면, 운영 단계에서는 지표(메트릭)(요청 수, 지연 시간, CPU 사용률 등 측정값)가 필수입니다. Prometheuspull 방식(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 파이프라인과 완전한 예제를 다룹니다.

목차

  1. Prometheus 메트릭
  2. C++에서 메트릭 노출
  3. Prometheus 설정과 수집
  4. Grafana 연동
  5. 완전한 Prometheus·Grafana 예제
  6. 자주 발생하는 에러와 해결법
  7. 모범 사례
  8. 프로덕션 패턴
  9. 구현 체크리스트
  10. 정리

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


1. Prometheus 메트릭

Counter·Gauge·Histogram

  • Counter: 단조 증가하는 값(요청 수, 바이트 전송량). rate()로 초당 증가량을 뽑습니다.
  • Gauge: 올라갔다 내려갔다 하는 값(연결 수, 큐 길이, 메모리 사용량).
  • Histogram: 분포(지연 시간). 버킷총합·카운트를 노출하고, Prometheus에서 histogram_quantile로 백분위수를 계산합니다.
  • 라벨: 메트릭 이름에 라벨(예: method, path, status)을 붙이면 필터·그룹화가 가능합니다. 라벨 조합이 폭발하지 않도록 카디널리티를 제한하는 것이 좋습니다.

Histogram 버킷 선택 가이드

서비스 유형권장 버킷 (초)설명
저지연 API0.001, 0.005, 0.01, 0.025, 0.05, 0.1ms 단위 지연 측정
일반 API0.005, 0.025, 0.1, 0.5, 1.0, 2.5REST/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 연동

데이터 소스 설정

  • PrometheusGrafana 데이터 소스로 추가하고, 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. 정리

항목요약
PrometheusCounter/Gauge/Histogram·라벨·pull·텍스트 포맷
C++atomic·라이브러리 또는 수동 직렬화·/metrics 엔드포인트
GrafanaPromQL·대시보드·알림
프로덕션포트 분리·알림 규칙·라벨 카디널리티 제한

43번 시리즈는 gRPC·Protobuf보안 코딩·OpenSSLObservability(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]