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

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

43-1, 43-2에서 RPC와 보안을 다뤘다면, 운영 단계에서는 요청 수·지연 시간·에러 수 같은 지표(메트릭)가 있어야 문제에 대응할 수 있습니다. Prometheus는 pull 방식(Prometheus 서버가 타겟에 요청해 메트릭을 가져오는 방식)으로 타겟의 HTTP 엔드포인트에서 메트릭을 수집하며, Grafana로 대시보드를 만들어 시각화합니다.
C++ 서버에서 Prometheus 형식으로 메트릭을 노출하려면: Counter·Gauge·Histogram을 정의하며, /metrics 같은 경로에서 텍스트로 내보내면 됩니다. prometheus-cpp 같은 라이브러리를 쓰거나, 간단한 포맷만 직접 구현할 수 있습니다.

메트릭이 없으면 흔한 장애 상황에서 손을 쓰기 어렵습니다. 밤사이 응답 지연이 급증해도 로그만으로는 요청 수·지연 분포·에러율의 추이를 볼 수 없고, 메모리가 며칠에 걸쳐 서서히 오르는 문제도 힙 사용량·연결 수·큐 길이를 시간별로 기록해 두지 않으면 재시작으로 덮고 넘어가게 됩니다. 전체 에러율은 낮은데 특정 엔드포인트만 실패하는 경우는 경로별 라벨이 있어야 드러나고, 배포 후 성능 회귀는 배포 전후의 p99 지연을 비교할 Histogram이 있어야 판단할 수 있습니다.

Prometheus 메트릭

Counter·Gauge·Histogram

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

Histogram의 _bucket 값은 누적 카운트입니다. le="0.1" 버킷은 0.1초 이하인 관측값 전체 개수이므로, 3ms짜리 요청은 le="0.005"부터 +Inf까지 모든 버킷을 올립니다.

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장시간 작업

histogram_quantile은 버킷 안에서 선형 보간으로 백분위를 추정하므로, 경계가 실제 지연 분포와 맞지 않으면 결과가 부정확해집니다. SLO 기준값(예: 목표 응답 시간 300ms) 근처에 버킷을 촘촘히 두고, 먼 구간은 듬성듬성 둡니다. 버킷 하나하나가 별도 시계열이라 라벨 조합 수와 곱해져 저장 비용이 늘어난다는 점도 함께 고려합니다.

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

C++에서 메트릭 노출

라이브러리 vs 수동

  • prometheus-cpp: Counter/Gauge/Histogram을 등록하면 라이브러리가 /metrics 응답을 만들어 줍니다. 메트릭 객체의 갱신 연산은 스레드 안전하므로 여러 스레드에서 바로 호출할 수 있습니다. 라벨이 많거나 Histogram이 여러 개라면 이쪽이 편합니다.
  • 수동: 의존성을 늘리기 어렵거나 메트릭이 몇 개뿐일 때 적합합니다. 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>
#include <array>
struct Metrics {
    std::atomic<uint64_t> requests_total{0};
    std::atomic<uint64_t> errors_total{0};
    std::atomic<uint64_t> in_flight_requests{0};
    std::atomic<uint64_t> queue_length{0};
    // Histogram 버킷 경계(초). +Inf 버킷은 count와 같으므로 따로 두지 않음
    static constexpr std::array<double, 5> bounds{0.005, 0.025, 0.1, 0.5, 1.0};
    std::array<std::atomic<uint64_t>, 5> buckets{};  // 누적(le) 카운트
    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);
        // Prometheus 버킷은 누적: 경계 이상인 모든 버킷을 올림
        for (size_t i = 0; i < bounds.size(); ++i) {
            if (duration_sec <= bounds[i]) buckets[i].fetch_add(1, std::memory_order_relaxed);
        }
        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 request_started()  { in_flight_requests.fetch_add(1, std::memory_order_relaxed); }
    void request_finished() { in_flight_requests.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_in_flight_requests Requests currently being processed\n";
        out << "# TYPE http_in_flight_requests gauge\n";
        out << "http_in_flight_requests " << in_flight_requests.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";
        for (size_t i = 0; i < bounds.size(); ++i) {
            out << "http_request_duration_seconds_bucket{le=\"" << bounds[i] << "\"} " << buckets[i].load() << "\n";
        }
        // +Inf 버킷 = 전체 관측 수
        out << "http_request_duration_seconds_bucket{le=\"+Inf\"} " << duration_count.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();
    }
};

