C++可观测性实战:从日志到Metrics再到分布式追踪的全链路监控方案
“你的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仪表盘配置
- 添加Prometheus数据源(URL:
http://localhost:9090); - 导入官方仪表盘(如“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))。
- QPS:
三、分布式追踪:跟踪“请求去哪了”(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可视化追踪链路
- 启动Jaeger(Docker方式):
docker run -d --name jaeger \
-p 6831:6831/udp -p 16686:16686 \
jaegertracing/all-in-one:latest
- 访问
http://localhost:16686,在Jaeger UI中搜索Trace ID,查看完整调用链(包含每个Span的耗时、标签)。
四、可观测性“三位一体”:日志+Metrics+追踪的协同
- 日志:记录详细事件(如“用户123下单失败:库存不足”),用于事后排查;
- Metrics:监控聚合状态(如“QPS突增到1000”“内存占用达80%”),用于实时告警;
- 追踪:展示调用链路(如“请求在service_b耗时5s”),用于定位瓶颈。
协同示例:用户下单失败的全链路分析
- Metrics告警:Grafana监控到
order_failed_total突增,触发告警; - 日志定位:通过日志
spdlog::error("Order failed: user={}, reason={}", 123, "stock_empty")确认失败原因; - 追踪分析:在Jaeger中找到对应Trace,发现
service_inventory的check_stockSpan耗时3s(正常<100ms),定位库存服务性能问题。
五、总结:可观测性是系统的“体检报告”
- 日志:用spdlog(高性能异步)或glog(简单)记录关键事件,注意分级和结构化;
- Metrics:用Prometheus+Grafana监控QPS、内存等聚合指标,关注阈值告警;
- 追踪:用OpenTelemetry+Jaeger跟踪跨服务调用链,定位性能瓶颈。
行动建议:从核心服务开始,先接入日志和Metrics,再逐步叠加分布式追踪。当线上故障时,你会发现“可观测性”是比“经验”更可靠的排障工具。
系统不再是“黑盒”——日志告诉你细节,Metrics告诉你状态,追踪告诉你路径,三者结合,让问题无所遁形。
更多推荐


所有评论(0)