.NET Core 中的日志与分布式链路追踪html
分布式链路追踪框架的基本实现原理(当前)前端
开源一个简单的兼容 Jaeger 的框架git
柠檬(Lemon丶)大佬在一月份开业了柠檬研究院,研究院指导成员学习分布式和云原生技术,本月课题是分布式链路追踪,学习 Dapper 论文、Jaeger 的使用,以及完成一个兼容 Jaeger 的链路追踪框架。github
笔者将做业分为三部分,三篇文章加上实现代码,本文是第二篇。数据库
当咱们使用 Google 或者 百度搜索时,查询服务会将关键字分发到多台查询服务器,每台服务器在本身的索引范围内进行搜索,搜索引擎能够在短期内得到大量准确的搜索结果;同时,根据关键字,广告子系统会推送合适的相关广告,还会从竞价排名子系统得到网站权重。一般一个搜索可能须要成千上万台服务器参与,须要通过许多不一样的系统提供服务。编程
多台计算机经过网络组成了一个庞大的系统,这个系统便是分布式系统。json
在微服务或者云原生开发中,通常认为分布式系统是经过各类中间件/服务网格链接的,这些中间件提供了共享资源、功能(API等)、文件等,使得整个网络能够看成一台计算机进行工做。segmentfault
在分布式系统中,用户的一个请求会被分发到多个子系统中,被不一样的服务处理,最后将结果返回给用户。用户发出请求和得到结果这段时间是一个请求周期。后端
当咱们购物时,只须要一个很简单的过程:api
获取优惠劵 -> 下单 -> 付款 -> 等待收货
然而在后台系统中,每个环节都须要通过多个子系统进行协做,而且有严格的流程。例如在下单时,须要检查是否有优惠卷、优惠劵能不能用于当前商品、当前订单是否符合使用优惠劵条件等。
下图是一个用户请求后,系统处理请求的流程。
【图片来源:鹰眼下的淘宝分布式调用跟踪系统介绍】
图中出现了不少箭头,这些箭头指向了下一步要流经的服务/子系统,这些箭头组成了链路网络。
在一个复杂的分布式系统中,任何子系统出现性能不佳的状况,都会影响整个请求周期。根据上图,咱们设想:
1.系统中有可能天天都在增长新服务或删除旧服务,也可能进行升级,当系统出现错误,咱们如何定位问题?
2.当用户请求时,响应缓慢,怎么定位问题?
3.服务可能由不一样的编程语言开发,一、2 定位问题的方式,是否适合全部编程语言?
随着微服务和云原生开发的兴起,愈来愈多应用基于分布式进行开发,可是大型应用拆分为微服务后,服务之间的依赖和调用变得愈来愈复杂,这些服务是不一样团队、使用不一样语言开发的,部署在不一样机器上,他们之间提供的接口可能不一样(gRPC、Restful api等)。
为了维护这些服务,软件领域出现了 Observability 思想,在这个思想中,对微服务的维护分为三个部分:
Metrics
):用于监控和报警;这三部分并非独立开来的,例如 Metrics 能够监控 Tracing 、Logging 服务是否正常运行。Tacing 和 Metrics 服务在运行过程当中会产生日志。
深刻了解请戳爆你的屏幕:https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html
近年来,出现了 APM 系统,APM 称为 应用程序性能管理系统,能够进行 软件性能监视和性能分析。APM 是一种 Metrics,可是如今有融合 Tracing 的趋势。
回归正题,分布式追踪系统(Tracing)有什么用呢?这里能够以 Jaeger 举例,它能够:
Jaeger 须要结合后端进行结果分析,jaeger 有个 Jaeger UI,可是功能并很少,所以还须要依赖 Metrics 框架从结果呈现中可视化,以及自定义监控、告警规则,因此很天然 Metrics 可能会把 Tracing 的事情也作了。
Dapper 是 Google 内部使用的分布式链路追踪系统,并无开源,可是 Google 发布了一篇 《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》 论文,这篇论文讲述了分布式链路追踪的理论和 Dapper 的设计思想。
有不少链路追踪系统是基于 Dapper 论文的,例如淘宝的鹰眼、Twitter 的 Zipkin、Uber 开源的 Jaeger,分布式链路追踪标准 OpenTracing 等。
论文地址:
https://static.googleusercontent.com/media/research.google.com/en//archive/papers/dapper-2010-1.pdf
译文:
http://bigbully.github.io/Dapper-translation/
不能访问 github.io 的话,能够 clone 仓库去看 https://github.com/bigbully/Dapper-translation/tree/gh-pages
Dapper 用户接口:
下图是一个由用户 X 请求发起的,穿过多个服务的分布式系统,A、B、C、D、E 表示不一样的子系统或处理过程。
在这个图中, A 是前端,B、C 是中间层、D、E 是 C 的后端。这些子系统经过 rpc 协议链接,例如 gRPC。
一个简单实用的分布式链路追踪系统的实现,就是对服务器上每一次请求以及响应收集跟踪标识符(message identifiers)和时间戳(timestamped events)。
分布式服务的跟踪系统须要记录在一次特定的请求后系统中完成的全部工做的信息。用户请求能够是并行的,同一时间可能有大量的动做要处理,一个请求也会通过系统中的多个服务,系统中时时刻刻都在产生各类跟踪信息,必须将一个请求在不一样服务中产生的追踪信息关联起来。
为了将全部记录条目与一个给定的发起者X关联上并记录全部信息,如今有两种解决方案,黑盒(black-box)和基于标注(annotation-based)的监控方案。
黑盒方案:
假定须要跟踪的除了上述信息以外没有额外的信息,这样使用统计回归技术来推断二者之间的关系。
基于标注的方案:
依赖于应用程序或中间件明确地标记一个全局ID,从而链接每一条记录和发起者的请求。
优缺点:
虽然黑盒方案比标注方案更轻便,他们须要更多的数据,以得到足够的精度,由于他们依赖于统计推论。基于标注的方案最主要的缺点是,很明显,须要代码植入。在咱们的生产环境中,由于全部的应用程序都使用相同的线程模型,控制流和 RPC 系统,咱们发现,能够把代码植入限制在一个很小的通用组件库中,从而实现了监测系统的应用对开发人员是有效地透明。
Dapper 基于标注的方案,接下来咱们将介绍 Dapper 中的一些概念知识。
从形式上看,Dapper 跟踪模型使用的是树形结构,Span 以及 Annotation。
在前面的图片中,咱们能够看到,整个请求网络是一个树形结构,用户请求是树的根节点。在 Dapper 的跟踪树结构中,树节点是整个架构的基本单元。
span 称为跨度,一个节点在收到请求以及完成请求的过程是一个 span,span 记录了在这个过程当中产生的各类信息。每一个节点处理每一个请求时都会生成一个独一无二的的 span id,当 A -> C -> D 时,多个连续的 span 会产生父子关系,那么一个 span 除了保存本身的 span id,也须要关联父、子 span id。生成 span id 必须是高性能的,而且可以明确表示时间顺序,这点在后面介绍 Jaeger 时会介绍。
Annotation 译为注释,在一个 span 中,能够为 span 添加更多的跟踪细节,这些额外的信息能够帮助咱们监控系统的行为或者帮助调试问题。Annotation 能够添加任意内容。
到此为止,简单介绍了一些分布式追踪以及 Dapper 的知识,可是这些不足以严谨的说明分布式追踪的知识和概念,建议读者有空时阅读 Dapper 论文。
要实现 Dapper,还须要代码埋点、采样、跟踪收集等,这里就再也不细谈了,后面会介绍到,读者也能够看看论文。
OpenTracing 是与分布式系统无关的API和用于分布式跟踪的工具,它不只提供了统一标准的 API,还致力于各类工具,帮助开发者或服务提供者开发程序。
OpenTracing 为标准 API 提供了接入 SDK,支持这些语言:Go, JavaScript, Java, Python, Ruby, PHP, Objective-C, C++, C#。
固然,咱们也能够自行根据通信协议,本身封装 SDK。
读者能够参考 OpenTracing 文档:https://opentracing.io/docs/
接下来咱们要一点点弄清楚 OpenTracing 中的一些概念和知识点。因为 jaeger 是 OpenTracing 最好的实现,所以后面讲 Jaeger 就是 Opentracing ,不须要将二者严格区分。
首先是 JAEGER 部分,这部分是代码埋点等流程,在分布式系统中处理,当一个跟踪完成后,经过 jaeger-agent 将数据推送到 jaeger-collector。jaeger-collector 负责处理四面八方推送来的跟踪信息,而后存储到后端,能够存储到 ES、数据库等。Jaeger-UI 能够将让用户在界面上看到这些被分析出来的跟踪信息。
OpenTracing API 被封装成编程语言的 SDK(jaeger-client),例如在 C# 中是 .dll ,Java 是 .jar,应用程序代码经过调用 API 实现代码埋点。
jaeger-Agent 是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。
【图片来源:http://www.javashuo.com/article/p-zqcwamkm-d.html】
在 OpenTracing 中,跟踪信息被分为 Trace、Span 两个核心,它们按照必定的结构存储跟踪信息,因此它们是 OpenTracing 中数据模型的核心。
Trace 是一次完整的跟踪,Trace 由多个 Span 组成。下图是一个 Trace 示例,由 8 个 Span 组成。
[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)
Tracing:
a Trace can be thought of as a directed acyclic graph (DAG) of Spans。
有点难翻译,大概意思是 Trace 是多个 Span 组成的有向非循环图。
在上面的示例中,一个 Trace 通过了 8 个服务,A -> C -> F -> G 是有严格顺序的,可是从时间上来看,B 、C 是能够并行的。为了准确表示这些 Span 在时间上的关系,咱们能够用下图表示:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··]
有个要注意的地方, 并非 A -> C -> F 表示 A 执行结束,而后 C 开始执行,而是 A 执行过程当中,依赖 C,而 C 依赖 F。所以,当 A 依赖 C 的过程完成后,最终回到 A 继续执行。因此上图中 A 的跨度最大。
要深刻学习,就必须先了解 Span,请读者认真对照下面的图片和 Json:
json 地址: https://github.com/whuanle/DistributedTracing/issues/1
后续将围绕这张图片和 Json 来举例讲述 Span 相关知识。
一个简化的 Trace 以下:
注:不一样编程语言的字段名称有所差别,gRPC 和 Restful API 的格式也有所差别。
"traceID": "790e003e22209ca4", "spans":[...], "processes":{...}
前面说到,在 OpenTracing 中,Trace 是一个有向非循环图,那么 Trace 一定有且只有一个起点。
这个起点会建立一个 Trace 对象,这个对象一开始初始化了 trace id 和 process,trace id 是一个 32 个长度的字符串组成,它是一个时间戳,而 process 是起点进程所在主机的信息。
下面笔者来讲一些一下 trace id 是怎么生成的。trace id 是 32个字符串组成,而实际上只使用了 16 个,所以,下面请以 16 个字符长度去理解这个过程。
首先获取当前时间戳,例如得到 1611467737781059
共 16 个数字,单位是微秒,表示时间 2021-01-24 13:55:37,秒如下的单位这里就不给出了,明白表示时间就行。
在 C# 中,将当前时间转为这种时间戳的代码:
public static long ToTimestamp(DateTime dateTime) { DateTime dt1970 = new DateTime(1970, 1, 1, 0, 0, 0, 0); return (dateTime.Ticks - dt1970.Ticks)/10; } // 结果:1611467737781059
若是咱们直接使用 Guid 生成或者 string 存储,都会消耗一些性能和内存,而使用 long,刚恰好能够表示时间戳,还能够节约内存。
得到这个时间戳后,要传输到 Jaeger Collector,要转为 byet 数据,为何要这样不太清楚,按照要求传输就是了。
将 long 转为一个 byte 数组:
var bytes = BitConverter.GetBytes(time); // 大小端 if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); }
long 占 8 个字节,每一个 byte 值以下:
0x00 0x05 0xb9 0x9f 0x12 0x13 0xd3 0x43
而后传输到 Jaeger Collector 中,那么得到的是一串二进制,怎么表示为字符串的 trace id?
能够先还原成 long,而后将 long 输出为 16 进制的字符串:
转为字符串(这是C#):
Console.WriteLine(time.ToString("x016"));
结果:
0005b99f1213d343
Span id 也是这样转的,每一个 id 由于与时间戳相关,因此在时间上是惟一的,生成的字符串也是惟一的。
这就是 trace 中的 trace id 了,而 trace process 是发起请求的机器的信息,用 Key-Value 的形式存储信息,其格式以下:
{ "key": "hostname", "type": "string", "value": "Your-PC" }, { "key": "ip", "type": "string", "value": "172.6.6.6" }, { "key": "jaeger.version", "type": "string", "value": "CSharp-0.4.2.0" }
Ttace 中的 trace id 和 process 这里说完了,接下来讲 trace 的 span。
Span 由如下信息组成:
span 之间若是是父子关系,则可使用 SpanContext 绑定这种关系。父子关系有 ChildOf
、FollowsFrom
两种表示,ChildOf
表示 父 Span 在必定程度上依赖子 Span,而 FollowsFrom
表示父 Span 彻底不依赖其子Span 的结果。
一个 Span 的简化信息以下(不用理会字段名称大小写):
{ "traceID": "790e003e22209ca4", "spanID": "4b73f8e8e77fe9dc", "flags": 1, "operationName": "print-hello", "references": [], "startTime": 1611318628515966, "duration": 259, "tags": [ { "key": "internal.span.format", "type": "string", "value": "proto" } ], "logs": [ { "timestamp": 1611318628516206, "fields": [ { "key": "event", "type": "string", "value": "WriteLine" } ] } ] }
在 OpenTracing API 中,有三个主要对象:
Tracer
能够建立Spans
并了解如何跨流程边界对它们的元数据进行Inject
(序列化)和Extract
(反序列化)。它具备如下功能:
Span
Inject
一个SpanContext
到一个载体Extract
一个SpanContext
从载体由起点进程建立一个 Tracer,而后启动进程发起请求,每一个动做产生一个 Span,若是有父子关系,Tracer 能够将它们关联起来。当请求完成后, Tracer 将跟踪信息推送到 Jaeger-Collector中。
详细请查阅文档:https://opentracing.io/docs/overview/tracers/
SpanContext 是在不一样的 Span 中传递信息的,SpanContext 包含了简单的 Trace id、Span id 等信息。
咱们继续如下图做为示例讲解。
A 建立一个 Tracer,而后建立一个 Span,表明本身 (A),再建立两个 Span,分别表明 B、C,而后经过 SpanContext 传递一些信息到 B、C;B 和 C 收到 A 的消息后,也建立一个 Tracer ,用来 Tracer.extract(...)
;其中 B 没有后续,能够直接返回结果;而 C 的 Tracer 继续建立两个 Span,往 D、E 传递 SpanContext。
这个过程比较复杂,笔者讲很差,建议读者参与 OpenTracing 的官方文档。
详细的 OpenTracing API,能够经过编程语言编写相应服务时,去学习各类 API 的使用。
.NET Core 笔者写了一篇,读者有兴趣能够阅读:【.NET Core 中的日志与分布式链路追踪】http://www.javashuo.com/article/p-oooxbaem-nz.html