std::atomic<double>의 fetch_add는 C++20부터 있으므로, 위 코드는 C++17에서도 동작하도록 compare-exchange 루프로 합계를 누적합니다. 버킷·sum·count를 따로 갱신하므로 스크레이프 순간에 셋이 아주 약간 어긋날 수 있지만, Prometheus의 rate 계산에서는 문제 되지 않습니다.

prometheus-cpp 라이브러리 사용 예제

#include <prometheus/counter.h>
#include <prometheus/gauge.h>
#include <prometheus/histogram.h>
#include <prometheus/registry.h>
#include <prometheus/exposer.h>
#include <chrono>
#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: 버킷 경계는 시계열을 Add할 때 지정 (5ms, 25ms, 100ms, 500ms, 1s)
    auto& duration_hist = prometheus::BuildHistogram()
        .Name("http_request_duration_seconds")
        .Help("Request duration")
        .Register(*registry);
    auto& get_duration = duration_hist.Add({{"method", "GET"}},
        prometheus::Histogram::BucketBoundaries{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();
    // 실제 서버라면 여기서 요청 처리 루프를 돌며 exposer가 계속 /metrics를 응답함
    return 0;
}

prometheus-cpp 빌드 및 의존성

# vcpkg로 설치
vcpkg install prometheus-cpp
# CMakeLists.txt: core는 메트릭 타입, pull은 Exposer(HTTP /metrics)
find_package(prometheus-cpp CONFIG REQUIRED)
target_link_libraries(my_server PRIVATE prometheus-cpp::core prometheus-cpp::pull)
# 또는 FetchContent로 소스에서 빌드 (서드파티 서브모듈 civetweb 등도 함께 받음)
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 PRIVATE prometheus-cpp::core prometheus-cpp::pull)

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__

Grafana 연동

데이터 소스 설정

  • Prometheus를 Grafana 데이터 소스로 추가하며, PromQL로 쿼리합니다.
  • URL: http://prometheus:9090 (Docker/K8s 환경) 또는 http://localhost:9090

주요 PromQL 쿼리 예시

# 초당 요청 수 (RPS)
rate(http_requests_total[5m])
# p99 지연 시간 (초): 인스턴스가 여럿이면 le별로 합친 뒤 계산
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
# 에러율 (%)
100 * sum(rate(http_errors_total[5m])) / sum(rate(http_requests_total[5m]))
# 진행 중 요청 수 (Gauge는 rate 불필요)
http_in_flight_requests
# 큐 길이
http_queue_length

대시보드 패널 구성

  • 그래프: RPS, 지연 백분위수(p50, p95, p99), 에러율 시계열
  • 싱글 스탯: 진행 중 요청 수, 큐 길이
  • 테이블: path별 요청 수, method별 에러율
  • 알림: p99 > 1초, 에러율 > 5% 시 Slack/이메일 알림

추가 PromQL 쿼리 (실전 활용)

# p50, p95, p99 동시 표시
histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.99, sum by (le) (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"}

Docker Compose 스택, Beast /metrics 서버, 대시보드 JSON

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, sum by (le) (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"
      }]
    }
  ]
}

스크래핑 실패, 파싱 에러, No data, 라벨 카디널리티 폭발

문제 1: Prometheus가 “connection refused” 또는 “context deadline exceeded”

C++ 서버의 /metrics 포트가 닫혀 있거나, 방화벽·네트워크 분리로 Prometheus가 접근하지 못하는 경우입니다. 서버를 127.0.0.1에만 바인딩했다면 다른 컨테이너에서는 접근할 수 없다는 점도 확인합니다.

# 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으로 두면 데이터 레이스는 없고, 어긋남은 다음 스크레이프에서 해소되므로 대부분 허용됩니다. 완전한 일관성이 필요하면 mutex로 갱신과 export를 함께 보호합니다.

std::mutex metrics_mutex;
void record_request(double duration_sec) {
    std::lock_guard<std::mutex> lock(metrics_mutex);
    // 버킷, sum, count 업데이트
}

