可观测性模式:日志、指标、链路追踪

说实话,可观测性这个词,前几年还只是个概念。

但到了今天,谁要是没在系统里搭过一套完整的可观测体系,那基本等于在裸奔。我见过太多线上事故,明明系统挂了,却连根毛都查不到——日志没打、指标没采、链路没连,最后只能靠猜。嗯,猜出来的结论,十有八九是错的。

可观测性说白了就三件事:日志(Logs)、指标(Metrics)、链路追踪(Traces)。这三者合起来,业界叫它“三大支柱”。今天我们就一个一个拆开讲,再聊聊怎么用ELK、Prometheus、Jaeger把它们落地。

核心观点: 可观测性不是监控的升级版,而是让你在未知故障面前,有能力问出“为什么”并找到答案。

1. 日志(Logs)——最原始,也最容易被忽视

日志是最古老的可观测手段。我刚开始写Java那会儿,System.out.println就是我的调试神器。但到了生产环境,这么干就是找死。

日志的关键在于结构化集中化。你想想看,几百台机器,每台都写本地文件,出问题了你一台台去翻?不现实。

我个人习惯用ELK(Elasticsearch + Logstash + Kibana)这套组合。Logstash负责采集和解析,Elasticsearch负责存储和搜索,Kibana负责展示。

这里有个避坑指南:我曾经在一个项目里,日志量一天冲到几个TB,Elasticsearch直接OOM。后来加了日志采样和生命周期管理,才稳住。所以,日志一定要设保留期限采样率,别什么都往里塞。

// Java示例:使用Logback输出结构化JSON日志
import ch.qos.logback.classic.Logger;
import net.logstash.logback.argument.StructuredArguments;

Logger logger = (Logger) LoggerFactory.getLogger("MyService");
logger.info("用户下单成功",
    StructuredArguments.keyValue("userId", 12345),
    StructuredArguments.keyValue("orderId", "ORD-20231001-001"),
    StructuredArguments.keyValue("amount", 299.99)
);
// 输出:{"message":"用户下单成功","userId":12345,"orderId":"ORD-20231001-001","amount":299.99}
// C++示例:使用spdlog输出结构化日志
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>

auto logger = spdlog::rotating_logger_mt("file_logger", "logs/app.log", 1024*1024*10, 3);
logger->set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");
logger->info("用户下单成功, userId={}, orderId={}, amount={}", 12345, "ORD-20231001-001", 299.99);

2. 指标(Metrics)——系统的“体温计”

指标和日志不同。日志是离散的事件,指标是连续的数值。你想想看,体温计不会告诉你“你几点几分咳嗽了”,它只告诉你“你现在38.5度”。

指标最常用的工具是Prometheus。它采用拉模型,定期从你的服务端点抓取数据。配合Grafana做可视化,基本是标配。

常见的指标类型有四种:

类型 说明 示例
Counter(计数器) 只增不减,用于累计值 请求总数、错误总数
Gauge(仪表盘) 可增可减,用于瞬时值 当前内存使用量、在线用户数
Histogram(直方图) 统计分布,用于延迟等 请求延迟的P50、P99
Summary(摘要) 类似直方图,但可计算分位数 请求延迟的滑动窗口分位数

我在项目中遇到过一个问题:Prometheus的拉模式在Kubernetes环境下非常方便,但如果你服务实例频繁重启,可能会丢失短暂的数据点。这时候可以配合Pushgateway做补充,但要注意别滥用。

// Java示例:使用Micrometer暴露Prometheus指标
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;

public class OrderService {
    private final Counter orderCounter;

    public OrderService(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("已创建的订单总数")
            .register(registry);
    }

    public void createOrder() {
        // 业务逻辑...
        orderCounter.increment();
    }
}
// C++示例:使用Prometheus-cpp暴露指标
#include <prometheus/counter.h>
#include <prometheus/registry.h>

