Java应用中的可观测性:Metrics、Traces、Logs的统一采集与Context传递

Java应用可观测性:Metrics、Traces、Logs的统一采集与Context传递

大家好,今天我们来探讨Java应用中的可观测性,重点关注Metrics(指标)、Traces(链路追踪)和Logs(日志)的统一采集与Context传递。可观测性对于构建和维护复杂的分布式系统至关重要。它让我们能够深入了解系统的内部状态,从而更快地诊断问题、优化性能并提高整体可靠性。

1. 可观测性的三大支柱:Metrics, Traces, Logs

可观测性主要由三个核心要素构成:

  • Metrics (指标): 数值型数据,通常代表一段时间内的聚合统计。例如,请求的响应时间、CPU使用率、内存占用等。Metrics适合用于监控系统整体健康状况和趋势。

  • Traces (链路追踪): 记录单个请求或事务在系统中的完整调用链。它可以帮助我们确定性能瓶颈、错误传播路径以及服务间的依赖关系。

  • Logs (日志): 离散的文本事件,记录了系统运行过程中发生的各种事件。日志通常包含详细的错误信息、调试信息和审计信息,可以帮助我们深入分析问题。

这三者并非相互独立,而是互补的。Metrics 提供宏观视图,Traces 追踪请求路径,Logs 则提供详细的事件上下文。将它们结合起来,我们可以更全面地了解系统的行为。

要素 描述 适用场景 示例
Metrics 数值型数据,聚合统计,反映系统整体状态 监控系统健康状况、识别趋势、触发告警 CPU使用率、响应时间、请求吞吐量
Traces 请求的调用链,记录服务间的依赖关系 诊断性能瓶颈、追踪错误传播、了解服务间依赖关系 用户登录请求的完整调用链,涉及多个微服务
Logs 文本事件,记录系统运行过程中的详细信息 深入分析问题、调试代码、审计操作 异常堆栈信息、用户登录日志、数据库查询日志

2. Metrics采集:Micrometer与Prometheus

Micrometer 是一个针对 JVM 的应用监控工具的度量外观(metrics facade)。它可以让我们以统一的方式采集各种指标,并将其导出到不同的监控系统,例如 Prometheus、Datadog、InfluxDB 等。

示例:使用 Micrometer 采集 HTTP 请求计数

首先,添加 Micrometer 和 Prometheus 的依赖到 pom.xml

<dependencies>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-core</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

然后,创建一个 Spring Boot Controller,并使用 MeterRegistry 注入 Micrometer 的核心接口:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    private final Counter requestCounter;

    public MyController(MeterRegistry registry) {
        this.requestCounter = Counter.builder("http_requests_total")
                .description("Total number of HTTP requests")
                .register(registry);
    }

    @GetMapping("/hello")
    public String hello() {
        requestCounter.increment();
        return "Hello, world!";
    }
}

在这个例子中,我们创建了一个名为 http_requests_total 的 Counter,每次访问 /hello 接口时,这个 Counter 的值都会增加。

配置 Prometheus 端点

application.properties 中添加如下配置,暴露 Prometheus 端点:

management.endpoints.web.exposure.include=prometheus
management.metrics.export.prometheus.enabled=true

现在,可以通过 /actuator/prometheus 端点访问 Prometheus 指标。

Prometheus 配置

配置 Prometheus 抓取 Java 应用暴露的 metrics 端点。在 prometheus.yml 文件中添加以下配置:

scrape_configs:
  - job_name: 'java-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['localhost:8080'] # 替换为你的 Java 应用的地址

重启 Prometheus 后,就可以在 Prometheus Web UI 中查询 http_requests_total 指标。

自定义 Metrics

除了使用 Micrometer 提供的标准 Metrics 类型(Counter, Gauge, Timer, Distribution Summary, LongTaskTimer),还可以自定义 Metrics。例如,可以创建一个 Gauge 来监控 JVM 内存使用情况:

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;

@Component
public class MemoryMetrics {

    private final MeterRegistry registry;
    private final MemoryMXBean memoryMXBean;

    public MemoryMetrics(MeterRegistry registry) {
        this.registry = registry;
        this.memoryMXBean = ManagementFactory.getMemoryMXBean();
    }

    @PostConstruct
    public void init() {
        Gauge.builder("jvm_memory_used", memoryMXBean, MemoryMXBean::getHeapMemoryUsage).
                description("JVM heap memory used")
                .register(registry);
    }
}

这段代码创建了一个名为 jvm_memory_used 的 Gauge,它会定期读取 JVM 的堆内存使用情况。

