分布式追踪方案.md
参考文档
- OpenTracing官网
- OpenTracing标准(中文版)
- OpenTracing:开放式分布式追踪规范
- 开放分布式追踪(OpenTracing)入门与 Jaeger 实现
- service mesh istio微服务实验之监控日志与可视化
- Istio是啥?一文带你彻底了解!
- 【go-micro实践】jaeger分布式链路追踪
- 通过 Jaeger 上报 Java 应用数据
- Configuring Jaeger in Spring application
- Jaeger Tracing(Open Tracing) 遇到线程池哑火
- 一个java.lang.NoSuchMethodError问题的解决
- Spring MVC【入门】就这一篇!
- 详述 IntelliJ IDEA 创建 Maven 项目及设置 java 源目录的方法
为什么需要追踪
容器、Serverless 编程方式的诞生极大提升了软件交付与部署的效率。在架构的演化过程中,可以看到两个变化:
- 应用架构开始从单体系统逐步转变为微服务,其中的业务逻辑随之而来就会变成微服务之间的调用与请求。
- 资源角度来看,传统服务器这个物理单位也逐渐淡化,变成了看不见摸不到的虚拟资源模式
从以上两个变化可以看到这种弹性、标准化的架构背后,原先运维与诊断的需求也变得越来越复杂。为了应对这种变化趋势,诞生一系列面向 DevOps 的诊断与分析系统,包括集中式日志系统(Logging),集中式度量系统(Metrics)和分布式追踪系统(Tracing)。
Tracing 的诞生
Tracing 是在90年代就已出现的技术。但真正让该领域流行起来的还是源于 Google 的一篇论文"Dapper, a Large-Scale Distributed Systems Tracing Infrastructure",而另一篇论文"Uncertainty in Aggregate Estimates from Sampled Distributed Traces"中则包含关于采样的更详细分析。论文发表后一批优秀的 Tracing 软件孕育而生,比较流行的有:
- Dapper(Google) : 各 tracer 的基础
- StackDriver Trace (Google)
- Zipkin(twitter)
- Appdash(golang)
- 鹰眼(taobao)
- 谛听(盘古,阿里云云产品使用的Trace系统)
- 云图(蚂蚁Trace系统)
- sTrace(神马)
- X-ray(aws)
分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示。
下图是一个分布式调用的例子,客户端发起请求,请求首先到达负载均衡器,接着经过认证服务,计费服务,然后请求资源,最后返回结果。
数据被采集存储后,分布式追踪系统一般会选择使用包含时间轴的时序图来呈现这个 Trace。
Opentracing规范介绍
OpenTracing是一个跨编程语言的标准。
OpenTracing数据模型
Trace定义
OpenTracing中的Trace(调用链)通过归属于此调用链的Span来隐性的定义。 特别说明,一条Trace(调用链)可以被认为是一个由多个Span组成的有向无环图(DAG图), Span与Span的关系被命名为References。
注: Span,可以被翻译为跨度,可以被理解为一次方法调用, 一个程序块的调用, 或者一次RPC/数据库访问.只要是一个具有完整时间周期的程序访问,都可以被认为是一个span.在此译本中,为了便于理解,Span和其他标准内声明的词汇,全部不做名词翻译。
例如:下面的示例Trace就是由8个Span组成:
Trace因果关系图
单个Trace中,span间的因果关系
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被调用, FollowsFrom)
Tarce时序图
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
span介绍
span状态
每个Span包含以下的状态:
-
An operation name,操作名称
-
A start timestamp,起始时间
-
A finish timestamp,结束时间
-
Span Tag,一组键值对构成的Span标签集合。键值对中,键必须为string,值可以是字符串,布尔,或者数字类型。
-
Span Log,一组span的日志集合。
每次log操作包含一个键值对,以及一个时间戳。
键值对中,键必须为string,值可以是任意类型。
但是需要注意,不是所有的支持OpenTracing的Tracer,都需要支持所有的值类型。 -
SpanContext,Span上下文对象 (下面会详细说明)
-
References(Span间关系),相关的零个或者多个Span(Span间通过SpanContext建立这种关系)
每一个SpanContext包含以下状态: -
任何一个OpenTracing的实现,都需要将当前调用链的状态(例如:trace和span的id),依赖一个独特的Span去跨进程边界传输
-
Baggage Items,Trace的随行数据,是一个键值对集合,它存在于trace中,也需要跨进程边界传输
span间关系
一个Span可以与一个或者多个SpanContexts存在因果关系。OpenTracing目前定义了两种关系:ChildOf
(父子) 和 FollowsFrom
(跟随)。这两种关系明确的给出了两个父子关系的Span的因果模型。 将来,OpenTracing可能提供非因果关系的span间关系。(例如:span被批量处理,span被阻塞在同一个队列中,等等)。
ChildOf
引用: 一个span可能是一个父级span的孩子,即"ChildOf"关系。在"ChildOf"引用关系下,父级span某种程度上取决于子span。下面这些情况会构成"ChildOf"关系:
- 一个RPC调用的服务端的span,和RPC服务客户端的span构成ChildOf关系
- 一个sql insert操作的span,和ORM的save方法的span构成ChildOf关系
- 很多span可以并行工作(或者分布式工作)都可能是一个父级的span的子项,他会合并所有子span的执行结果,并在指定期限内返回
下面都是合理的表述一个"ChildOf"关系的父子节点关系的时序图。
[-Parent Span---------]
[-Child Span----]
[-Parent Span--------------]
[-Child Span A----]
[-Child Span B----]
[-Child Span C----]
[-Child Span D---------------]
[-Child Span E----]
FollowsFrom
引用: 一些父级节点不以任何方式依赖他们子节点的执行结果,这种情况下,我们说这些子span和父span之间是"FollowsFrom"的因果关系。"FollowsFrom"关系可以被分为很多不同的子类型,未来版本的OpenTracing中将正式的区分这些类型
下面都是合理的表述一个"FollowFrom"关系的父子节点关系的时序图。
[-Parent Span-] [-Child Span-]
[-Parent Span--]
[-Child Span-]
[-Parent Span-]
[-Child Span-]
Opentracing API
OpenTracing标准中有三个重要的相互关联的类,分别是Tracer
, Span
和 SpanContext
。下面,我们分别描述每种类的行为,一般来说,每个行为都会在各语言实现层面上,会演变成一个方法,而实际上由于方法重载,很可能演变成一系列相似的方法。
当我们讨论“可选”参数时,需要强调的是,不同的语言针对可选参数有不同理解,概念和实现方式 。例如,在Go中,我们习惯使用"functional Options",而在Java中,我们可能使用builder模式。
Tracer
Tracer
接口用来创建Span
,以及处理如何处理Inject
(serialize) 和 Extract
(deserialize),用于跨进程边界传递。它具有如下官方能力:
创建一个新Span
必填参数
- operation name, 操作名, 一个具有可读性的字符串,代表这个span所做的工作(例如:RPC方法名,方法名,或者一个大型计算中的某个阶段或子任务)。操作名应该是一个抽象、通用,明确、具有统计意义的名称。因此,
"get_user"
作为操作名,比"get_user/314159"
更好。
例如,假设一个获取账户信息的span会有如下可能的名称:
操作名 | 指导意见 |
---|---|
get | 太抽象 |
get_account/792 | 太明确 |
get_account | 正确的操作名,关于account_id=792 的信息应该使用Tag操作 |
可选参数
- 零个或者多个关联(references)的
SpanContext
,如果可能,同时快速指定关系类型,ChildOf
还是FollowsFrom
。 - 一个可选的显性传递的开始时间;如果忽略,当前时间被用作开始时间。
- 零个或者多个tag。
返回值,返回一个已经启动Span
实例(已启动,但未结束。译者注:英语上started和finished理解容易混淆)
将SpanContext
上下文Inject(注入)到carrier
必填参数
- **
SpanContext
**实例 - format(格式化)描述,一般会是一个字符串常量,但不做强制要求。通过此描述,通知
Tracer
实现,如何对SpanContext
进行编码放入到carrier中。 - carrier,根据format确定。
Tracer
实现根据format声明的格式,将SpanContext
序列化到carrier对象中。
将SpanContext
上下文从carrier中Extract(提取)
必填参数
- format(格式化)描述,一般会是一个字符串常量,但不做强制要求。通过此描述,通知
Tracer
实现,如何从carrier中解码SpanContext
。 - carrier,根据format确定。
Tracer
实现根据format声明的格式,从carrier中解码SpanContext
。
返回值,返回一个SpanContext
实例,可以使用这个SpanContext
实例,通过Tracer
创建新的Span
。
注意,对于Inject(注入)和Extract(提取),format是必须的。
Inject(注入)和Extract(提取)依赖于可扩展的format参数。format参数规定了另一个参数"carrier"的类型,同时约束了"carrier"中SpanContext
是如何编码的。所有的Tracer实现,都必须支持下面的format。
- Text Map: 基于字符串:字符串的map,对于key和value不约束字符集。
- HTTP Headers: 适合作为HTTP头信息的,基于字符串:字符串的map。(RFC 7230.在工程实践中,如何处理HTTP头具有多样性,强烈建议tracer的使用者谨慎使用HTTP头的键值空间和转义符)
- Binary: 一个简单的二进制大对象,记录
SpanContext
的信息。
Span
当Span
结束后(span.finish()
),除了通过Span
获取SpanContext
外,下列其他所有方法都不允许被调用。
获取Span
的SpanContext
不需要任何参数。
返回值,Span
构建时传入的SpanContext
。这个返回值在Span
结束后(span.finish()
),依然可以使用。
复写操作名(operation name)
必填参数
- 新的操作名operation name,覆盖构建
Span
时,传入的操作名。
结束Span
可选参数
- 一个明确的完成时间;如果省略此参数,使用当前时间作为完成时间。
为Span
设置tag
必填参数
- tag key,必须是string类型
- tag value,类型为字符串,布尔或者数字
注意,OpenTracing标准包含**“standard tags,标准Tag”**,此文档中定义了Tag的标准含义。
Log结构化数据
必填参数
- 一个或者多个键值对,其中键必须是字符串类型,值可以是任意类型。某些OpenTracing实现,可能支持更多的log值类型。
可选参数
- 一个明确的时间戳。如果指定时间戳,那么它必须在span的开始和结束时间之内。
注意,OpenTracing标准包含**“standard log keys,标准log的键”**,此文档中定义了这些键的标准含义。
设置一个baggage(随行数据)元素
Baggage元素是一个键值对集合,将这些值设置给给定的Span
,Span
的SpanContext
,以及所有和此Span
有直接或者间接关系的本地Span
。 也就是说,baggage元素随trace一起保持在带内传递。(译者注:带内传递,在这里指,随应用程序调用过程一起传递)
Baggage元素具有强大的功能,使得OpenTracing能够实现全栈集成(例如:任意的应用程序数据,可以在移动端创建它,显然的,它会一直传递了系统最底层的存储系统),同时他也会产生巨大的开销,请小心使用此特性。
再次强调,请谨慎使用此特性。每一个键值都会被拷贝到每一个本地和远程的下级相关的span中,因此,总体上,他会有明显的网络和CPU开销。
必填参数
- baggage key, 字符串类型
- baggage value, 字符串类型
获取一个baggage元素
必填参数
- baggage key, 字符串类型
返回值,相应的baggage value,或者可以标识元素值不存在的返回值(译者注:如Null)。
SpanContext
相对于OpenTracing中其他的功能,SpanContext
更多的是一个“概念”。也就是说,OpenTracing实现中,需要重点考虑,并提供一套自己的API。
OpenTracing的使用者仅仅需要,在创建span、向传输协议Inject(注入)和从传输协议中Extract(提取)时,使用SpanContext
和references,
OpenTracing要求,SpanContext
是不可变的,目的是防止由于Span
的结束和相互关系,造成的复杂生命周期问题。
遍历所有的baggage元素
遍历模型依赖于语言,实现方式可能不一致。在语义上,要求调用者可以通过给定的SpanContext
实例,高效的遍历所有的baggage元素
NoopTracer
所有的OpenTracing API实现,必须提供某种方式的NoopTracer
实现。NoopTracer
可以被用作控制或者测试时,进行无害的inject注入(等等)。例如,在 OpenTracing-Java实现中,NoopTracer
在他自己的模块中。
可选 API 元素
有些语言的OpenTracing实现,为了在串行处理中,传递活跃的Span
或SpanContext
,提供了一些工具类。例如,opentracing-go
中,通过context.Context
机制,可以设置和获取活跃的Span
。
方案
1.jaeger
2.zipkin
jaeger介绍
Jaeger 是 Uber 推出的一款开源分布式追踪系统,兼容 OpenTracing API。
Jaeger 架构
如上图所示,Jaeger 主要由以下几部分组成。
- Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 - jaeger-agent。
- Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
- Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
- Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
- Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。
spring中添加jaeger客户端方法介绍
手动埋点
1.打开pom.xml,添加对jaeger客户端的依赖
<dependencies>
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-spring-cloud-starter</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>0.35.0</version>
</dependency>
</dependencies>
2.配置初始化参数并创建 Tracer(spring配置)。
Tracer 对象可以用来创建 Span 对象(记录分布式操作时间)、跨机器透传数据(Extract/Inject 方法),或设置当前 Span(activeSpan)。Tracer 对象还配置了上报数据的网关地址、本机 IP、采样率、服务名等数据。您可以通过调整采样率来减少因上报数据产生的开销。
io.jaegertracing.Configuration config = new io.jaegertracing.Configuration("manualDemo");
io.jaegertracing.Configuration.SenderConfiguration sender = new io.jaegertracing.Configuration.SenderConfiguration();
// 将 <endpoint> 替换为控制台概览页面上相应客户端和相应地域的接入点
sender.withEndpoint("<endpoint>");
config.withSampler(new io.jaegertracing.Configuration.SamplerConfiguration().withType("const").withParam(1));
config.withReporter(new io.jaegertracing.Configuration.ReporterConfiguration().withSender(sender).withMaxQueueSize(10000));
GlobalTracer.register(config.getTracer());
3.记录请求数据。
// 获取tracer
Tracer tracer = GlobalTracer.get();
// 创建span
Span span = tracer.buildSpan("parentSpan").withTag("myTag", "spanFrist").start();
// try 结束后自动scope自动close,还原activeSpan
try (Scope ignored = tracer.activateSpan(span)) {
tracer.activeSpan().setTag("methodName", "testTracing");
// .... 业务逻辑
secondBiz();
} catch (Exception e) {
//记录异常信息
TracingHelper.onError(e, span);
throw e;
} finally {
span.finish();
}
4.(可选)为了方便排查问题,您可以为某个记录添加一些自定义标签(Tag),例如记录是否发生错误、请求的返回值等。
tracer.activeSpan().setTag("methodName", "testCall");
5.在分布式系统中发送 RPC 请求时会带上 Tracing 数据,包括 TraceId、ParentSpanId、SpanId、Sampled 等。您可以在 HTTP 请求中使用 Extract/Inject 方法在 HTTP Request Headers 上透传数据。总体流程如下:
a.在客户端调用 Inject 方法传入 Context 信息。
private void attachTraceInfo(Tracer tracer, Span span, final Request request) {
tracer.inject(span.context(), Format.Builtin.TEXT_MAP, new TextMap() {
@Override
public void put(String key, String value) {
request.setHeader(key, value);
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException("TextMapInjectAdapter should only be used with Tracer.inject()");
}
});
}
b.在服务端调用 Extract 方法解析 Context 信息。
protected Span extractTraceInfo(Request request, Tracer tracer) {
Tracer.SpanBuilder spanBuilder = tracer.buildSpan("/api/xtrace/test03");
try {
SpanContext spanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapExtractAdapter(request.getAttachments()));
if (spanContext != null) {
spanBuilder.asChildOf(spanContext);
}
} catch (Exception e) {
spanBuilder.withTag("Error", "extract from request fail, error msg:" + e.getMessage());
}
return spanBuilder.start();
}
组件埋点
目前 OpenTracing 社区已有许多组件可支持各种 Java 框架,例如:
请下载demo工程,进入 springboot-jaeger-demo 目录,并按照 Readme 的说明运行程序。
1.打开pom.xml,添加对jaeger客户端的依赖
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-spring-cloud-starter</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>io.jaegertracing</groupId>
<artifactId>jaeger-client</artifactId>
<version>0.35.0</version>
</dependency>
2.添加 OpenTracing Tracer Bean
@Bean
public io.opentracing.Tracer tracer() {
io.jaegertracing.Configuration config = new io.jaegertracing.Configuration("springFrontend");
io.jaegertracing.Configuration.SenderConfiguration sender = new io.jaegertracing.Configuration.SenderConfiguration();
// 将 <endpoint> 替换为控制台概览页面上相应客户端和相应地域的接入点
sender.withEndpoint("<endpoint>");
config.withSampler(new io.jaegertracing.Configuration.SamplerConfiguration().withType("const").withParam(1));
config.withReporter(new io.jaegertracing.Configuration.ReporterConfiguration().withSender(sender).withMaxQueueSize(10000));
return config.getTracer();
}
效果图
go中添加jaeger客户端方法介绍
Demo(https://github.com/david-zh-cn/JaegerDemo)
里面有dubbo过滤器jaeger集成方案,简略版(之前需要2019年写的,如果问题,如果好的想法,或者项目需要,可以沟通,邮箱358313250@qq.com,欢迎沟通)