​“你的C++服务上线后,突然CPU飙升却找不到原因?用户反馈请求超时,却不知道卡在哪个环节?​​ 可观测性是线上系统的‘眼睛’——日志告诉你‘发生了什么’,Metrics告诉你‘状态如何’,分布式追踪告诉你‘请求去哪了’。本文将用spdlog/glog记录日志、Prometheus+Grafana监控QPS/内存、OpenTelemetry追踪调用链,帮你构建‘看得见、摸得着’的系统监控体系。”

在分布式系统中,“可观测性”是排查问题、优化性能的核心能力。传统开发只关注“功能实现”,而忽视“运行状态感知”,导致线上故障难以定位。本文聚焦日志、Metrics、分布式追踪三大支柱,通过实战代码演示如何用C++工具链构建全链路可观测性。

一、日志:记录“发生了什么”

日志是可观测性的基础,用于记录程序运行中的关键事件(如错误、状态变更、业务流程)。C++常用日志库有spdlog​(高性能异步)和glog​(Google出品,简单易用)。

1.1 日志库对比:spdlog vs glog

特性 spdlog glog
定位 高性能异步日志库(MIT协议,轻量灵活) Google开源日志库(简单易用,分级日志)
核心优势 异步写入(不阻塞主线程)、多sinks(控制台/文件/网络)、格式化强 自动分级(INFO/WARNING/ERROR)、崩溃时生成堆栈dump
依赖 单头文件或轻量库(无外部依赖) 依赖Google基础库(如gflags)

1.2 spdlog实战:异步日志与多Sink输出

spdlog的核心是“异步日志”——日志先写入内存缓冲区,后台线程批量刷盘/输出,避免主线程阻塞。

(1)安装与初始化
# 安装spdlog(vcpkg或源码)
vcpkg install spdlog

初始化日志器(控制台+文件双输出,异步模式):

#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/async.h>  // 异步日志支持

void init_logger() {
    // 1. 创建控制台sink(输出到终端)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console_sink->set_level(spdlog::level::info);  // 控制台只输出INFO及以上

    // 2. 创建文件sink(输出到文件,按天滚动)
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log", true);
    file_sink->set_level(spdlog::level::debug);  // 文件输出DEBUG及以上

    // 3. 组合sinks,创建异步日志器(队列大小10万条,单线程后台刷新)
    spdlog::init_thread_pool(100000, 1);  // 线程池:队列大小+后台线程数
    auto logger = std::make_shared<spdlog::async_logger>(
        "app_logger",  // 日志器名称
        spdlog::sinks_init_list{console_sink, file_sink},  // 绑定sinks
        spdlog::thread_pool(),  // 使用线程池
        spdlog::async_overflow_policy::block  // 队列满时阻塞(避免丢日志)
    );

    // 4. 设置为全局默认日志器
    spdlog::set_default_logger(logger);
    spdlog::set_level(spdlog::level::debug);  // 全局日志级别(DEBUG/INFO/WARN/ERR)
    spdlog::flush_on(spdlog::level::warn);  // WARN及以上级别自动刷盘
}
(2)日志记录示例
// 不同级别日志(DEBUG/INFO/WARN/ERROR/CRITICAL)
spdlog::debug("Debug message: user_id={}, action={}", 123, "login");  // 调试用,发布时可关闭
spdlog::info("User {} logged in", "alice");  // 常规事件
spdlog::warn("Disk space low: {}% remaining", 10);  // 警告
spdlog::error("Failed to connect to DB: {}", "timeout");  // 错误(需关注)
spdlog::critical("Service crashed!");  // 致命错误(触发告警)

// 结构化日志(JSON格式,便于日志系统解析)
spdlog::info(R"({{"event":"order_created","order_id":456,"amount":99.9}})");

1.3 glog实战:简单场景的快速集成

glog适合快速上手,无需复杂配置,自动处理日志分级和文件滚动。

(1)安装与初始化
vcpkg install glog

初始化(默认输出到/tmp/<program>.<hostname>.<user>.log.<severity_level>.<date>.<time>.<pid>):

#include <glog/logging.h>

int main(int argc, char* argv[]) {
    google::InitGoogleLogging(argv[0]);  // 初始化glog(传入程序名)
    FLAGS_log_dir = "./logs";  // 日志目录(需手动创建)
    FLAGS_minloglevel = google::INFO;  // 最低日志级别(INFO及以上)
    FLAGS_logtostderr = false;  // 不输出到控制台(仅文件)
    FLAGS_alsologtostderr = true;  // 同时输出到控制台(调试用)
    google::InstallFailureSignalHandler();  // 崩溃时生成堆栈dump
}
(2)日志记录示例
LOG(INFO) << "User " << "alice" << " logged in";  // 等效spdlog::info
LOG(WARNING) << "Disk space low: " << 10 << "% remaining";
LOG(ERROR) << "Failed to connect to DB: " << "timeout";
CHECK_NE(db_conn, nullptr) << "DB connection is null";  // 条件检查(失败则LOG(FATAL))

