作者:禅与计算机程序设计艺术
1.简介
分布式跟踪(Distributed tracing)是指应用程序组件之间的交互过程的可视化表示,用于帮助开发人员理解系统行为并进行故障排查、性能调优、可靠性分析等。该技术通常被用来监测微服务、云原生应用程序和基于容器的架构。Jaeger是一个开源分布式跟踪系统,由uber开源并贡献给云原生计算联盟(CNCF)。
本文主要介绍OpenTracing API和Jaeger的功能,为读者提供一个完整的使用指南。在阅读本文之前,需要读者对OpenTracing标准协议有基本的了解。本文假设读者已经安装了Java、Maven、Docker及Kubernetes相关环境,并熟悉相关概念,如进程间通信、分布式系统和微服务架构。另外,本文不涉及实际案例研究,而是通过一些示例代码演示了Jaeger的基本用法。
本文将从以下几个方面进行介绍:
- OpenTracing介绍。
- Jaeger系统架构。
- 使用Jaeger构建一个简单的分布式追踪系统。
- Jaeger UI的使用方法。
- Jaeger客户端库的选择。
- Jaeger部署到Kubernetes集群上的方法。
- 总结。
2.OpenTracing介绍
OpenTracing 是 CNCF 提供的一套开放标准,它定义了一种标准化的接口,可以用来向已有的应用中添加分布式跟踪能力。该规范致力于成为应用程序间的通用跟踪解决方案,使得各种分布式环境下的监控、日志和跟踪工具可以相互集成。
2.1 分布式跟踪的意义
当用户访问一个网站时,不仅需要根据网站的URL定位服务器,还要知道每个请求的响应时间、返回码、错误信息等其他详细信息。对于复杂的分布式系统来说,这种能力是至关重要的。比如说,网站首页的加载时间过长,导致用户点击刷新按钮等待反应的场景;或者是用户由于网络问题无法正常登录某个功能模块,这时候管理员只能够通过查看日志文件来发现问题所在。在这些情况下,如果能够通过跟踪系统捕获每个请求的全过程信息,就能够准确诊断出问题的原因,改善系统的可用性和性能,提升客户体验。
2.2 OpenTracing的作用
OpenTracing 作为分布式跟踪的标准规范,旨在定义一种统一的调用上下文格式,并通过定义统一的抽象API来向各种语言、框架和中间件提供分布式跟踪的能力。这样就可以实现跨不同框架和编程模型的应用之间的分布式跟踪,并让开发者更方便地接入和集成分布式跟踪功能。其主要作用如下所示:
为监控和度量数据提供统一的数据格式,屏蔽底层数据源的差异。
为开发者提供一个统一的API,通过API,可以轻松地创建分布式跟踪的上下文、记录事件、发送span数据。
提供统一的抽象,可以在不同的语言、框架和组件之间复用相同的代码,提高透明度。
支持多种可插拔的后端存储系统,包括内存、基于文件的存储、数据库、消息队列和其他分布式跟踪系统。
通过插件机制,可以支持不同的传输方式,如RESTful API、gRPC和Kafka等。
对Tracing数据进行采样,降低收集量和带宽占用,提高性能。
支持分布式追踪中的跨进程跟踪。
2.3 调用链路追踪
调用链路追踪(trace)描述的是一个事务过程中所有的相关活动。其一般包括四个主要元素:
Trace ID: 一串唯一标识符,用来唯一标记一次事务。
Span ID: 一串唯一标识符,用来唯一标记一次操作。
Parent span: 表示当前Span的父级Span。
Child span: 表示当前Span的子级Span。
调用链路追踪的一个最典型的例子就是支付宝支付流程。在这个流程中,需要经历多个子系统的协同才能完成整个交易。每个子系统都可以独立完成自己的工作,但又需要互相协作,因此会产生很多依赖关系。通过调用链路追踪,就可以清晰地展示整个支付流程中的各个子系统和调用关系。
2.4 OpenTracing API
OpenTracing提供的API包括三个部分:
- Tracer接口:Tracer接口用于生成新的spans,并把它们绑定到调用栈上。
- Span接口:Span接口用于描述一个操作的范围,比如一个远程过程调用,HTTP 请求,数据库查询等。它具有开始时间、结束时间、标签键值对、上下文信息等属性。
- ScopeManager接口:ScopeManager接口管理Span的生命周期,并控制Span的进入和退出。
下图展示了OpenTracing API的组成:
2.5 OpenTracing术语表
OpenTracing提供了一份术语表来帮助理解OpenTracing的各种概念。以下是术语表的摘要:
名称 | 描述 |
---|---|
Tracer | 负责生成和 propagating spans 的实体。 |
Context | 传递 trace 和 span 的 carrier 对象。 |
Propagator | 将 span context 从一个 process 传播到另一个 process 中。 |
Span | 代表一个跨度(operation),记录了事件的时间顺序,并且可以嵌套子 Span 。 |
Scope Manager | 管理 scope,即决定何时开始或结束一个 span。 |
Reference | 引用是指向其它 spans 的指针。 |
3.Jaeger系统架构
Jaeger是一个开源分布式跟踪系统,由uber开源并贡献给云原生计算联盟(CNCF)。Jaeger包含三个主要组件:
Jaeger客户端库:由语言独立的库组成,例如 Java、Go、Node.js 等。它们负责生成、收集、处理和报告 spans。
数据存储组件:接收、存储和查询 spans。Jaeger 提供了一个独立的选项,可以让您部署自己的自定义数据存储。目前支持 Cassandra、ElasticSearch、Kafka、MongoDB、MySQL 或 PostgreSQL 等。
查询服务:查询服务是一个独立的组件,用来检索和过滤 spans。它可以使用 UI 来呈现 traces,或者提供其他查询语言的 API。
下图展示了Jaeger的系统架构:
4.使用Jaeger构建一个简单的分布式追踪系统
在本节中,我们将用 Java 编写一个简单的分布式追踪系统,来演示如何使用 Jaeger 组件。
4.1 创建 Maven 项目
首先,创建一个名为 simple-tracing
的 Maven 项目。为简单起见,我们将运行一个 Spring Boot 应用,该应用包含一个简单的 REST 服务,它接收 GET 请求,返回固定字符串。
pom.xml 文件如下所示:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>simple-tracing</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加 opentracing-util 依赖来生成Spans-->
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-util</artifactId>
<version>0.33.0</version>
</dependency>
<!-- 添加 jaeger-core 依赖来连接 Jaeger client 和 server -->
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-core</artifactId>
<version>1.6.0</version>
</dependency>
</dependencies>
</project>
在 application.properties
文件中,配置数据存储器和端口号:
server.port=9000
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
# 使用本地内存作为数据存储器
spring.profiles.active=dev
# 使用 Elasticsearch 作为数据存储器
#spring.profiles.active=prod-es
#spring.data.elasticsearch.cluster-nodes=localhost:9200
# 配置 Elasticsearch Index
#spring.data.elasticsearch.properties.index.number_of_shards=1
#spring.data.elasticsearch.properties.index.number_of_replicas=0
# 指定 Jaeger Client 的地址
#spring.jaeger.agent-host-port=localhost:6831
Spring Boot 在启动时,它会根据配置文件激活相应的 profile。在 dev
模式下,它默认使用 H2 内存数据库做为数据存储器,而在 prod-es
模式下,它会使用 Elasticsearch 作为数据存储器。
为了创建 spans,我们需要引入 opentracing-util
依赖,该依赖包中包含一个 GlobalTracer
,我们可以通过它来获取全局 tracer 实例。
4.2 编写业务逻辑代码
编写 GreetingController
来处理 HTTP 请求,它有一个 greeting()
方法来生成问候语。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import io.opentracing.*;
@RestController
public class GreetingController {
private static final Logger logger = LoggerFactory.getLogger(GreetingController.class);
private static final String SERVICE_NAME = "greeting";
// 获取全局tracer实例
private static Tracer tracer = GlobalTracer.get();
@GetMapping("/greeting")
public String greeting() {
try (Scope ignored = tracer.buildSpan("greeting").asChildOf(tracer.activeSpan().context()).startActive(true)) {
return "Hello World!";
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
这里,我们使用 try-with-resources 语法自动关闭 spans,使用 scopeManager
来管理 scopes,并获取全局 tracer 实例。我们构建了一个名为 "greeting"
的 span,并把它设置为它的父级 span,然后将其开启。
4.3 初始化 Jaeger 客户端
编写 SimpleTracingApplication
来初始化 Jaeger client:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.uber.jaeger.Configuration;
import com.uber.jaeger.Configuration.ReporterConfiguration;
import com.uber.jaeger.Configuration.SamplerConfiguration;
import com.uber.jaeger.samplers.ConstSampler;
import com.uber.jaeger.senders.UdpSender;
@SpringBootApplication
public class SimpleTracingApplication implements CommandLineRunner {
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(SimpleTracingApplication.class, args);
}
@Autowired
Configuration configuration;
@Override
public void run(String... args) throws Exception {
SamplerConfiguration samplerConfig =
new SamplerConfiguration().withType(ConstSampler.TYPE).withParam(1);
ReporterConfiguration reporterConfig =
new ReporterConfiguration().withLogSpans(true).withFlushIntervalMs(1000).
withMaxQueueSize(1000).withSender(new UdpSender());
configuration = new Configuration(SERVICE_NAME,
samplerConfig, reporterConfig);
}
}
这里,我们通过 Spring 注入的方式初始化 Jaeger client。我们设置了一个定制化的 Sampler 配置,并指定了使用的 Sender,该 Sender 会将 spans 发送到指定的地址。然后,我们通过 configuration
变量保存配置。
4.4 启用追踪功能
在 application.properties
文件中,开启追踪功能:
logging.level.root=TRACE
spring.sleuth.enabled=true
spring.zipkin.enabled=false
logging.level.root
属性可以调整日志级别,此处我们调整为 TRACE 以便查看 spans 相关的信息。spring.sleuth.enabled
属性开启 Sleuth 的 tracing 功能,spring.zipkin.enabled
属性关闭 Zipkin 的 tracing 功能,避免冲突。
4.5 执行测试
编译项目,执行以下命令运行 Spring Boot 应用:
mvn clean package
java -jar target/simple-tracing-1.0-SNAPSHOT.jar
打开浏览器访问 http://localhost:9000/greeting
,然后查看 logs 中的 spans 信息:
2021-02-09 16:19:51.181 TRACE 36330 --- [nio-9000-exec-1] o.s.c.support.DefaultLifecycleProcessor : Failed to start bean'metricBeans'; nested exception is java.lang.UnsupportedOperationException
2021-02-09 16:19:51.702 DEBUG 36330 --- [nio-9000-exec-1] c.u.j.Configuration : Initializing a ConstSampler with parameter 1
2021-02-09 16:19:51.704 INFO 36330 --- [nio-9000-exec-1] i.o.p.s.b.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4c8a0d46: startup date [Wed Feb 09 16:19:51 CST 2021]; root of context hierarchy
2021-02-09 16:19:52.313 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet : Initialization of Spring DispatcherServlet complete in 610 ms
2021-02-09 16:19:52.565 DEBUG 36330 --- [nio-9000-exec-1].w.s.r.r.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting]}" onto public java.lang.String com.example.simpletracing.controller.GreetingController.greeting()
2021-02-09 16:19:52.579 DEBUG 36330 --- [nio-9000-exec-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ResponseBody methodExceptionHandlerMethodResolver: public java.lang.Object org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.NativeWebRequest)
2021-02-09 16:19:52.581 DEBUG 36330 --- [nio-9000-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "null /greeting null" onto public java.lang.String com.example.simpletracing.controller.GreetingController.greeting()
2021-02-09 16:19:52.677 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 81 ms
2021-02-09 16:19:53.239 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Created POST request for "http://localhost:9000/greeting"
2021-02-09 16:19:53.319 TRACE 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Getting http://localhost:9000/greeting
2021-02-09 16:19:53.329 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Closing HttpClient
2021-02-09 16:19:53.359 DEBUG 36330 --- [nio-9000-exec-1] ration$$EnhancerBySpringCGLIB$$bbfaedca : Creating instance of GreetingController
2021-02-09 16:19:53.424 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Created GET request for "http://localhost:9000/"
2021-02-09 16:19:53.434 TRACE 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Getting http://localhost:9000/
2021-02-09 16:19:53.444 DEBUG 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Closing HttpClient
2021-02-09 16:19:53.445 TRACE 36330 --- [nio-9000-exec-1] o.s.web.client.RestTemplate : Response 200 OK
2021-02-09 16:19:53.504 DEBUG 36330 --- [nio-9000-exec-1].a.ControllerMethodInvocableHandlerMethod : Handling "GET" dispatch for [/]
2021-02-09 16:19:53.504 DEBUG 36330 --- [nio-9000-exec-1].s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{rel=[self], href=[http://localhost:9000/]}] using [org.springframework.hateoas.MediaTypes$HalJson]
2021-02-09 16:19:53.534 DEBUG 36330 --- [nio-9000-exec-1].t.e.SpringBootErrorController : Using resolved error view in response entity.
2021-02-09 16:19:53.535 DEBUG 36330 --- [nio-9000-exec-1] f.s.WebFluxResponseStatusExceptionHandler : No match found for status code 404
2021-02-09 16:19:53.564 DEBUG 36330 --- [nio-9000-exec-1] o.s.w.s.mvc.method.RequestMappingInfoHandlerMapping : URI=/greeting
2021-02-09 16:19:53.574 DEBUG 36330 --- [nio-9000-exec-1] o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json' given [*/*] and supported [application/json, application/*+json, application/json_seq]
2021-02-09 16:19:53.584 DEBUG 36330 --- [nio-9000-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{message=Hello World!}]
2021-02-09 16:19:53.584 TRACE 36330 --- [nio-9000-exec-1] o.s.web.server.DefaultWebServer : Encoding [TextOutputMessage, CompositeByteBuf(ridx: 0, widx: 15/{length=15}, cap: 15)] using [org.springframework.http.codec.json.Jackson2CodecSupport]
2021-02-09 16:19:53.595 DEBUG 36330 --- [nio-9000-exec-1] o.s.w.s.adapter.HttpWebHandlerAdapter : [2cd8e6c1] Resolved [java.lang.String]
2021-02-09 16:19:53.595 DEBUG 36330 --- [nio-9000-exec-1] o.s.w.s.adapter.HttpWebHandlerAdapter : [2cd8e6c1] Writing [b'{"message":"Hello World!"}']
2021-02-09 16:19:53.624 DEBUG 36330 --- [ctor-http-nio-3] reactor.netty.transport.TransportConfig : Connect timeout configured so applying it globally: 5000 MILLISECONDS
2021-02-09 16:19:53.644 DEBUG 36330 --- [ctor-http-nio-3] r.n.resources.PooledConnectionProvider : [id: 0x3c0e14b5] Created a pooled channel, active connections count: 1
2021-02-09 16:19:53.654 DEBUG 36330 --- [ctor-http-nio-3] r.n.resources.PooledConnectionProvider : [id: 0x3c0e14b5, L:/127.0.0.1:61142! R:localhost/127.0.0.1:6831] Registering pool release on close event for channel
2021-02-09 16:19:53.674 DEBUG 36330 --- [ctor-http-nio-3] r.n.http.client.HttpClientOperations : [id: 0x3c0e14b5, L:/127.0.0.1:61142! R:localhost/127.0.0.1:6831] Received response (auto-read:false) : [DefaultHttpResponse(decodeResult: success, version: HTTP/1.1)
HTTP/1.1 200 OK
connection: keep-alive
transfer-encoding: chunked
content-type: application/json;charset=UTF-8
4b
{"message":"Hello World!"}
0
]
2021-02-09 16:19:53.685 DEBUG 36330 --- [ctor-http-nio-3] r.n.resources.PooledConnectionProvider : [id: 0x3c0e14b5, L:/127.0.0.1:61142! R:localhost/127.0.0.1:6831] Channel cleaned up, now 0 active connections and 1 inactive connections total
2021-02-09 16:19:53.685 DEBUG 36330 --- [ctor-http-nio-3] r.n.http.client.HttpClientOperations : [id: 0x3c0e14b5, L:/127.0.0.1:61142! R:localhost/127.0.0.1:6831] Received last chunk
可以看到,Jaeger client 已经创建了一个名为 "greeting"
的 span,并将其设置为它的父级 span,然后将其开启。