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-TraceId、X-B3-SpanId 和 X-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 提供的 TraceRunnable 和 TraceCallable 来自动传递 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 整合起来,我们可以更全面地了解系统的行为,从而更快地诊断问题、优化性能并提高整体可靠性。 未来,随着云原生技术的不断发展,可观测性将会变得更加重要。 我们需要不断学习和掌握新的可观测性技术,以便更好地构建和维护我们的系统。