1. 项目概述:为什么现代C++服务需要全链路可观测性?
如果你正在维护一个用C++写的后端服务,无论是高频交易引擎、游戏服务器还是音视频处理服务,我猜你肯定遇到过这样的场景:半夜被报警电话叫醒,线上服务响应时间飙升,CPU占用率异常,但登录服务器一看,日志文件里除了几条不痛不痒的警告,什么关键线索都没有。你只能凭经验,在几十万行代码里大海捞针,试图重现那个只在生产环境出现的幽灵问题。这种无力感,我经历过太多次了。
这就是我们今天要聊的“可观测性”(Observability)。它早已不是“有日志就行”那么简单。对于一个复杂的分布式C++服务,你需要的是从单点深入到全局、从现象追溯到根源的能力。简单来说,可观测性让你能回答三个核心问题:我的系统现在正在发生什么?为什么会出现这个问题?接下来会怎样?
传统的C++开发,往往把重心放在性能和算法上,日志(Logging)被当作事后排查的“黑匣子”。但在微服务、云原生的架构下,一次用户请求可能流经十几个用C++、Go、Java混合编写的服务。一个慢查询,可能是C++服务内部锁竞争,也可能是下游Go服务超时,或者是Redis缓存命中率下降。没有全链路的视角,你就像在玩一个没有地图的迷宫游戏。
因此,一个完整的C++可观测性方案,必须构建三大支柱:日志(Logs)、指标(Metrics)和分布式追踪(Distributed Tracing),也就是常说的“三大支柱”。日志告诉你“发生了什么”,是离散的事件记录;指标告诉你“整体状况如何”,是聚合的时间序列数据;分布式追踪则告诉你“一次请求的完整旅程”,将跨服务的调用串联起来。
这个实战方案的目标,就是带你一步步,在一个真实的C++服务中,从零搭建起这套体系。我们不只讲理论,更聚焦于落地:选用哪些轻量又强大的库?如何以最小性能损耗接入?日志怎么结构化输出才能被高效分析?指标如何定义才有业务价值?分布式追踪的上下文如何在C++的线程、协程间无损传递?这些才是真正卡住项目脖子的细节。
2. 核心组件选型与架构设计
搭建可观测性体系,第一步不是写代码,而是选对工具。工具选型决定了后续开发的复杂度、系统性能开销以及最终的运维体验。我们的核心原则是:轻量级、高性能、与云原生生态无缝集成。
2.1 日志库:spdlog 为何是C++社区的事实标准?
在C++的世界里,日志库的选择一度很混乱。是继续用printf加文件操作?还是引入庞大的log4cxx?现在,答案非常明确:spdlog。它几乎成了现代C++项目日志的默认选择,原因很实在:
- 极致的性能:spdlog在设计上就为性能而生。它采用异步日志模式作为默认选项,日志消息被放入一个无锁队列,由后台线程批量写入文件或网络,这确保了即使在高频日志输出时,也不会阻塞主业务线程。其格式化速度在主流库中也是最快的之一。
- 丰富的特性:支持多线程、多日志器(logger)、多种接收器(sink,如控制台、滚动文件、系统日志、TCP等)、日志级别、格式自定义,功能非常全面。
- 头文件库:只需包含头文件即可使用,无需编译复杂的依赖库,集成成本极低。
- 活跃的社区:生态活跃,遇到问题容易找到解决方案。
实操心得:生产环境配置要点直接使用默认的异步日志器(async_logger)是明智的。但关键参数需要调整:
// 创建一个异步日志器,指向按日滚动的文件 auto daily_sink = std::make_shared<spdlog::sinks::daily_file_sink_mt>("logs/app.log", 0, 0); auto async_logger = std::make_shared<spdlog::async_logger>("main_logger", daily_sink, spdlog::thread_pool(), spdlog::async_overflow_policy::block); spdlog::register_logger(async_logger); spdlog::set_level(spdlog::level::info); // 生产环境通常设为info spdlog::flush_on(spdlog::level::err); // 遇到错误立即刷新,确保关键信息不丢失这里有几个坑要注意:
- 队列大小:异步日志器的队列默认大小可能不够。如果日志量暴增导致队列满,根据
async_overflow_policy策略,可能会丢弃日志(overrun_oldest)或阻塞(block)。生产环境建议监控队列使用情况,并适当增大队列尺寸。 - 格式化开销:避免在日志语句中进行昂贵的字符串拼接或计算。使用spdlog提供的格式化语法,它只在日志级别满足时才会进行实际的格式化操作。
- 结构化日志:这是关键!不要再用纯文本了。输出JSON格式的日志,便于后续的日志分析系统(如ELK)进行字段解析和检索。
SPDLOG_LOGGER_INFO(logger, "User login", "user_id"_a=12345, "ip"_a="10.0.0.1", "result"_a="success"); // 输出类似:{"timestamp":"...", "level":"info", "message":"User login", "user_id":12345, "ip":"10.0.0.1", "result":"success"}2.2 指标(Metrics)库:Prometheus Client C++ 的集成之道
指标用于衡量系统的健康度和性能,如请求数、错误率、响应时长分位数、内存使用量等。Prometheus 已经成为云原生监控的事实标准,其拉取(Pull)模型非常适合动态的微服务环境。
对于C++,官方维护的prometheus-cpp库是我们的首选。它提供了四种核心指标类型:
- Counter(计数器):只增不减,如总请求数、总错误数。
- Gauge(仪表盘):可增可减,如当前连接数、内存使用量。
- Histogram(直方图):用于统计样本分布,特别是计算分位数(如P95, P99延迟),它会自动生成
_sum,_count,_bucket等多个时间序列。 - Summary(摘要):客户端计算分位数,与Histogram类似但计算在客户端,适用于不能容忍Prometheus服务端计算延迟的场景。
架构设计:暴露HTTP端点C++服务需要启动一个HTTP服务器,暴露一个/metrics端点。Prometheus Server会定期来这个端点拉取数据。我们可以使用一个轻量级的HTTP库(如cpp-httplib)来快速实现这个端点。
关键实现细节:
#include <prometheus/exposer.h> #include <prometheus/registry.h> #include <prometheus/counter.h> #include <prometheus/histogram.h> // 创建全局的注册表和指标 auto registry = std::make_shared<prometheus::Registry>(); auto& request_counter = prometheus::BuildCounter() .Name("http_requests_total") .Help("Total HTTP requests") .Register(*registry) .Add({{"method", "GET"}, {"endpoint", "/api/v1/data"}}); auto& latency_histogram = prometheus::BuildHistogram() .Name("http_request_duration_seconds") .Help("HTTP request latency in seconds") .Register(*registry) .Add({{"method", "GET"}, {"endpoint", "/api/v1/data"}}, prometheus::Histogram::BucketBoundaries{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0}); // 在请求处理中更新指标 void handle_request() { request_counter.Increment(); auto start = std::chrono::steady_clock::now(); // ... 处理业务逻辑 ... auto end = std::chrono::steady_clock::now(); std::chrono::duration<double> duration = end - start; latency_histogram.Observe(duration.count()); } // 使用Exposer暴露/metrics端点 prometheus::Exposer exposer{"0.0.0.0:8080"}; exposer.RegisterCollectable(registry);注意:指标标签(Label)的设计至关重要。标签过多会导致时间序列爆炸,给Prometheus服务器带来巨大压力。标签过少又无法进行有效的维度下钻分析。一个好的实践是,为指标添加固定的、有业务区分度的标签,如
service_name,instance,api_endpoint,http_method, 避免使用用户ID、请求ID等高基数字段作为标签。
2.3 分布式追踪:OpenTelemetry C++ SDK 的接入实践
分布式追踪是理解跨服务调用链的利器。OpenTelemetry(OTel)是目前CNCF旗下追踪、指标、日志的统一标准,其C++ SDK虽然相对年轻,但已是构建未来可观测性体系的基础。
核心概念:Trace, Span, Context
- Trace(追踪):代表一个完整的事务或请求流程,由一个全局唯一的
TraceId标识。 - Span(跨度):代表一个工作单元,如一次函数调用、一次数据库查询。一个Trace由多个Span组成树状结构。每个Span有自己的
SpanId和父SpanId。 - Context(上下文):承载
TraceId和SpanId等信息,需要在服务间和进程内(跨线程)传递。
集成方案:手动插桩与自动插桩
- 手动插桩:在代码的关键路径上,显式地创建和结束Span。这种方式灵活、精准,但代码侵入性强。
#include <opentelemetry/trace/provider.h> auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("my_service"); void handle_core_function() { auto span = tracer->StartSpan("core_operation"); auto scope = tracer->WithActiveSpan(span); // 将此Span设为当前活跃Span // ... 业务逻辑 ... // 子操作会自动成为当前Span的子Span call_database(); span->End(); } - 自动插桩:通过包装或拦截通用组件(如HTTP客户端/服务器、gRPC、数据库驱动)来自动生成Span。这是更理想的方式,OTel社区提供了对常见库(如libcurl、gRPC)的插件支持。你需要链接相应的插件库,并进行配置。
数据导出(Exporting)OTel SDK负责生成追踪数据,但需要导出到后端系统进行存储和展示,如Jaeger或Zipkin。你需要配置一个导出器(Exporter)。
# 环境变量配置示例(推荐方式) OTEL_EXPORTER_JAEGER_AGENT_HOST=jaeger-agent OTEL_EXPORTER_JAEGER_AGENT_PORT=6831 OTEL_SERVICE_NAME=my_cpp_service在代码中,你只需要初始化SDK并设置一个全局的TracerProvider即可,导出器会根据环境变量自动配置。
实操中的大坑:上下文传播在异步或基于线程池的C++服务中,保证追踪上下文不丢失是最大挑战。你不能简单地将Span对象在线程间传递。OTel C++ SDK提供了Context和Token机制。
// 在线程A中,获取当前上下文 auto current_ctx = opentelemetry::context::RuntimeContext::GetCurrent(); // 将上下文对象(本质是键值对存储)传递给线程B std::thread new_thread([captured_ctx = current_ctx] { // 在线程B中,附着该上下文 auto token = opentelemetry::context::RuntimeContext::Attach(captured_ctx); // 此时创建的Span会自动成为线程A中活跃Span的子Span auto child_span = tracer->StartSpan("async_work"); // ... 工作 ... child_span->End(); opentelemetry::context::RuntimeContext::Detach(token); });对于协程库(如libco, Boost.Coroutine),需要查阅OTel文档或社区是否有对应的集成方案,通常需要将上下文存储在协程的局部存储中。
3. 全链路集成实战:构建可观测的C++微服务
有了核心组件,下一步是将它们有机地整合到一个服务中,并处理一些棘手的集成问题。我们以一个提供REST API的C++服务为例。
3.1 服务初始化与全局可观测性上下文创建
在main函数或服务启动类中,我们需要一次性初始化三大支柱。
// observability_init.h #pragma once #include <memory> #include <prometheus/registry.h> #include <opentelemetry/trace/provider.h> class ObservabilityContext { public: static ObservabilityContext& GetInstance(); // 日志相关 std::shared_ptr<spdlog::logger> GetLogger(const std::string& name); // 指标相关 std::shared_ptr<prometheus::Registry> GetMetricsRegistry(); prometheus::Counter& BuildCounter(const std::string& name, const std::string& help, const std::map<std::string, std::string>& labels = {}); prometheus::Histogram& BuildHistogram(const std::string& name, const std::string& help, const prometheus::Histogram::BucketBoundaries& buckets, const std::map<std::string, std::string>& labels = {}); // 追踪相关 opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer> GetTracer(); private: ObservabilityContext(); std::shared_ptr<prometheus::Registry> metrics_registry_; std::unique_ptr<prometheus::Exposer> metrics_exposer_; // ... 其他私有成员,如日志sink、OTel provider等 ... };这个单例类封装了所有可观测性资源的创建和管理,避免全局变量散落各处。在构造函数中:
- 初始化spdlog,配置异步日志器和JSON格式化。
- 创建Prometheus Registry和Exposer,启动
/metrics端点HTTP服务器。 - 初始化OpenTelemetry SDK,配置Jaeger导出器(通过环境变量)。
3.2 HTTP请求处理中的三支柱联动
这是价值最大的部分。我们希望在处理每一个HTTP请求时,能自动记录日志、收集指标、创建追踪Span,并且它们能通过同一个请求ID关联起来。
思路:创建请求范围的上下文我们可以利用HTTP中间件(Middleware)或拦截器的概念。在处理请求伊始,生成或提取请求ID(如果上游已传递,如通过X-Request-ID头),并创建一个“请求可观测性上下文”对象。
class RequestObservabilityScope { public: RequestObservabilityScope(const httplib::Request& req, httplib::Response& res) : start_time_(std::chrono::steady_clock::now()) { // 1. 提取或生成请求ID request_id_ = req.get_header_value("X-Request-ID"); if (request_id_.empty()) { request_id_ = generate_uuid_v4(); } res.set_header("X-Request-ID", request_id_); // 2. 创建追踪Span auto tracer = ObservabilityContext::GetInstance().GetTracer(); // 从HTTP头中提取追踪上下文(W3C Trace-Context格式) auto parent_ctx = opentelemetry::trace::propagation::Extract(req.headers); span_ = tracer->StartSpan("HTTP " + req.method + " " + req.path, { { opentelemetry::trace::SemanticConventions::kHttpRequestId, opentelemetry::common::AttributeValue{request_id_} } }, parent_ctx); scope_ = std::make_unique<opentelemetry::trace::Scope>(span_); // 激活Span // 3. 记录结构化日志(请求开始) auto logger = ObservabilityContext::GetInstance().GetLogger("http"); SPDLOG_LOGGER_INFO(logger, "Request started", "request_id"_a=request_id_, "method"_a=req.method, "path"_a=req.path, "remote_ip"_a=req.remote_addr); } ~RequestObservabilityScope() { // 1. 记录请求耗时和结果 auto end_time = std::chrono::steady_clock::now(); std::chrono::duration<double> duration = end_time - start_time_; // 2. 更新Prometheus指标 auto& latency_histogram = ObservabilityContext::GetInstance().GetHistogram("http_request_duration_seconds"); latency_histogram.Observe(duration.count()); auto& request_counter = ObservabilityContext::GetInstance().GetCounter("http_requests_total"); request_counter.Increment(); // 3. 为Span设置属性并结束 span_->SetAttribute(opentelemetry::trace::SemanticConventions::kHttpStatusCode, response_status_code_); span_->SetAttribute("http.duration_ms", duration.count() * 1000); if (response_status_code_ >= 500) { span_->SetStatus(opentelemetry::trace::StatusCode::kError); } span_->End(); // 4. 记录结构化日志(请求结束) auto logger = ObservabilityContext::GetInstance().GetLogger("http"); SPDLOG_LOGGER_INFO(logger, "Request finished", "request_id"_a=request_id_, "status"_a=response_status_code_, "duration_ms"_a=duration.count() * 1000); } void SetResponseStatus(int code) { response_status_code_ = code; } private: std::string request_id_; opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span> span_; std::unique_ptr<opentelemetry::trace::Scope> scope_; std::chrono::steady_clock::time_point start_time_; int response_status_code_ = 200; };然后在每个HTTP请求处理器中,首先实例化这个RequestObservabilityScope对象。利用C++的RAII(资源获取即初始化)特性,无论请求处理成功还是异常退出,在析构函数中都能自动完成指标记录、Span结束和日志记录,确保数据不丢失。
3.3 下游调用(数据库、RPC)的上下文传播
当一个C++服务需要调用数据库或另一个gRPC服务时,必须将当前的追踪上下文传播过去,这样才能形成完整的调用链。
对于gRPC调用:如果使用gRPC,OTel提供了grpc客户端插件。配置后,它会自动将追踪信息注入到gRPC的metadata中。
// 在创建gRPC存根(Stub)时,使用经过OTel拦截的Channel auto channel = grpc::CreateChannel("server:50051", grpc::InsecureChannelCredentials()); // 假设已通过OTel配置,创建了带拦截器的Channel auto stub = MyService::NewStub(channel); // 发起调用,上下文会自动传播 grpc::ClientContext context; auto status = stub->SomeMethod(&context, request, &response);对于HTTP客户端调用(如使用libcurl):需要手动将当前的TraceId和SpanId以W3C Trace-Context格式(如traceparent头)添加到HTTP请求头中。
void call_downstream_http() { auto current_span = opentelemetry::trace::Tracer::GetCurrentSpan(); auto current_ctx = opentelemetry::context::RuntimeContext::GetCurrent(); // 将上下文注入到carrier(这里是一个map)中 std::map<std::string, std::string> headers; opentelemetry::trace::propagation::Inject(headers, current_ctx); CURL* curl = curl_easy_init(); // ... 设置URL等其他参数 ... struct curl_slist* chunk = nullptr; for (const auto& [key, value] : headers) { std::string header = key + ": " + value; chunk = curl_slist_append(chunk, header.c_str()); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); // ... 执行请求 ... }对于数据库查询:可以在执行SQL查询前后手动创建子Span,并将查询语句、耗时、影响行数等信息记录为Span的属性。一些ORM库或数据库驱动可能有OTel插件。
4. 数据收集、可视化与告警闭环
数据采集上来后,需要汇聚、存储、展示,并最终触发行动(告警),形成闭环。
4.1 日志收集与处理:Filebeat + ELK Stack
我们的C++服务将结构化的JSON日志输出到文件。使用Filebeat作为日志采集器,它轻量、可靠,负责跟踪日志文件的变化,并将日志行发送到Elasticsearch。
Filebeat配置 (filebeat.yml) 核心部分:
filebeat.inputs: - type: log enabled: true paths: - /var/log/my_cpp_service/*.log json.keys_under_root: true # 解析JSON日志 json.add_error_key: true tags: ["cpp-service"] output.elasticsearch: hosts: ["elasticsearch:9200"] indices: - index: "cpp-service-logs-%{+yyyy.MM.dd}" pipeline: "cpp_log_parse" # 可选:使用Ingest Pipeline进行更复杂的处理在Kibana中,你可以轻松地搜索日志(如request_id:"abc-123"来查看一个请求的所有相关日志),并创建可视化仪表盘,比如展示错误日志随时间的变化趋势。
4.2 指标抓取与展示:Prometheus + Grafana
Prometheus Server根据配置,定期从我们服务暴露的:8080/metrics端点拉取数据。配置prometheus.yml:
scrape_configs: - job_name: 'cpp-backend' static_configs: - targets: ['cpp-service-1:8080', 'cpp-service-2:8080'] metrics_path: '/metrics'然后,在Grafana中,添加Prometheus作为数据源,就可以创建丰富的仪表盘了。例如:
- 服务健康总览:用Stat面板显示请求总数、错误率、当前活跃连接数(Gauge)。
- API性能面板:用Graph面板展示各接口的P95、P99延迟变化,并设置阈值告警线。
- 资源监控:与Node Exporter结合,监控服务所在容器的CPU、内存使用情况。
4.3 追踪数据可视化:Jaeger UI
OpenTelemetry SDK将追踪数据发送到Jaeger Collector。打开Jaeger UI,你可以:
- 搜索追踪:按服务名、操作名、标签(如
http.status_code=500)或持续时间进行搜索。 - 分析单个追踪:直观地看到整个调用链的火焰图,哪个服务、哪个操作耗时最长一目了然。可以下钻查看每个Span的详细标签和日志(是的,Jaeger支持将关联的日志也展示在Span详情中,如果你在日志中输出了
trace_id)。 - 比较追踪:对比成功和失败请求的调用路径差异。
4.4 告警规则配置:从Prometheus Alertmanager到实战
监控的最终目的是为了快速发现问题。我们需要在Prometheus中定义告警规则(Alerting Rules)。
# prometheus_rules.yml groups: - name: cpp_service_alerts rules: - alert: HighRequestLatency expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5 for: 2m # 持续2分钟满足条件才触发 labels: severity: warning annotations: summary: "高请求延迟 (实例 {{ $labels.instance }})" description: "P95延迟在过去5分钟高于500ms,当前值为 {{ $value }}s。" - alert: ErrorRateSpike expr: rate(http_requests_total{status_code=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05 for: 1m labels: severity: critical annotations: summary: "错误率激增 (服务 {{ $labels.service_name }})" description: "HTTP 5xx错误率超过5%,当前值为 {{ $value | humanizePercentage }}."当告警触发时,Prometheus会将告警发送给Alertmanager。Alertmanager负责对告警进行分组、去重、静默,并通过路由规则发送到不同的接收器,如电子邮件、Slack、钉钉、PagerDuty等。
告警实战心得:
- 避免告警疲劳:精心设计告警阈值和
for持续时间。不要为每一个轻微抖动都告警。使用多级告警(warning, critical)。 - 告警信息 actionable:告警消息里应包含足够的信息,让接收者能立即开始排查,比如具体的服务名、实例、错误类型、相关的追踪ID或日志关键词。
- 设置运行状态告警:除了业务指标,别忘了对监控系统本身(如Prometheus、Exporter的up状态)设置告警。
5. 性能开销评估与生产环境调优
加入可观测性必然带来性能开销,我们的目标是将其控制在可接受的范围内(通常<5%)。
5.1 各组件开销分析与实测数据
- 日志(spdlog异步模式):开销极低,主要成本在于日志字符串的格式化(即使不输出也会发生)和内存队列的操作。实测在每秒数万条日志级别下,对业务线程的延迟影响可忽略不计。主要压力在IO,由后台线程承担。
- 指标(Prometheus客户端):Counter和Gauge的更新是原子操作,开销很小。Histogram是开销最大的部分,因为每次
Observe()需要遍历桶边界来确定落入哪个桶。如果某个操作每秒被调用数百万次,为其单独创建Histogram需要谨慎。可以考虑采样,或者只为关键路径(如外部API调用)创建Histogram。 - 追踪(OpenTelemetry):每个Span的创建、属性设置、上下文传播都有成本。采样(Sampling)是控制开销的关键。在生产环境,通常不会记录100%的请求追踪。可以配置头部采样(Head-based Sampling),例如只对1%的请求,或者对延迟超过一定阈值的请求进行完整追踪。
5.2 关键调优参数与配置
日志调优:
- 调整
spdlog的异步队列大小(queue_size)和后台线程刷新间隔。 - 生产环境务必关闭
TRACE和DEBUG级别日志,或通过动态配置在需要时开启。 - 使用条件日志宏,避免不必要的参数计算:
SPDLOG_LOGGER_DEBUG(logger, "Value: {}", expensive_computation());只有在日志级别为DEBUG时才会计算expensive_computation()。
- 调整
指标调优:
- 精简标签。评估每个标签的必要性。
- 对于超高频指标,考虑使用
Counter而不是Histogram,或者使用客户端聚合后再上报(但会损失精度)。 - 调整Prometheus的抓取间隔(
scrape_interval),从默认的15秒调整为30秒或60秒,可以降低双方压力。
追踪调优:
- 配置采样率。在OTel SDK初始化时配置采样器。
auto sampler = std::make_shared<opentelemetry::trace::ParentBasedSampler>( std::make_shared<opentelemetry::trace::TraceIdRatioBasedSampler>(0.01) // 1%采样率 ); auto provider = opentelemetry::trace::Provider::SetTracerProvider( std::make_shared<opentelemetry::trace::TracerProvider>(sampler) );- 限制单个Span的属性(Attributes)和事件(Events)数量。
- 使用批处理导出器(BatchSpanProcessor),而不是简单导出器(SimpleSpanProcessor),减少网络往返。
5.3 资源限制与熔断机制
为可观测性组件设置资源上限,防止其在异常情况下拖垮主服务。
- 日志:使用
spdlog的滚动文件策略,限制单个日志文件大小和总日志文件数量,定期清理旧日志。 - 指标:Prometheus客户端库的内存使用相对固定,主要关注抓取端点的网络带宽。
- 追踪:OTel的批处理导出器有队列大小限制。如果导出后端(如Jaeger)不可用,队列满后新的Span可能会被丢弃。需要监控导出队列的长度和导出错误。
6. 典型问题排查与实战调试技巧
即使体系搭建完毕,问题依然会出现。这里分享几个我踩过的坑和对应的排查思路。
6.1 问题一:Grafana图表显示“No Data”
现象:在Grafana中查询某个指标,始终没有数据。排查步骤:
- 检查数据源:确认Grafana连接的是正确的Prometheus地址,且Prometheus是健康的。
- 检查抓取目标:在Prometheus的
Status -> Targets页面,查看对应cpp-backendjob的状态是否为UP。如果是DOWN,查看错误信息,通常是网络不通或/metrics端点无法访问。 - 直接访问端点:在浏览器或使用
curl直接访问http://cpp-service:8080/metrics,看是否能返回明文指标数据。如果返回404,检查C++服务中Exposer是否正确启动并注册了Registry。 - 检查指标名称和标签:在Prometheus的
Graph页面,输入up{job="cpp-backend"}查看服务是否在线。然后输入{__name__=~".*http.*"}模糊搜索你的指标名,确认指标是否真的被上报,以及标签是否正确。 - 检查C++代码:确认更新指标的代码逻辑确实被执行到了。可以在更新指标的地方加一行日志。
6.2 问题二:Jaeger中找不到某个请求的追踪
现象:已知一个失败请求的request_id,但在Jaeger UI中搜索不到。排查步骤:
- 确认采样:首先确认这个请求是否被采样。检查采样率配置。如果是头部采样,可能恰好这个请求没被采到。可以临时降低采样率到100%进行调试。
- 检查上下文传播:这是最常见的问题。确认在服务内部跨线程,以及调用下游服务时,追踪上下文被正确传递。
- 在关键函数入口和出口,打印当前Span的
TraceId(可以通过opentelemetry::trace::Tracer::GetCurrentSpan()->GetContext().trace_id()获取并转换成16进制字符串),看是否一致。 - 对于HTTP调用,使用
curl -v或类似工具,查看发出的请求头中是否包含了traceparent等追踪头。
- 在关键函数入口和出口,打印当前Span的
- 检查导出器配置:确认OTel SDK配置的导出器地址(Jaeger Agent或Collector)是正确的,并且网络可达。查看SDK是否有导出错误日志。
- 检查Jaeger存储:确认Jaeger Collector和Query服务运行正常,存储(如Elasticsearch)有足够的空间和资源。
6.3 问题三:日志量过大,磁盘被写满
现象:服务器磁盘报警,发现是日志文件占用了大量空间。解决方案与预防:
- 立即清理:使用日志轮转和清理策略。
spdlog的daily_file_sink或rotating_file_sink可以按时间或大小滚动。同时,配合操作系统的logrotate工具或Kubernetes的日志轮转策略。 - 调整日志级别:生产环境长期保持
INFO级别。将一些过于频繁的INFO日志降级为DEBUG。 - 优化日志内容:避免在日志中打印完整的大对象(如整个请求体、响应体)。只记录摘要或关键标识。
- 结构化与过滤:因为日志是结构化的,可以在Filebeat或Logstash中配置过滤,只将错误(
level: ERROR)或更高级别的日志发送到Elasticsearch进行长期存储,将INFO日志存储在成本更低的对象存储中,或缩短其保留时间。
6.4 高级调试:利用追踪ID关联日志、指标和追踪
这是全链路监控的终极价值。当收到一个慢请求告警时:
- 从告警信息或Grafana图表中,定位到具体时间点和大概的服务、接口。
- 在Prometheus中,查询该时间段内该接口的高延迟指标,获取具体的
instance标签。 - 登录到对应实例,去日志文件中搜索该时间段附近的错误或警告日志。由于日志里有
request_id,可以快速定位到具体请求的日志流。 - 将这个
request_id(如果采样到了,它应该和trace_id有映射关系或就是trace_id的一部分)拿到Jaeger中搜索,得到完整的调用链火焰图,立刻就能看到时间消耗在哪个服务、哪个数据库查询上。
整个排查过程从小时级缩短到分钟级,这就是构建这套体系带来的最大回报。