auto registry = std::make_shared<prometheus::Registry>();
auto& counter_family = prometheus::BuildCounter()
    .Name("orders_created_total")
    .Help("已创建的订单总数")
    .Register(*registry);
auto& counter = counter_family.Add({});

// 业务逻辑中调用
counter.Increment();

3. 链路追踪(Traces)——跨服务的“侦探”

微服务架构下,一个请求可能要经过十几个服务。日志和指标只能告诉你“哪个服务慢了”,但链路追踪能告诉你“慢在哪一步”。

链路追踪的核心是Trace IDSpan ID。一个Trace代表一次完整的请求,每个Span代表请求中的一个步骤。Span之间通过Parent-Child关系串联。

我常用的工具是Jaeger,它和OpenTelemetry协议配合得很好。你只需要在代码里埋点,Jaeger会自动收集并展示调用拓扑。

小技巧: 链路追踪的采样率不要设100%。高并发系统下,全量采样会带来巨大的性能开销。我一般设1%~10%,配合错误采样(错误请求全量采样),效果很好。
// Java示例:使用OpenTelemetry进行链路追踪
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

Tracer tracer = openTelemetry.getTracer("order-service");
Span span = tracer.spanBuilder("createOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
    // 业务逻辑...
    span.setAttribute("orderId", "ORD-20231001-001");
} finally {
    span.end();
}
// C++示例:使用OpenTelemetry C++ SDK
#include <opentelemetry/trace/span.h>
#include <opentelemetry/trace/tracer.h>

auto tracer = opentelemetry::trace::TracerProvider::GetTracer("order-service");
auto span = tracer->StartSpan("createOrder");
auto scope = tracer->WithActiveSpan(span);
// 业务逻辑...
span->SetAttribute("orderId", "ORD-20231001-001");
span->End();

4. 三大支柱如何协同工作?

日志、指标、链路追踪不是孤立的。它们应该互相补充,形成闭环。

举个例子:

  1. Prometheus告警:订单服务响应时间P99超过2秒。
  2. 你打开Jaeger,找到对应时间段的慢Trace,发现瓶颈在“库存服务”的某个Span。
  3. 你再去ELK里搜这个Trace ID对应的日志,发现库存服务抛了一个数据库连接超时异常。
  4. 问题定位:数据库连接池太小。

你看,三个工具缺一不可。没有指标,你发现不了问题;没有链路,你定位不到服务;没有日志,你找不到根因。

注意: 不要为了可观测性而牺牲性能。日志的I/O开销、指标的采集开销、链路的序列化开销,都是真实存在的。我曾经见过一个团队,因为日志打得太猛,把CPU打满了。所以,一定要做性能压测,找到平衡点。

5. 整体架构图

下面这张图展示了日志、指标、链路追踪三者的数据流和工具链关系:

可观测性三大支柱架构图 应用服务 应用服务 应用服务 日志(Logs) Logstash 采集 → Elasticsearch 存储 → Kibana 展示 指标(Metrics) Prometheus 拉取 → TSDB 存储 → Grafana 展示 链路追踪(Traces) OpenTelemetry 埋点 → Jaeger 收集 → 拓扑展示

6. 落地建议

最后,我分享几个实战中的经验:

  • 不要一步到位。 先上日志,再上指标,最后上链路。一口吃不成胖子。
  • 统一标准。 日志格式、指标命名、Trace ID传递方式,团队内部一定要统一。否则后期维护成本极高。
  • 关注成本。 可观测性工具本身也是资源消耗大户。ELK的磁盘、Prometheus的内存、Jaeger的网络,都要提前规划。
  • 自动化告警。 光有数据没用,得配上告警规则。我习惯用Prometheus的Alertmanager,配合钉钉或企业微信通知。
一句话总结: 日志告诉你发生了什么,指标告诉你哪里出了问题,链路追踪告诉你问题出在谁身上。三者结合,才是真正的可观测性。
公众号:蓝海资料掘金营,微信deep3321