문제 6: /metrics 응답이 느림

export 비용은 노출하는 시계열 수에 비례하므로, 응답이 느리다면 먼저 라벨 카디널리티가 커지지 않았는지 확인합니다. 그다음으로 export 중에 갱신 경로와 같은 락을 오래 잡고 있지 않은지 봅니다. 스크레이프는 보통 10~15초에 한 번이라, 시계열 수가 적당하다면 매번 문자열을 새로 만들어도 부담이 크지 않습니다.

문제 7: Prometheus “out of order” 또는 “duplicate” 샘플

Prometheus는 보통 스크레이프 시각을 샘플 타임스탬프로 쓰므로, 애플리케이션이 명시적인 타임스탬프를 붙이지 않는 한 재시작만으로 순서가 뒤바뀌지는 않습니다(카운터가 0으로 돌아가는 것은 rate()가 리셋으로 처리합니다). 이 에러는 한 응답 안에 같은 이름·라벨 조합의 샘플을 두 번 내보냈거나, 같은 타겟을 같은 라벨로 여러 job에서 수집하거나, 직접 붙인 타임스탬프가 과거로 돌아갈 때 주로 생깁니다.

# job_name 중복 확인, 하나의 타겟은 하나의 job에서만
scrape_configs:
  - job_name: 'cpp-server'
    static_configs:
      - targets: ['cpp-server:8080']
  # ❌ 같은 타겟을 다른 job에서 또 스크래핑하지 말 것

메트릭 네이밍, 라벨 원칙, 스크래핑 주기, /metrics 보안

메트릭 네이밍

  • Counter: _total 접미사 (예: http_requests_total)
  • 단위: _seconds, _bytes 등 (예: http_request_duration_seconds)
  • 소문자·스네이크: http_requests_total (camelCase 지양)

라벨 사용 원칙

  • 카디널리티 제한: 라벨 값의 종류가 고정된 작은 집합인지 확인
  • 고정된 값: env, service, region 등
  • 동적 값 주의: user_id, request_id는 라벨로 쓰지 말 것

스크래핑 주기

애플리케이션 메트릭은 흔히 1015초, 인프라 메트릭은 30초1분 간격으로 수집합니다. 주기가 짧을수록 저장량이 늘고, rate()의 범위는 최소한 스크레이프 주기의 몇 배 이상으로 잡아야 값이 안정적입니다.

/metrics 보안

  • 내부 전용: 관리 네트워크에서만 접근
  • 인증: Basic Auth 또는 mTLS
  • 별도 포트: 비즈니스 포트와 분리 (예: 8080 API, 8081 metrics; 9090은 Prometheus 서버 자신의 기본 포트라 피함)

메트릭 수집 시 성능 영향

항목권장 사항
atomic 연산memory_order_relaxed 사용 (단, 일관성 필요 시 seq_cst)
Histogram Observe핫 경로에서 mutex 대신 atomic 버킷
export 빈도Prometheus가 10–15초마다 호출하므로, 호출 시 문자열 생성 비용 최소화
라벨값의 종류가 작은 고정 집합인 라벨만 사용

메트릭 포트 분리, 요청 래퍼 자동 수집, 알림 규칙

패턴 1: 메트릭 포트 분리

// API 서버: 8080
// 메트릭 서버: 8081 (내부망만 바인딩)
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: 요청 처리 래퍼로 자동 메트릭 수집

// RAII로 진행 중 요청 수·지연 시간 자동 기록 (예외가 나도 소멸자에서 기록)
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.request_started();
    }
    ~ScopedRequestMetrics() {
        auto dur = std::chrono::duration<double>(
            std::chrono::steady_clock::now() - start).count();
        m.record_request(error, dur);
        m.request_finished();
    }
};
// 사용 예
void handle_request() {
    ScopedRequestMetrics scope(metrics);
    try {
        do_work();
    } catch (...) {
        scope.error = true;
        throw;
    }
}

패턴 3: 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, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "p99 지연 시간 1초 초과"

같이 보면 좋은 글


다음으로 C++26 프리뷰(#44-1)를 읽어 보면 좋습니다. 이전 글: 실전 도메인 #43-2: 보안 코딩·OpenSSL 다음 글: [C++의 미래 #44-1] C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들