Jaeger入门教程:拥抱分布式追踪的必备利器
Jaeger是一款开源的分布式追踪系统,专为微服务架构设计,可帮助开发者监控和排查复杂系统中的问题。文章介绍了Jaeger的核心概念(Trace、Span、SpanContext等)及其灵活架构(Client、Agent、Collector等组件),并提供了Docker和Kubernetes的快速部署方案。此外,还展示了如何在Java和Go应用中集成Jaeger客户端,通过示例代码演示了创建Spa
文章目录
在当今微服务架构大行其道的时代,我们的应用系统往往由数十甚至上百个服务组成。当一个请求在这些服务之间传递时,如何跟踪它的完整旅程?如何找出性能瓶颈?这就是分布式追踪系统大显身手的时候了!而Jaeger作为CNCF毕业项目,绝对是这个领域中的佼佼者。
什么是Jaeger?为什么需要它?
Jaeger(德语中的"猎人")是一个开源的端到端分布式追踪系统,最初由Uber开发,后来贡献给了云原生计算基金会(CNCF)。它的灵感来源于Google的Dapper和Twitter的Zipkin,专为微服务环境设计,帮助我们监控和排查复杂分布式系统中的问题。
说实话,在我刚接触微服务架构时,就碰到了一个让人抓狂的问题 —— 一个API请求突然变慢了,但到底是哪个服务出了问题?没有分布式追踪系统,简直就像大海捞针!
Jaeger能够解决这些问题:
- 分布式事务监控
- 性能和延迟优化
- 根本原因分析
- 服务依赖性分析
- 分布式上下文传播
Jaeger的核心概念
在深入了解Jaeger之前,我们需要先搞明白一些基本概念(这部分超级重要)!
Trace(追踪)
一个Trace代表了一个事务或请求在分布式系统中的完整旅程。想象一下,当你在电商网站点击"下单"按钮时,这个请求可能需要经过用户服务、库存服务、支付服务等多个微服务才能完成。Trace就是这整个过程的记录。
Span(跨度)
Span是分布式追踪的基本单位,代表了一个工作单元。每个Span包含名称、开始和结束时间戳、Span上下文、Tags、Logs等信息。
在一个Trace中,第一个Span被称为Root Span,其他Span都是它的子Span。Span之间通过父子关系形成一个树状结构。
想象一下,如果把Trace比作一次旅行,那Span就是旅行中的每一段行程。
SpanContext(跨度上下文)
SpanContext包含了Trace ID、Span ID以及其他需要跨进程边界传播的数据。它使得不同进程中的Span能够关联到同一个Trace。
Tags vs Logs
- Tags:键值对,用于标记Span(例如:http.method=“GET”)
- Logs:带时间戳的事件,记录Span执行过程中的特定时刻发生的事情
Jaeger架构解析
Jaeger的架构设计得相当灵活,主要由以下几个组件构成:
Jaeger Client(客户端)
这是嵌入到应用程序代码中的库,负责创建Spans并发送给Jaeger Agent。Jaeger官方提供了多种语言的客户端实现,包括Go、Java、Node.js、Python、C++等。
客户端库实现了OpenTracing API,这意味着如果你的代码已经使用了OpenTracing,切换到Jaeger只需要更换一下Tracer的实现即可!
Jaeger Agent(代理)
Jaeger Agent是一个网络守护进程,监听通过UDP发送的spans,并将它们批量发送到Collector。Agent通常部署为基础设施组件,每台主机一个。这种设计让应用程序不需要知道Collector的位置。
Jaeger Collector(收集器)
Collector从Agent接收traces,对其进行验证和处理,然后将其保存到存储后端。Collector设计为无状态的,因此你可以同时运行任意数量的Collector实例。
Storage(存储)
Jaeger支持多种存储后端:
- Cassandra
- Elasticsearch
- Kafka(作为缓冲区)
- BadgerDB(仅用于开发环境)
存储选择取决于你的规模和需求。对于大多数中小型项目,Elasticsearch可能是个不错的选择,因为它既可以存储数据,又提供了强大的搜索功能。
Jaeger Query(查询服务)
Query服务提供了API来从存储中检索traces,并为Jaeger UI提供数据支持。
Jaeger UI(用户界面)
Jaeger提供了一个漂亮的Web UI,让你可以搜索和查看traces,分析性能问题。UI的功能包括:
- 根据服务、操作、标签等搜索traces
- 查看完整的trace详情及其包含的spans
- 查看系统架构的依赖关系图
快速上手:部署Jaeger
好了,理论知识讲完了,让我们动手实践一下!Jaeger提供了多种部署方式,最简单的是使用官方提供的all-in-one Docker镜像。
使用Docker部署All-in-One版本
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:1.39
这个命令会启动一个包含所有Jaeger组件的容器。容器启动后,你可以通过访问 http://localhost:16686 来打开Jaeger UI。
不过,all-in-one版本使用内存存储,仅适合开发和测试环境。对于生产环境,你需要配置持久化存储如Elasticsearch或Cassandra。
使用Kubernetes部署
如果你的应用运行在Kubernetes集群中,可以使用Jaeger Operator来部署Jaeger:
- 首先安装Jaeger Operator:
kubectl create namespace observability
kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.39.0/jaeger-operator.yaml -n observability
- 然后创建一个简单的Jaeger实例:
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: simple-prod
spec:
strategy: production
storage:
type: elasticsearch
elasticsearch:
nodeCount: 3
resources:
requests:
cpu: 1
memory: 1Gi
limits:
memory: 1Gi
将上面的YAML保存为jaeger.yaml,然后执行:
kubectl apply -f jaeger.yaml -n observability
在应用中集成Jaeger
理论和部署搞定了,现在让我们看看如何在实际应用中使用Jaeger!我会展示几种常见编程语言的集成方式。
Java应用集成
对于Java应用,你可以使用jaeger-client库。首先,添加依赖:
<!-- Maven -->
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>1.8.0</version>
</dependency>
或者Gradle:
implementation 'io.jaegertracing:jaeger-client:1.8.0'
然后,初始化Jaeger Tracer:
import io.jaegertracing.Configuration;
import io.opentracing.Span;
import io.opentracing.Tracer;
public class JaegerExample {
public static void main(String[] args) {
// 初始化Tracer
Tracer tracer = Configuration.fromEnv("my-service").getTracer();
// 创建一个Span
Span span = tracer.buildSpan("say-hello").start();
try {
// 业务逻辑
span.setTag("hello-to", "World");
// 记录一个事件
span.log("开始处理请求");
// 模拟处理过程
Thread.sleep(100);
// 记录另一个事件
span.log("请求处理完成");
} catch (InterruptedException e) {
// 记录错误
span.setTag("error", true);
span.log(Map.of("event", "error", "message", e.getMessage()));
} finally {
// 完成Span
span.finish();
}
// 关闭Tracer,确保所有Span都被发送
((io.jaegertracing.internal.JaegerTracer)tracer).close();
}
}
Go应用集成
对于Go应用,可以使用官方的jaeger-client-go库:
package main
import (
"context"
"log"
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
)
func main() {
// 初始化Jaeger Tracer
cfg := &config.Configuration{
ServiceName: "my-service",
Sampler: &config.SamplerConfig{
Type: "const",
Param: 1,
},
Reporter: &config.ReporterConfig{
LogSpans: true,
},
}
tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
if err != nil {
log.Fatalf("无法创建tracer: %v", err)
}
defer closer.Close()
// 将tracer设置为全局tracer
opentracing.SetGlobalTracer(tracer)
// 创建一个span
span := tracer.StartSpan("say-hello")
defer span.Finish()
// 设置一个tag
span.SetTag("hello-to", "World")
// 记录一个事件
span.LogKV("event", "开始处理请求")
// 模拟处理过程
time.Sleep(100 * time.Millisecond)
// 记录另一个事件
span.LogKV("event", "请求处理完成")
}
Python应用集成
对于Python应用,你可以使用jaeger-client库:
import time
from jaeger_client import Config
def init_tracer(service_name):
config = Config(
config={
'sampler': {
'type': 'const',
'param': 1,
},
'logging': True,
},
service_name=service_name,
)
return config.initialize_tracer()
def main():
# 初始化tracer
tracer = init_tracer('my-service')
# 创建一个span
with tracer.start_span('say-hello') as span:
# 设置tag
span.set_tag('hello-to', 'World')
# 记录事件
span.log_kv({'event': '开始处理请求'})
# 模拟处理过程
time.sleep(0.1)
# 记录另一个事件
span.log_kv({'event': '请求处理完成'})
# 确保span被发送出去
time.sleep(2)
tracer.close()
if __name__ == '__main__':
main()
Node.js应用集成
对于Node.js应用,你可以使用jaeger-client库:
const initJaegerTracer = require('jaeger-client').initTracer;
// 初始化Tracer
function initTracer(serviceName) {
const config = {
serviceName: serviceName,
sampler: {
type: 'const',
param: 1,
},
reporter: {
logSpans: true,
},
};
const options = {
logger: {
info(msg) {
console.log('INFO', msg);
},
error(msg) {
console.error('ERROR', msg);
},
},
};
return initJaegerTracer(config, options);
}
const tracer = initTracer('my-service');
// 创建一个span
const span = tracer.startSpan('say-hello');
// 设置tag
span.setTag('hello-to', 'World');
// 记录事件
span.log({event: '开始处理请求'});
// 模拟处理过程
setTimeout(() => {
// 记录另一个事件
span.log({event: '请求处理完成'});
// 完成span
span.finish();
// 关闭tracer
tracer.close(() => {
console.log('Tracer关闭');
});
}, 100);
跨服务追踪:上下文传播
到目前为止,我们只在单个服务内创建了spans。但在微服务架构中,请求通常会跨越多个服务。那么,如何将这些服务的spans关联起来形成一个完整的trace呢?答案是:上下文传播。
上下文传播的原理是将当前span的上下文(trace ID、span ID等)通过某种方式(如HTTP头)传递给下游服务,下游服务在接收到请求时,从中提取上下文,然后创建新的子span。
HTTP请求中的上下文传播
以下是在Go中通过HTTP请求进行上下文传播的示例:
// 客户端代码
func makeRequest(ctx context.Context) {
// 创建一个span
span, ctx := opentracing.StartSpanFromContext(ctx, "makeRequest")
defer span.Finish()
// 创建HTTP请求
req, err := http.NewRequest("GET", "http://localhost:8080/api", nil)
if err != nil {
span.SetTag("error", true)
span.LogKV("event", "error", "message", err.Error())
return
}
// 将span上下文注入到HTTP头中
tracer := opentracing.GlobalTracer()
tracer.Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header),
)
// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
span.SetTag("error", true)
span.LogKV("event", "error", "message", err.Error())
return
}
defer resp.Body.Close()
}
// 服务端代码
func handleRequest(w http.ResponseWriter, r *http.Request) {
tracer := opentracing.GlobalTracer()
// 从HTTP头中提取span上下文
spanCtx, err := tracer.Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(r.Header),
)
// 创建一个新的span,如果能提取到上下文,则将其作为父span
var span opentracing.Span
if err != nil {
span = tracer.StartSpan("handleRequest")
} else {
span = tracer.StartSpan("handleRequest", opentracing.ChildOf(spanCtx))
}
defer span.Finish()
// 处理请求...
}
常见框架的集成
很多流行的框架和中间件都提供了与OpenTracing/Jaeger的集成:
- Spring Boot: 使用
spring-cloud-sleuth-jaeger
可以轻松集成 - Express.js: 可以使用
jaeger-client
提供的中间件 - gRPC: 提供了拦截器接口,可用于注入和提取追踪上下文
高级功能与最佳实践
采样策略
在生产环境中,记录每个请求的所有spans可能会产生大量数据。Jaeger提供了多种采样策略:
- 常量采样:始终采样(采样率为1.0)或从不采样(采样率为0)
- 概率采样:以指定的概率采样
- 速率限制采样:限制每秒采样的trace数量
- 远程控制采样:从Jaeger后端获取采样策略
例如,在Java中配置概率采样:
tracer = new Configuration("my-service")
.withSampler(new Configuration.SamplerConfiguration()
.withType("probabilistic")
.withParam(0.1)) // 10%的采样率
.withReporter(new Configuration.ReporterConfiguration()
.withLogSpans(true))
.getTracer();
使用Baggage Items传递数据
除了传递trace和span IDs外,Jaeger还支持Baggage Items,这是一种键值对,会随着trace上下文一起传播到所有下游spans。
Baggage Items对于传递诸如用户ID、请求ID等需要在整个trace中可用的信息非常有用。但要注意,它们会增加网络和CPU开销,因此应该谨慎使用。
// 在上游服务中设置
span.setBaggageItem("user-id", "123");
// 在下游服务中获取
String userId = span.getBaggageItem("user-id");
集成日志系统
为了更好地排查问题,将分布式追踪与日志系统集成是一个好主意。通常的做法是在日志中包含trace ID和span ID,这样你就可以在日志和追踪系统之间建立关联。
以下是在Java中使用SLF4J MDC(Mapped Diagnostic Context)的示例:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
public void processRequest() {
Span span = tracer.buildSpan("processRequest").start();
try (Scope scope = tracer.scopeManager().activate(span)) {
// 将trace ID和span ID添加到MDC
MDC.put("traceId", span.context().toTraceId());
MDC.put("spanId", span.context().toSpanId());
// 现在日志会包含trace ID和span ID
logger.info("开始处理请求");
// 业务逻辑...
logger.info("请求处理完成");
} finally {
MDC.clear();
span.finish();
}
}
服务健康监控
Jaeger提供了一些指标来监控自身组件的健康状况,如spans发送成功率、队列长度等。这些指标可以通过Prometheus收集并在Grafana中可视化。
故障排查案例
让我分享一个我亲身经历的案例,展示Jaeger如何帮助我们解决微服务架构中的性能问题。
在我们的电商系统中,用户反馈下单页面响应很慢。通过Jaeger UI,我们查询了相关traces,发现问题出在订单服务调用库存服务时:
- 订单服务在处理请求时,会并行查询多个商品的库存
- 但库存服务的查询方法没有做到并发安全,导致每个查询都会获取一个数据库连接
- 当连接池耗尽时,后续请求就会被阻塞
通过Jaeger的可视化界面,我们清晰地看到了这些查询的时间重叠,以及等待数据库连接的长时间延迟。
解决方案是优化库存服务的查询方法,改为批量查询,一次获取所有商品的库存信息。优化后,订单页面的响应时间从原来的3秒缩短到了300毫秒!!!
总结
Jaeger作为一个强大的分布式追踪系统,为我们提供了在复杂微服务架构中监控和排查问题的能力。通过它,我们可以:
- 可视化请求流程,了解服务间的调用关系
- 识别性能瓶颈,优化系统响应时间
- 分析系统行为,发现异常模式
- 监控服务健康状况,及时发现问题
随着微服务架构的普及,Jaeger这样的分布式追踪系统已经成为现代应用监控体系的必备组件。如果你正在构建或维护微服务系统,强烈建议你将Jaeger纳入你的工具箱!
希望这篇教程能帮助你快速上手Jaeger,开启分布式追踪之旅。要记住,Rome wasn’t built in a day,熟练使用Jaeger需要实践和经验积累。慢慢来,相信你很快就能掌握这个强大的工具!
在实际应用中如果遇到任何问题,可以查阅Jaeger的官方文档或社区资源。Jaeger社区非常活跃,有大量的案例和最佳实践可供参考。
加油,祝你在微服务之旅中一切顺利!
更多推荐
所有评论(0)