3. Traces 采集:Spring Cloud Sleuth 与 Zipkin/Jaeger

Spring Cloud Sleuth 是一个 Spring Boot 的分布式追踪解决方案。它可以自动追踪 Spring 应用中的 HTTP 请求、消息队列和数据库调用。Sleuth 通常与 Zipkin 或 Jaeger 等追踪系统配合使用,用于存储和可视化追踪数据。

示例:使用 Spring Cloud Sleuth 追踪 HTTP 请求

首先,添加 Spring Cloud Sleuth 和 Zipkin 的依赖到 pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-zipkin</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

然后,在 application.properties 中配置 Zipkin 服务器的地址:

spring.zipkin.baseUrl=http://localhost:9411
spring.application.name=my-service

现在,启动 Zipkin 服务器(可以使用 Docker 镜像 openzipkin/zipkin),然后启动 Spring Boot 应用。 Sleuth 会自动追踪 HTTP 请求,并将追踪数据发送到 Zipkin 服务器。

跨服务追踪

在微服务架构中,链路追踪需要跨多个服务。 Sleuth 可以自动传播追踪上下文(trace ID 和 span ID)到下游服务。 例如,如果一个服务调用另一个服务,Sleuth 会在 HTTP 请求头中添加 X-B3-TraceIdX-B3-SpanIdX-B3-ParentSpanId 等头部,以便下游服务可以继续追踪。

手动创建 Span

有时候,我们需要手动创建 Span 来追踪一些特殊的代码段。可以使用 Tracer 接口来实现:

import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private Tracer tracer;

    public void doSomething() {
        tracer.startScopedSpan("my-custom-span");
        try {
            // 执行一些操作
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            tracer.currentSpan().ifPresent(span -> span.tag("operation", "doSomething"));
            tracer.currentSpan().ifPresent(brave.Span::finish);
        }
    }
}

这段代码创建了一个名为 my-custom-span 的 Span,并添加了一个名为 operation 的 Tag。

使用 OpenTelemetry

OpenTelemetry 是一个云原生可观测性框架,旨在提供统一的 API、SDK 和工具,用于生成和收集遥测数据(Metrics, Traces, Logs)。 相比于 Spring Cloud Sleuth,OpenTelemetry 更加标准化和灵活,支持更多的追踪系统和编程语言。

要使用 OpenTelemetry,你需要添加 OpenTelemetry 的依赖到 pom.xml,并配置 OpenTelemetry SDK。 具体配置涉及到选择Exporter,例如 Jaeger、Zipkin或OTLP。

4. Logs 采集:SLF4J, Logback/Log4j2 与 ELK Stack

SLF4J (Simple Logging Facade for Java) 是一个日志抽象层,它允许我们使用统一的 API 来记录日志,而无需关心底层的日志实现。 Logback 和 Log4j2 是常用的 SLF4J 实现。

示例:使用 SLF4J 和 Logback 记录日志

首先,添加 SLF4J 和 Logback 的依赖到 pom.xml

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

然后,在 Java 代码中使用 Logger 接口来记录日志:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    private static final Logger logger = LoggerFactory.getLogger(MyService.class);

    public void doSomething() {
        logger.info("Doing something...");
        try {
            // 执行一些操作
            throw new Exception("Something went wrong");
        } catch (Exception e) {
            logger.error("Error occurred: {}", e.getMessage(), e);
        }
    }
}

在这个例子中,我们使用了 logger.info()logger.error() 方法来记录日志。

配置 Logback

可以通过 logback.xml 文件来配置 Logback。例如,可以配置日志的输出格式、日志级别和日志文件。

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

这段配置将日志输出到控制台,并使用指定的格式。

ELK Stack (Elasticsearch, Logstash, Kibana)

ELK Stack 是一个流行的日志管理和分析平台。 Elasticsearch 用于存储和索引日志,Logstash 用于收集和处理日志,Kibana 用于可视化日志数据。

  • Logstash: 从各种来源(例如文件、TCP/UDP 端口、消息队列)收集日志,并对其进行解析、转换和增强。
  • Elasticsearch: 存储 Logstash 处理后的日志数据,并提供强大的搜索和分析功能。
  • Kibana: 提供 Web 界面,用于查询、分析和可视化 Elasticsearch 中的日志数据。

将日志发送到 ELK Stack

可以使用 Logstash 来收集 Java 应用的日志,并将其发送到 Elasticsearch。 首先,配置 Logstash 的输入插件,监听 Java 应用的日志文件:

input {
  file {
    path => "/path/to/your/application.log"
    start_position => "beginning"
  }
}