二、Metrics:监控“状态如何”(Prometheus+Grafana)

Metrics是聚合统计数据​(如QPS、内存占用、接口耗时),用于监控系统健康状态和性能趋势。核心工具链:​Prometheus​(时序数据库,拉取metrics)+ ​Grafana​(可视化仪表盘)。

2.1 核心概念

  • 时序数据库​:按“时间戳+指标名+标签”存储数据(如http_requests_total{method="GET", status="200"} 100);
  • Metrics类型​:
    • Counter​:累加指标(如QPS、错误总数);
    • Gauge​:瞬时值(如内存占用、在线用户数);
    • Histogram​:分布统计(如接口耗时分布,计算P95/P99)。

2.2 C++集成Prometheus:用prometheus-cpp库

prometheus-cpp是轻量级客户端库,支持暴露metrics端点(如/metrics),供Prometheus拉取。

(1)安装与初始化
vcpkg install prometheus-cpp

创建metrics暴露器(HTTP服务):

#include <prometheus/exposer.h>
#include <prometheus/registry.h>
#include <prometheus/counter.h>
#include <prometheus/gauge.h>
#include <prometheus/histogram.h>

void init_metrics() {
    // 1. 创建exposer(暴露/metrics端点,默认端口9180)
    prometheus::Exposer exposer{"0.0.0.0:9180"};

    // 2. 创建注册表(存储所有metrics)
    auto registry = std::make_shared<prometheus::Registry>();

    // 3. 定义metrics(示例:QPS计数器、内存占用 gauge、接口耗时直方图)
    auto& request_counter = prometheus::BuildCounter()
        .Name("http_requests_total")
        .Help("Total HTTP requests")
        .Register(*registry)
        .Add({{"method", "GET"}, {"endpoint", "/api/data"}});  // 标签区分维度

    auto& memory_gauge = prometheus::BuildGauge()
        .Name("memory_usage_bytes")
        .Help("Current memory usage")
        .Register(*registry);

    auto& latency_histogram = prometheus::BuildHistogram()
        .Name("http_request_duration_seconds")
        .Help("HTTP request latency distribution")
        .Register(*registry)
        .Add({}, prometheus::Histogram::BucketBoundaries{0.1, 0.5, 1, 2, 5});  // 桶边界(秒)

    // 4. 将注册表绑定到exposer(Prometheus会拉取/metrics)
    exposer.RegisterCollectable(registry);
}
(2)在业务中更新Metrics
// 记录HTTP请求(QPS计数器+耗时直方图)
void handle_http_request(const std::string& method, const std::string& endpoint, double duration_sec) {
    request_counter.Add({{"method", method}, {"endpoint", endpoint}});  // Counter累加
    latency_histogram.With({{"method", method}, {"endpoint", endpoint}}).Observe(duration_sec);  // Histogram记录耗时
}

// 更新内存占用(Gauge设为瞬时值)
void update_memory_usage(size_t bytes) {
    memory_gauge.Set(static_cast<double>(bytes));
}

2.3 Prometheus+Grafana可视化

(1)Prometheus配置(prometheus.yml)
global:
  scrape_interval: 15s  # 每15秒拉取一次metrics
scrape_configs:
  - job_name: 'c++_service'
    static_configs:
      - targets: ['localhost:9180']  # 目标服务的metrics端点
(2)Grafana仪表盘配置
  1. 添加Prometheus数据源(URL:http://localhost:9090);
  2. 导入官方仪表盘(如“Prometheus 2.0 Stats”)或自定义面板:
    • QPS​:rate(http_requests_total[1m])(1分钟内平均QPS);
    • 内存占用​:memory_usage_bytes
    • 接口耗时P95​:histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[1m])) by (le))

三、分布式追踪:跟踪“请求去哪了”(OpenTelemetry)

分布式追踪用于记录跨服务请求的完整调用链​(如“网关→服务A→服务B”),定位瓶颈(如哪个服务耗时最长)。核心工具链:​OpenTelemetry​(追踪标准)+ ​Jaeger/Zipkin​(追踪数据存储与可视化)。

3.1 核心概念

  • Trace​:完整调用链(如一次用户请求);
  • Span​:调用链中的一段(如“服务A处理请求”),包含开始/结束时间、标签(如service.name);
  • Context​:传递追踪上下文(如通过HTTP头traceparent),确保跨服务链路关联。

