在现代软件开发中,理解应用程序的运行时行为对于维护性能、诊断问题和确保可靠性至关重要。追踪和可观测性已成为实现这些目标的关键实践。本文探讨了 Java 应用程序中的追踪概念,深入研究了代码插桩技术,并展示了它们如何促进全栈可观测性。
追踪涉及记录应用程序中请求或事务的流程。它捕获关于操作执行的详细信息,包括时间、持续时间和上下文。在分布式系统中,追踪有助于可视化请求如何在多个服务和组件之间传播。
可观测性是指通过系统的外部输出来推断其内部状态的能力,主要由日志、指标和追踪三大支柱构成:日志用于记录离散事件,指标反映随时间变化的数值数据,追踪则展现请求在系统中的路径。这三者共同为应用程序的行为和健康状况提供了全面的视角。
Java 应用程序,特别是那些基于微服务的应用程序,可能很复杂且难以调试。追踪提供了可见性以洞察请求流和服务交互、性能监控以识别瓶颈和延迟问题、错误诊断以追踪错误到特定组件或操作,以及依赖映射以理解服务依赖关系和调用图。
插桩是向代码中添加观测能力的过程。在 Java 中,可以使用几种技术来实现:
开发者明确添加代码来记录追踪数据。示例:使用 OpenTelemetry 进行手动插桩,实现支付处理的追踪功能。
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class PaymentService {
private final Tracer tracer;
public PaymentService(Tracer tracer) {
this.tracer = tracer;
}
public void processPayment(String paymentId) {
// 创建支付处理追踪span - Create payment processing tracing span
Span span = tracer.spanBuilder("processPayment").startSpan();
try (Scope scope = span.makeCurrent()) {
// 设置支付ID属性 - Set payment ID attribute
span.setAttribute("payment.id", paymentId);
// 模拟支付处理工作 - Simulate payment processing work
Thread.sleep(100);
} catch (Exception e) {
// 记录异常到追踪span - Record exception to tracing span
span.recordException(e);
throw e;
} finally {
// 结束追踪span - End the tracing span
span.end();
}
}
}
这种方法的优点在于对插桩过程具有完全的控制权,并能够捕获特定于应用程序的上下文信息,但缺点是实施过程耗时且容易出错,同时还需要对代码进行更改。
使用代理和库在运行时自动注入追踪代码,例如下载OpenTelemetry Java Agent并使用 JVM 参数运行应用程序:
java -javaagent:path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=my-service \
-Dotel.traces.exporter=jaeger \
-jar myapp.jar
这种方法的优点在于无需更改代码、支持常见框架和库且易于部署,但缺点是对插桩的控制较少,并且可能会增加运行时开销。
使用注解来标记应追踪的方法,例如使用 Spring Cloud Sleuth 的 @NewSpan 注解,示例:
import org.springframework.cloud.sleuth.annotation.NewSpan;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@NewSpan("createOrder")
public void createOrder(String orderId) {
// 订单创建逻辑
}
}
这种方法的优点在于最小的代码侵入性和与 Spring 生态系统的良好集成,但缺点是仅限于支持的框架。
全栈可观测性意味着跨前端、后端、数据库和外部服务的可见性。
结合所有三大支柱以获得全面的洞察。
示例:将追踪与日志关联
import io.opentelemetry.api.trace.Span;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class InventoryService {
private static final Logger logger = LoggerFactory.getLogger(InventoryService.class);
public void updateInventory(String itemId) {
Span span = Span.current();
MDC.put("traceId", span.getSpanContext().getTraceId());
MDC.put("spanId", span.getSpanContext().getSpanId());
logger.info("更新物品{}的库存", itemId);
// 库存更新逻辑
MDC.clear();
}
}
通过将追踪 ID 添加到日志中,你可以将日志条目与追踪关联。
在分布式系统中,请求跨越多个服务,分布式追踪通过在服务之间传播上下文来跟踪这些请求,例如使用 OpenTelemetry 进行上下文传播:
服务 A(调用者):
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapSetter;
import java.net.http.HttpRequest;
public class ServiceA {
private final Tracer tracer;
private final TextMapPropagator propagator;
public void callServiceB() {
// 创建调用服务B的追踪span - Create tracing span for calling Service B
Span span = tracer.spanBuilder("callServiceB").startSpan();
try (Scope scope = span.makeCurrent()) {
// 创建HTTP请求构建器 - Create HTTP request builder
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create("http://service-b/endpoint"));
// 将当前上下文注入到请求头中 - Inject current context into request headers
propagator.inject(Context.current(), requestBuilder, new TextMapSetter<HttpRequest.Builder>() {
@Override
public void set(HttpRequest.Builder carrier, String key, String value) {
carrier.header(key, value);
}
});
HttpRequest request = requestBuilder.build();
// 发送HTTP请求(代码省略) - Send HTTP request (code omitted)
} finally {
// 结束追踪span - End the tracing span
span.end();
}
}
}
服务 B(被调用者):
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import javax.servlet.http.HttpServletRequest;
public class ServiceB {
private final TextMapPropagator propagator;
public void handleRequest(HttpServletRequest request) {
// 从请求头中提取追踪上下文 - Extract tracing context from request headers
Context extractedContext = propagator.extract(Context.current(), request, new TextMapGetter<HttpServletRequest>() {
@Override
public Iterable<String> keys(HttpServletRequest carrier) {
return Collections.list(carrier.getHeaderNames());
}
@Override
public String get(HttpServletRequest carrier, String key) {
return carrier.getHeader(key);
}
});
// 创建子span并设置父上下文 - Create child span and set parent context
Span span = tracer.spanBuilder("handleRequest")
.setParent(extractedContext)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 处理HTTP请求(代码省略) - Handle HTTP request (code omitted)
} finally {
// 结束追踪span - End the tracing span
span.end();
}
}
}
插桩数据库调用和外部 API 请求以捕获完整的请求流,例如追踪数据库查询:
import io.opentelemetry.api.trace.Span;
public class UserRepository {
private final Tracer tracer;
private final DataSource dataSource;
public User findUserById(String userId) {
// 创建数据库查询追踪span - Create database query tracing span
Span span = tracer.spanBuilder("db.query").startSpan();
// 设置数据库系统属性 - Set database system attribute
span.setAttribute("db.system", "postgresql");
// 设置SQL语句属性 - Set SQL statement attribute
span.setAttribute("db.statement", "SELECT * FROM users WHERE id = ?");
try (Scope scope = span.makeCurrent();
// 获取数据库连接 - Get database connection
Connection conn = dataSource.getConnection();
// 准备SQL语句 - Prepare SQL statement
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
// 设置查询参数 - Set query parameter
stmt.setString(1, userId);
// 执行查询 - Execute query
ResultSet rs = stmt.executeQuery();
// 处理结果集(代码省略) - Process result set (code omitted)
} catch (SQLException e) {
// 记录数据库异常到追踪span - Record database exception to tracing span
span.recordException(e);
throw e;
} finally {
// 结束追踪span - End the tracing span
span.end();
}
}
}
在实施追踪时需要考虑性能开销问题,因为插桩可能会增加 CPU 和内存的使用率,影响应用程序的响应时间和吞吐量,因此需要通过优化插桩代码、选择合适的采样率以及定期性能监控来减少这种影响。同时,追踪会生成大量的数据,在高并发的生产环境中每天可能产生数 GB 甚至 TB 级的数据,这要求制定合理的存储和保留策略,包括数据压缩、分布式存储以及基于时间或重要性的自动清理机制来有效管理数据量。