然后,配置 Logstash 的输出插件,将日志发送到 Elasticsearch:

output {
  elasticsearch {
    hosts => ["http://localhost:9200"]
    index => "java-app-%{+YYYY.MM.dd}"
  }
}

最后,在 Kibana 中创建索引模式,并使用 Kibana 的可视化工具来分析日志数据。

5. Context传递:ThreadLocal 与 MDC

在分布式系统中,我们需要将一些上下文信息(例如 trace ID、user ID、correlation ID)在不同的线程和不同的服务之间传递。常用的方法是使用 ThreadLocal 和 MDC (Mapped Diagnostic Context)。

  • ThreadLocal: 提供线程局部变量,每个线程都拥有自己的变量副本。
  • MDC: 是 SLF4J 提供的一个功能,可以在日志消息中添加上下文信息。MDC 使用 ThreadLocal 来存储上下文信息。

示例:使用 MDC 传递 Trace ID

import org.slf4j.MDC;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class MyService {

    public void doSomething() {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        try {
            // 执行一些操作
            log.info("Doing something...");
        } finally {
            MDC.remove("traceId");
        }
    }
}

在这个例子中,我们生成一个 UUID 作为 trace ID,并将其放入 MDC 中。 之后,所有的日志消息都会包含这个 trace ID。

配置 Logback,在日志中包含 MDC 信息

logback.xml 文件中,可以使用 %X{traceId} 来引用 MDC 中的 traceId:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

现在,日志消息会包含 trace ID:

2023-10-27 10:00:00.000 [main] INFO  com.example.MyService [123e4567-e89b-12d3-a456-426614174000] - Doing something...

在异步任务中传递 Context

在使用线程池或异步任务时,需要手动将 Context 从父线程传递到子线程。 可以使用 ThreadLocal.get()ThreadLocal.set() 方法来实现。 但是,这种方法比较繁琐,容易出错。

可以使用 Spring Cloud Sleuth 提供的 TraceRunnableTraceCallable 来自动传递 Context。

import org.springframework.cloud.sleuth.instrument.async.TraceRunnable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Async
    public void doSomethingAsync() {
        Runnable task = () -> {
            // 执行一些操作
            log.info("Doing something async...");
        };
        new TraceRunnable(task).run();
    }
}

这段代码使用 TraceRunnable 来包装异步任务,它可以自动将 Context 从父线程传递到子线程。

OpenTelemetry 的 Context Propagation

OpenTelemetry 提供了更强大的 Context Propagation 机制。 它使用 Context 对象来存储 Context 信息,并使用 TextMapPropagator 接口来在不同的进程和服务之间传播 Context。

OpenTelemetry 的 Context Propagation 机制更加灵活和可扩展,可以支持更多的协议和传输方式。

6. 统一采集:整合 Metrics, Traces, Logs

为了更好地理解系统的行为,我们需要将 Metrics、Traces 和 Logs 整合起来。 例如,可以在日志消息中包含 trace ID,以便可以将日志消息与特定的请求关联起来。

示例:在日志中包含 Trace ID

在使用 Spring Cloud Sleuth 的情况下,Sleuth 会自动将 trace ID 放入 MDC 中。 只需要配置 Logback,在日志中包含 %X{traceId} 即可。

使用 OpenTelemetry 的 Context Propagation

OpenTelemetry 提供了统一的 Context Propagation 机制,可以将 trace ID 和其他 Context 信息在不同的进程和服务之间传播。 可以在 Metrics 和 Logs 中使用 OpenTelemetry 的 Context API 来访问 Context 信息。

使用 Correlation ID

Correlation ID 是一个自定义的 ID,可以用于将不同的事件关联起来。 例如,可以生成一个 Correlation ID,并将其放入 HTTP 请求头、消息队列消息头和日志消息中。

使用 Baggage

Baggage 是 OpenTelemetry 提供的一个功能,可以用于在不同的进程和服务之间传递自定义的 Context 信息。 Baggage 可以用于传递一些业务相关的 Context 信息,例如 user ID、customer ID 等。

7. 总结与展望

今天我们讨论了 Java 应用中的可观测性,重点关注 Metrics、Traces 和 Logs 的统一采集与 Context 传递。 可观测性是构建和维护复杂的分布式系统的关键。 通过将 Metrics、Traces 和 Logs 整合起来,我们可以更全面地了解系统的行为,从而更快地诊断问题、优化性能并提高整体可靠性。 未来,随着云原生技术的不断发展,可观测性将会变得更加重要。 我们需要不断学习和掌握新的可观测性技术,以便更好地构建和维护我们的系统。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注