3.2 OpenTelemetry C++ SDK实战

OpenTelemetry是CNCF标准,支持多种语言,C++ SDK可生成追踪数据并导出到Jaeger。

(1)安装与初始化
vcpkg install opentelemetry-cpp opentelemetry-ext-jaeger

初始化Tracer(追踪器)和Exporter(导出器,发送到Jaeger):

#include <opentelemetry/trace/tracer.h>
#include <opentelemetry/exporters/jaeger/jaeger_exporter.h>
#include <opentelemetry/sdk/trace/simple_processor.h>
#include <opentelemetry/sdk/trace/tracer_provider.h>

void init_tracing() {
    // 1. 创建Jaeger导出器(发送到Jaeger Agent,默认端口6831)
    auto exporter = std::unique_ptr<opentelemetry::sdk::trace::SpanExporter>(
        new opentelemetry::exporter::jaeger::JaegerExporter(
            opentelemetry::exporter::jaeger::JaegerExporterOptions{
                .agent_host = "localhost", .agent_port = 6831}));

    // 2. 创建处理器(SimpleProcessor:批量导出)
    auto processor = std::unique_ptr<opentelemetry::sdk::trace::SpanProcessor>(
        new opentelemetry::sdk::trace::SimpleSpanProcessor(std::move(exporter)));

    // 3. 创建TracerProvider(管理Tracer)
    auto provider = opentelemetry::nostd::shared_ptr<opentelemetry::trace::TracerProvider>(
        new opentelemetry::sdk::trace::TracerProvider(std::move(processor)));
    opentelemetry::trace::Provider::SetTracerProvider(provider);

    // 4. 获取Tracer(命名为服务名)
    auto tracer = provider->GetTracer("c++_service", OPENTELEMETRY_VERSION);
}
(2)创建Span跟踪请求链路
// 模拟网关调用服务A,服务A调用服务B
void service_b(opentelemetry::trace::SpanContext parent_context) {
    auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("service_b");
    auto span = tracer->StartSpan("service_b_handle", {{opentelemetry::trace::SpanKind::kServer}}, parent_context);
    // 业务逻辑...
    span->End();  // 结束span
}

void service_a(opentelemetry::trace::SpanContext parent_context) {
    auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("service_a");
    auto span = tracer->StartSpan("service_a_handle", {{opentelemetry::trace::SpanKind::kServer}}, parent_context);
    
    // 调用服务B(传递上下文)
    auto child_context = span->GetContext();
    service_b(child_context);
    
    span->End();
}

void gateway() {
    auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("gateway");
    auto span = tracer->StartSpan("gateway_handle_request");  // 根span
    
    // 调用服务A(传递上下文)
    auto child_context = span->GetContext();
    service_a(child_context);
    
    span->End();  // 结束根span(完整trace结束)
}

3.3 Jaeger可视化追踪链路

  1. 启动Jaeger(Docker方式):
docker run -d --name jaeger \
  -p 6831:6831/udp -p 16686:16686 \
  jaegertracing/all-in-one:latest
  1. 访问http://localhost:16686,在Jaeger UI中搜索Trace ID,查看完整调用链(包含每个Span的耗时、标签)。

四、可观测性“三位一体”:日志+Metrics+追踪的协同

  • 日志​:记录详细事件(如“用户123下单失败:库存不足”),用于事后排查;
  • Metrics​:监控聚合状态(如“QPS突增到1000”“内存占用达80%”),用于实时告警;
  • 追踪​:展示调用链路(如“请求在service_b耗时5s”),用于定位瓶颈。

协同示例:用户下单失败的全链路分析

  1. Metrics告警​:Grafana监控到order_failed_total突增,触发告警;
  2. 日志定位​:通过日志spdlog::error("Order failed: user={}, reason={}", 123, "stock_empty")确认失败原因;
  3. 追踪分析​:在Jaeger中找到对应Trace,发现service_inventorycheck_stock Span耗时3s(正常<100ms),定位库存服务性能问题。

五、总结:可观测性是系统的“体检报告”

  • 日志​:用spdlog(高性能异步)或glog(简单)记录关键事件,注意分级和结构化;
  • Metrics​:用Prometheus+Grafana监控QPS、内存等聚合指标,关注阈值告警;
  • 追踪​:用OpenTelemetry+Jaeger跟踪跨服务调用链,定位性能瓶颈。

行动建议​:从核心服务开始,先接入日志和Metrics,再逐步叠加分布式追踪。当线上故障时,你会发现“可观测性”是比“经验”更可靠的排障工具。

系统不再是“黑盒”——日志告诉你细节,Metrics告诉你状态,追踪告诉你路径,三者结合,让问题无所遁形。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