概念
当咱们把系统微服务化后,想查询某个接口一次请求的耗时信息,须要登陆多台机器查询相关日志才行。 以下图所示架构,当对应服务集群化部署后,想要查询到某一次请求信息更是难上加难。那咱们有什么办法能够解决这个问题么?html
答案固然是有的,分布式追踪系统正是为了解决这个问题而生。分布式跟踪为描述和分析跨进程事务提供了一种解决方案。如Google Dapper论文 (业界的分布式追踪系统基本都是以这篇论文为基础进行实现)所述,分布式跟踪的一些使用场景包括:java
- 异常检测,问题诊断
- 分布式系统内各组件的调用状况
- 性能/延迟优化
- 服务依赖性分析
由于业界分布式追踪系统众多,各家Api定义上有必定的差别,为了统一标准,因而OpenTracing出现了。node
什么是OpenTracing?python
OpenTracing经过提供平台无关、厂商无关的API,使得开发人员可以方便的添加或更换追踪系统的实现。OpenTracing正在为全球的分布式追踪,提供统一的概念和数据标准。git
除了OpenTracing外,还有OpenCensus 这个项目,OpenCensus 由google发起,它除了包含tracing外,还包含度量(metrics)。github
两套分布式追踪框架,都有不少追随者,都想统一对方,但最终结果是对峙不下,最后两个组织一块儿组队新建了OpenTelemetry项目。项目的第一宗旨就是:兼容OpenTracing和OpenSensus。对于使用OpenTracing或OpenSensus的应用不须要从新改动就能够接入OpenTelemetry。sql
模型
从应用角度看分布式追踪系统所处的位置docker
示例
先来看两张效果图感觉一下(以jaeger ui为例) shell
语义
这里的语义以 OpenTracing 为基础。数据库
数据模型
[Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G `FollowsFrom` Span F)
时序图
有些时候,使用下面这种基于时间轴的时序图能够更好的展示Trace(调用链)
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··]
这种展示方式增长显示了执行时间的上下文,相关服务间的层次关系,进程或者任务的串行或并行调用关系。这样的视图有助于发现系统调用的关键路径。经过关注关键路径的执行过程,项目团队可专一于优化路径中的关键位置,最大幅度的提高系统性能。
Trace
一条Trace是指一个请求包含的调用链(包含下游全部请求的调用链), 一条Trace能够被认为是一个由多个Span组成的有向无环图, Span与Span的关系被命名为References。
Operation Names
每个Span都有一个操做名称,这个名称简单,并具备可读性高。(例如:一个RPC方法的名称,一个函数名,或者一个大型计算过程当中的子任务或阶段)
Span
一个Span表明系统中具备开始时间和执行时长的逻辑运行单元 。具体能够理解为一次方法调用, 一个程序块的调用或者一次RPC/数据库访问。
每一个Span包含如下的状态:
- An operation name,操做名称
- A start timestamp,起始时间
- A finish timestamp,结束时间
- Span Tags,一组键值对构成的Span标签集合。键值对中,键必须为string,值能够是字符串,布尔,或者数字类型。
- Span Logs,一组span的日志集合。 每次log操做包含一个键值对,以及一个时间戳。 键值对中,键必须为string,值能够是任意类型。 可是须要注意,不是全部的支持OpenTracing的Tracer,都须要支持全部的值类型。
- SpanContext,Span上下文对象 (下面会详细说明)
- References(Span间关系),相关的零个或者多个Span(Span间经过SpanContext创建这种关系)
每个SpanContext包含如下状态:
- 任何一个OpenTracing的实现,都须要将当前调用链的状态(例如: spanID 和traceID ),依赖一个独特的Span去跨进程边界传输
- Baggage Items,Trace的随行数据,是一个键值对集合,它存在于trace中,也须要跨进程边界传输
References
一个Span能够和一个或者多个Span间存在因果关系。 OpenTracing定义了两种关系:ChildOf
和 FollowsFrom
。这两种引用类型表明了子节点和父节点间的直接因果关系。
ChildOf
引用
一个Span多是一个父级Span的孩子,即ChildOf
关系。在ChildOf
引用关系下,父级span某种程度上取决于子Span。下面这些状况会构成ChildOf
关系:
- 一个RPC调用的服务端的Span,和RPC服务客户端的Span构成
ChildOf
关系 - 一个sql insert操做的Span,和ORM的save方法的Span构成
ChildOf
关系 - 不少span能够并行工做(或者分布式工做)均可能是一个父级的Span的子项,他会合并全部子Span的执行结果,并在指按期限内返回
下面表述一个ChildOf
关系的父子节点关系的时序图:
FollowsFrom
引用
一些父级节点不以任何方式依然他们子节点的执行结果,这种状况下,咱们说这些子Span和父Span之间是FollowsFrom
的因果关系。 下面表述一个FollowsFrom
关系的父子节点关系的时序图:
[-Parent Span-] [-Child Span-] [-Parent Span--] [-Child Span-] [-Parent Span-] [-Child Span-]
Tags
每一个Span能够有多个键值对(key:value)形式的Tags,Tags是没有时间戳的,支持简单的对Span进行注解和补充。 Span的tag不会跨进程传输,所以它们不会被子级的span继承。
必填参数
- tag key,必须是string类型
- tag value,类型为字符串,布尔或者数字 注意,OpenTracing标准包含**"standard tags,标准Tag"**,此文档中定义了Tag的标准含义。
Logs
每一个Span能够进行屡次Logs操做,每一次Logs操做,都须要一个带时间戳的时间名称,以及可选的任意大小的存储结构。 必填参数
- 一个或者多个键值对,其中键必须是字符串类型,值能够是任意类型。某些OpenTracing实现,可能支持更多的log值类型。 可选参数
- 一个明确的时间戳。若是指定时间戳,那么它必须在span的开始和结束时间以内。 注意,OpenTracing标准包含**"standard log keys,标准log的键"**,此文档中定义了这些键的标准含义。
SpanContext
SpanContext更多的是一个“概念” 。每一个Span都必须提供方法访问SpanContext。SpanContext表明跨越进程边界,传递到下级Span的状态,并用于封装Baggage 。 OpenTracing的使用者仅仅须要,在建立Span、向传输协议Inject(注入)和从传输协议中Extract(提取)时使用 。
Baggage
Baggage元素是一个键值对集合,将这些值设置给给定的Span
,Span
的SpanContext
,以及全部和此Span
有直接或者间接关系的本地Span
。 也就是说,baggage元素随Trace一块儿应用程序调用过程 一同传播
Baggage拥有强大功能,也会有很大的消耗。因为Baggage的全局传输,若是包含的数量量太大,或者元素太多,它将下降系统的吞吐量或增长RPC的延迟。
Inject and Extract
SpanContext
能够经过Injected操做向Carrier增长,或者经过Extracted从Carrier中获取,跨进程通信数据。经过这种方式,SpanContexts能够跨越进程边界,并提供足够的信息来创建跨进程的span间关系(所以能够实现跨进程连续追踪)。
- Inject 类比传递序列化后的参数
- Extract 反序列化Inject的参数值
传递方式例如:
- 依赖HTTP头传递(B3-header)
- Dubbo 定制Filter经过 RpcContext 设置 Attachment 来传递
Carrier能够是一个接口或者一个数据载体,他对于跨进程通信是十分有帮助的。Carrier负责将追踪状态从一个进程"carries"传递到另外一个进程 。OpenTracing规定全部平台的实现者支持两种Carrier格式:基于"text map"(基于字符串的map)的格式和基于"binary"(二进制)的格式。
- text map 格式的 Carrier是一个平台惯用的map格式,基于unicode编码的
字符串
对字符串
键值对 - binary 格式的 Carrier 是一个不透明的二进制数组(更紧凑和有效)
Jaeger
Jaeger是 Uber 推出的一款开源分布式追踪系统(已从CNCF毕业),兼容 OpenTracing API。 它用于监视和诊断基于微服务的分布式系统,功能包括:
- 分布式上下文传播
- 分布式链路跟踪
- 服务依赖分析
技术栈
- 基于Go实现
- 数据支持多种类型的后端存储
- Cassandra 3.4+
- Elasticsearch 5.x, 6.x, 7.x
- Kafka
- memory storage
架构
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。 当前,咱们的管道会分析数据并为其创建索引,执行任何转换并最终存储它们。 Jaeger的存储设备是一个可插拔组件,目前支持 Cassandra, Elasticsearch and Kafka 存储.
- Query - 接收查询请求,而后从后端存储系统中检索 trace 并经过 UI 进行展现.
- Ingester - 后端存储被设计成一个可插拔的组件,支持将数据写入 Cassandra, Elasticsearch.
Jaeger包含两种架构方案: 1、收集器数据直接写入存储架构(tracing数据直接写入存储)
2、收集器数据缓冲后异步写入存储架构(tracing数据经过kafka缓冲后再异步消费写入存储)
我的推荐采用第二种架构方式部署
部署
为了快速搭建Jaeger环境,这里安装基于Helm部署(须要先搭建 Kubernetes 集群),能够参考前面写的文章来搭建。从 https://github.com/jaegertracing/helm-charts/tree/master/charts/jaeger 这里能够找到详细的部署流程,能够一步一步跟着执行部署。这里采用 收集器数据直接写入存储架构 部署
helm install jaeger jaegertracing/jaeger
官方推荐使用jaeger-operator来部署,可参考: https://www.jaegertracing.io/docs/1.17/operator/ 安装完成后查看服务状态
kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE jaeger-agent ClusterIP 10.97.3.215 <none> 5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP 7m45s jaeger-cassandra ClusterIP None <none> 7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP 7m45s jaeger-collector ClusterIP 10.111.141.231 <none> 14250/TCP,14267/TCP,14268/TCP,14269/TCP 7m45s jaeger-query ClusterIP 10.97.103.64 <none> 80/TCP,16687/TCP 7m45s
要访问jaeger ui 须要查看jaeger-query
项目对外暴露的端口,咱们看到经过helm安装,咱们采用的默认配置,这里的网络类型是ClusterIP
,若是想外网访问能够先临时改为NodePort
的方式,执行以下命令编辑对应配置:
kubectl edit service jaeger-query
找到最下面的ClusterIP
改为NodePort
保存便可,保存后会自动生效
kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE jaeger-agent ClusterIP 10.97.3.215 <none> 5775/UDP,6831/UDP,6832/UDP,5778/TCP,14271/TCP 8m38s jaeger-cassandra ClusterIP None <none> 7000/TCP,7001/TCP,7199/TCP,9042/TCP,9160/TCP 8m38s jaeger-collector ClusterIP 10.111.141.231 <none> 14250/TCP,14267/TCP,14268/TCP,14269/TCP 8m38s jaeger-query NodePort 10.97.103.64 <none> 80:31067/TCP,16687:31381/TCP 8m38s
能够发现如今jaeger-query
的网络类型已经变成了NodePort
,如今能够经过流量访问Jaeger Ui了 这里的地址是 http://47.57.100.110:31067/search (注意,IP地址及端口根据本身控制台的实际输出填入就行) 进入页面后能够到刚才部署的UI界面,并查询jaeger-query
项目自己的tracing信息。 我在列表页面找到一个trace_id: 73c00aa573bf1ed0
临时保存下它,后面分析会用到,打开后界面以下。
traces存储结构
咱们能够在jaeger源代码中找到后端cassandra的存储结构,具体信息能够看这里,位置比较隐蔽:
https://github.com/jaegertracing/jaeger/blob/master/plugin/storage/cassandra/schema/v001.cql.tmpl
不过咱们能够登陆Pod查看建立后的数据结构信息(cassandra)。让咱们一探究竟,首先登入cassandra对应的docker镜像,而后经过cql 链接cassandra集群。
若是对cql不了解的能够查看对应文档: https://cassandra.apache.org/doc/latest/cql/
kubectl exec -it jaeger-cassandra-0 --container jaeger-cassandra -- /bin/bash cqlsh Connected to jaeger at 127.0.0.1:9042. [cqlsh 5.0.1 | Cassandra 3.11.6 | CQL spec 3.4.4 | Native protocol v4] Use HELP for help.
进入对应的space,查看里面对应的表信息
cqlsh> desc keyspaces; #查看有哪些keyspaces jaeger_v1_test system_auth system_distributed system_schema system system_traces cqlsh> use jaeger_v1_test; #切换到jaeger对应的space cqlsh:jaeger_v1_test> desc tables; #查看jaeger space下面的表信息 service_name_index service_names service_operation_index traces dependencies_v2 tag_index duration_index operation_names_v2
咱们能够一个一个的表信息查看。这里咱们主要看下保存咱们trace信息的表 service_name_index
cqlsh:jaeger_v1_test> desc traces; CREATE TABLE jaeger_v1_test.traces ( trace_id blob, span_id bigint, span_hash bigint, duration bigint, flags int, logs list<frozen<log>>, operation_name text, parent_id bigint, process frozen<process>, refs list<frozen<span_ref>>, start_time bigint, tags list<frozen<keyvalue>>, PRIMARY KEY (trace_id, span_id, span_hash) ) WITH CLUSTERING ORDER BY (span_id ASC, span_hash ASC) AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'HOURS', 'max_threshold': '32', 'min_threshold': '4'} AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.0 AND default_time_to_live = 172800 AND gc_grace_seconds = 10800 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 AND read_repair_chance = 0.0 AND speculative_retry = 'NONE';
还记得咱们开始保存的那个trace_id: 73c00aa573bf1ed0
么,如今咱们能够在这个表中查看它是如何保存的,咱们可使用下面的cql进行查询,查询前须要对界面上的trace_id进行补位填充0x0000000000000000
,这里必定要注意,最终在cql里面查询的trace_id为:0x000000000000000073c00aa573bf1ed0
。
cqlsh:jaeger_v1_test> expand on; Now Expanded output is enabled cqlsh:jaeger_v1_test> select * from traces where trace_id=0x000000000000000073c00aa573bf1ed0; @ Row 1 ----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- trace_id | 0x000000000000000073c00aa573bf1ed0 span_id | 2300299680491247480 span_hash | 1417161953846781420 duration | 204491 flags | 1 logs | [{ts: 1589363774632310, fields: [{key: 'event', value_type: 'string', value_string: 'searching', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'trace_id', value_type: 'string', value_string: '4c423cfb69721367', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]}] operation_name | readTrace parent_id | 0 process | {service_name: 'jaeger-query', tags: [{key: 'jaeger.version', value_type: 'string', value_string: 'Go-2.22.1', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'hostname', value_type: 'string', value_string: 'jaeger-query-55c77745b5-ff8tt', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'ip', value_type: 'string', value_string: '192.168.61.148', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'client-uuid', value_type: 'string', value_string: '363a86b295da9842', value_bool: False, value_long: 0, value_double: 0, value_binary: null}]} refs | [{ref_type: 'child-of', trace_id: 0x000000000000000073c00aa573bf1ed0, span_id: 8340678215617945296}] start_time | 1589363774627367 tags | [{key: 'db.statement', value_type: 'string', value_string: '\n\t\tSELECT trace_id, span_id, parent_id, operation_name, flags, start_time, duration, tags, logs, refs, process\n\t\tFROM traces\n\t\tWHERE trace_id = ?', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'db.type', value_type: 'string', value_string: 'cassandra', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'component', value_type: 'string', value_string: 'gocql', value_bool: False, value_long: 0, value_double: 0, value_binary: null}, {key: 'internal.span.format', value_type: 'string', value_string: 'proto', value_bool: False, value_long: 0, value_double: 0, value_binary: null}] 此处省略4个Row.... (5 rows)
由于找到这个trace_id包含了5个span,因此这里查询出来了5条记录,能够经过这段文本及上面的图片进行一一观察,能够发现存储结构仍是很是清晰的,UI界面须要展现的信息基本均可以很容易从里面取到。
咱们再回过头来看看jaeger client 库thrift的结构(源码见:jaeger.thrift)
# 标签 struct Tag { 1: required string key 2: required TagType vType 3: optional string vStr 4: optional double vDouble 5: optional bool vBool 6: optional i64 vLong 7: optional binary vBinary } # 日志 struct Log { 1: required i64 timestamp 2: required list<Tag> fields } enum SpanRefType { CHILD_OF, FOLLOWS_FROM } # Span 之间的关系 struct SpanRef { 1: required SpanRefType refType 2: required i64 traceIdLow 3: required i64 traceIdHigh 4: required i64 spanId } # Span struct Span { 1: required i64 traceIdLow # the least significant 64 bits of a traceID 2: required i64 traceIdHigh # the most significant 64 bits of a traceID; 0 when only 64bit IDs are used 3: required i64 spanId # unique span id (only unique within a given trace) 4: required i64 parentSpanId # since nearly all spans will have parents spans, CHILD_OF refs do not have to be explicit 5: required string operationName 6: optional list<SpanRef> references # causal references to other spans 7: required i32 flags # a bit field used to propagate sampling decisions. 1 signifies a SAMPLED span, 2 signifies a DEBUG span. 8: required i64 startTime 9: required i64 duration 10: optional list<Tag> tags 11: optional list<Log> logs }
基本上能够跟存储的数据结构一一对应上。
采样策略
Jaeger客户端支持4种采样策略,分别是:
- Constant (
sampler.type=const
) 采样率的可设置的值为 0 和 1,分别表示关闭采样和所有采样 - Probabilistic (
sampler.type=probabilistic
) 按照几率采样,取值可在 0 至 1 之间,例如设置为 0.5 的话意为只对 50% 的请求采样 - Rate Limiting (
sampler.type=ratelimiting
) 设置每秒的采样次数上限 。 例如,当sampler.param = 2.0时,它将以每秒2条迹线的速率对请求进行采样。 - Remote (
sampler.type=remote
) 此为默认策略。 采样遵循远程设置,取值的含义和probabilistic
相同,都意为采样的几率,只不过设置为remote
后,Client 会从 Jaeger Agent 中动态获取采样率设置。 为了最大程度地减小开销,Jaeger默认采用 0.1% 的采样策略采集数据 (1000次里面采集1次)。
客户端
全部Jaeger客户端库都支持OpenTracing API ,下面这些都是官方支持的客户端库
语言 | GitHub Repo |
---|---|
Go | jaegertracing/jaeger-client-go |
Java | jaegertracing/jaeger-client-java |
Node.js | jaegertracing/jaeger-client-node |
Python | jaegertracing/jaeger-client-python |
C++ | jaegertracing/jaeger-client-cpp |
C# | jaegertracing/jaeger-client-csharp |
其余语言的客户端库还在开发中,具体进展能够来这里查看 issue #366