当代的互联网的服务,通常都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。
问题:
比如一个前端服务可能对上百台查询服务器发起了一个 Web 查询,这个查询可能会被发送到多个的子系统,这些子系统分别用来处理广告、进行拼写检查或是查找一些像图片、视频或新闻这样的特殊结果。根据每个子系统的查询结果进行筛选,得到最终结果,最后汇总到页面上。我们把这种搜索模型称为“全局搜索”(universal search)。
总的来说,这一次全局搜索有可能调用上千台服务器,涉及各种服务。而且,用户对搜索的耗时是很敏感的,而任何一个子系统的低效都导致最终的搜索耗时。如果一个工程师只能知道这个查询耗时不正常,但是他无从知晓这个问题到底是由哪个服务调用造成的,或者为什么这个调用性能差强人意。
首先,这个工程师可能无法准确的定位到这次全局搜索是调用了哪些服务,因为新的服务、乃至服务上的某个片段,都有可能在任何时间上过线或修改过,有可能是面向用户功能,也有可能是一些例如针对性能或安全认证方面的功能改进。
其次,你不能苛求这个工程师对所有参与这次全局搜索的服务都了如指掌,每一个服务都有可能是由不同的团队开发或维护的。
再次,这些暴露出来的服务或服务器有可能同时还被其他客户端使用着,所以这次全局搜索的性能问题甚至有可能是由其他应用造成的。
面对解耦成大量的分布式微服务时,监控、预警、定位故障就变困难了。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具。
当我们的系统发生故障时,我们需要登录到各个服务器上,使用 grep / sed / awk 等 Linux 脚本工具去日志里查找故障原因。
在没有日志系统的情况下,首先需要定位处理请求的服务器,如果这台服务器部署了多个实例,则需要去每个应用实例的日志目录下去找日志文件。
每个应用实例还会设置日志滚动策略(如:每天生成一个文件),还有日志压缩归档策略等,等数据过期之后,日志也就永久性的消失了。
综上所述,我们可能会考虑需要有一个日志集中收集和检索系统,这个日志系统就是ELK。
ELK,也就是 Elasticsearch、Logstash、Kibana三者的结合,是一套开源的分布式日志管理方案.
总之, ELK可以帮助我们实时的将各个服务的日志统一收集存储,并给我们提供可视化的日志检索页面。
Kubernetes官方提供了 EFK 的日志收集解决方案, Logstash是基于JDK的,Logstash 致命的问题是它的性能以及资源消耗(默认的堆大小是 1GB)。尽管它的性能在近几年已经有很大提升,与它的替代者们相比还是要慢很多的。在没有产生日志的情况单纯启动Logstash就大概要消耗500M内存,在每个Pod中都启动一个日志收集组件的情况下,使用Logstash有点浪费系统资源, 使用Fluentd替代比起Logstash相当轻量级。
传统的ELK方案需要开发者在编写代码时尽可能全地打印日志,再通过关键字段从ES中搜集筛选出与业务逻辑相关的日志数据,进而拼凑出业务执行的现场信息。
存在如下的痛点:
综上所述,随着业务逻辑和系统复杂度的攀升,传统的ELK方案在日志搜集、日志筛选和日志分析方面愈加的耗时耗力。
Sci-Hub | Clustering intrusion detection alarms to support root cause analysis. ACM Transactions on Information and System Security, 6(4), 443–471 | 10.1145/950191.950192
市面上的分布式会话跟踪系统框架几乎都是以 Google Dapper 论文为基础进行实现,整体大同小异。
核心思想:在用户一次请求服务的调⽤过程中,无论请求被分发到多少个子系统中,子系统又调用了更多的子系统,我们把系统信息和系统间调用关系都追踪记录下来。最终把数据集中起来可视化展示。
图1:这个路径由用户的X请求发起,穿过一个简单的服务系统。用字母标识的节点代表分布式系统中的不同处理过程。
将所有记录条目与一个给定的发起者关联上并记录所有信息,有两种解决方案,黑盒(black-box)和基于标注(annotation-based)的监控方案。
黑盒方案假定需要跟踪的除了上述信息之外没有额外的信息,使用统计回归技术来推断两者之间的关系。
基于标注的方案依赖于应用程序或中间件明确地标记一个全局ID,从而连接每一条记录和发起者的请求。
虽然黑盒方案比标注方案更轻便,他们需要更多的数据,以获得足够的精度,因为他们依赖于统计推论。
基于标注的方案最主要的缺点是需要代码植入。在 google 的生产环境中,因为所有的应用程序都使用相同的线程模型,控制流和RPC系统,可以把代码植入限制在一个很小的通用组件库中,从而实现了监测系统的应用对开发人员是有效地透明。
Dapper 的跟踪架构像是内嵌在 RPC 调用的树形结构。但核心数据模型不只局限于特定的RPC框架,还能跟踪其他行为,例如 SMTP sessions, HTTP request,和外部对 SQL 服务器的查询等。
Dapper 跟踪模型使用 trace, span, 和 annotation。
Trace的含义比较直观,就是链路,指一个请求经过所有服务的路径,可以用下面树状的图形表示。
图2:5个span在Dapper跟踪树种短暂的关联关系
Dapper 记录了 span 名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系。如果一个span没有父ID被称为root span。所有 span 都挂在一个特定的跟踪上,也共用一个 traceid。所有这些ID用全局唯一的64位整数标示( snowflake 算法)。在一个典型的Dapper跟踪中,每一个RPC对应到一个 span。
span 表示跨服务的一次调用,通过计算开始时间和结束时间的时间差,就能明确调用过程在这个 span 上产生的时间差。
简单的说就是,通过 trace 串联调用链路,再通过 span 反映出链路调用关系。
图3:在图2中所示的一个单独的span的细节图
如果应用程序开发者选择在跟踪中增加他们自己的 annotation (例如:业务数据),这些信息也会和其他span信息一样记录下来。
Dapper基于少量通用组件库的改造,可以以近乎零侵入的成本进行跟踪。如下:
思考:请求这么多,全部采集会不会损耗过高
低损耗的是Dapper的一个关键的设计目标,因为如果这个工具价值未被证实但又对性能有影响的话,你可以理解服务运营人员为什么不愿意部署它。
某些类型的Web服务对植入带来的性能损耗确实非常敏感。因此,除了把Dapper的收集工作对基本组件的性能损耗限制的尽可能小之外,还需要进一步控制损耗的办法,那就是遇到大量请求时只记录其中的一小部分。
任何给定进程的Dapper的消耗和每个进程单位时间的跟踪的采样率成正比。Dapper的第一个生产版本在Google内部的所有进程上使用统一的采样率,为1/1024。这个简单的方案是对高吞吐量的线上服务来说是非常有用。
新的Dapper用户往往觉得低采样率(在高吞吐量的服务下经常低至0.01%)将会不利于他们的分析。对于高吞吐量服务,如果一个显着的操作在系统中出现一次,他就会出现上千次。低吞吐量的服务(也许是每秒请求几十次,而不是几十万)可以负担得起跟踪每一个请求,这是促使使用自适应采样率的原因。
然而,在较低的采样率和较低的传输负载下可能会导致错过重要事件,而想用较高的采样率就需要能接受的性能损耗。对于这样的系统的解决方案就是覆盖默认的采样率,这需要手动干预的,这种情况是我们试图避免在dapper中出现的。在部署可变采样的过程中,参数化配置采样率时,不是使用一个统一的采样方案,而是使用一个采样期望率来标识单位时间内采样的追踪。这样一来,低流量低负载自动提高采样率,而在高流量高负载的情况下会降低采样率,使损耗一直保持在控制之下。实际使用的采样率会随着跟踪本身记录下来,这有利于从Dapper的跟踪数据中准确的分析。
图4:Dapper 收集管道的总览
Dapper的跟踪记录和收集管道的过程分为三个阶段。
首先,span数据写入(1)本地日志文件中。
然后Dapper的守护进程和收集组件把这些数据从生产环境的主机中拉出来(2)。
最终写到(3)Dapper的 Bigtable 仓库中。一次跟踪被设计成Bigtable中的一行,每一列相当于一个span。
Dapper使用了BigTable数据仓库,常用的存储还有ElasticSearch, HBase, In-memory DB等。
OpenTelemetry 的 go sdk 实现调用链拦截的基本思路是:基于 AOP 的思想采用装饰器模式,通过包装替换目标包(如 net/http)的核心接口或组件,实现在核心调用过程前后添加 Span 相关逻辑。
对于 httpserver 其核心就是 http.Handler 这个接口。因此,可以通过实现一个针对 http.Handler 接口的拦截器,来负责 Span 的生成和传播。
package http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
http.ListenAndServe(":8090", http.DefaultServeMux)
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
wrappedHttpHandler := otelhttp.NewHandler(http.DefaultServeMux, ...)
http.ListenAndServe(":8090", wrappedHttpHandler)
func ServeHTTP(ResponseWriter, *Request) {
ctx := tracer.Extract(r.ctx, r.Header)
ctx, span := tracer.Start(ctx, genOperation(r))
r.WithContext(ctx)
// original ServeHTTP
span.End()
}
wrppedHttpHandler 中将主要实现如下逻辑(伪代码):
ctx := tracer.Extract(r.ctx, r.Header)
:从请求的 header 中提取 TraceId和 SpanId,进而构建 SpanContext 对象,并最终存储在 ctx 中;ctx, span := tracer.Start(ctx, genOperation(r))
:生成跟踪当前请求处理过程的 Span(即前文所述的Span1),并记录开始时间,这时会从 ctx 中读取 SpanContext,将 SpanContext.TraceId 作为当前 Span 的TraceId,将 SpanContext.SpanId 作为当前 Span的ParentSpanId,然后将自己作为新的 SpanContext 写入返回的 ctx 中;r.WithContext(ctx)
:将新生成的 SpanContext 添加到请求 r 的 context 中,以便被拦截的 handler 内部在处理过程中,可以从 r.ctx 中拿到 Span1 的 SpanId 作为其 ParentSpanId 属性,从而建立 Span 之间的父子关系;span.End()
:记录处理完成的时间,然后将它发送给 exporter 上报到服务端httpclient 发送请求的关键操作是 http.RoundTriper 接口, 包装一下 httpclient 原来使用的 RoundTripper 实现 span 的生成。
package http
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
wrappedTransport := otelhttp.NewTransport(http.DefaultTransport)
client := http.Client{Transport: wrappedTransport}
func RoundTrip ( r *http.Request) (*Response, error){
ctx, span := tracer.Start(r.Context(), url)
tracer.Inject(ctx, r.Header)
// original RoundTrip
span.End()
}
如图所示,wrappedTransport 将主要完成以下任务(伪代码):
req, _ := http.NewRequestWithContext(r.ctx, “GET”,url, nil)
:这里我们将上一步 http.Handler 的请求的 ctx,传递到 httpclient 要发出的 request 中,这样在之后我们就可以从 request.Context() 中提取出 Span1 的信息,来建立 Span 之间的关联;ctx, span := tracer.Start(r.Context(), url)
:执行 client.Do() 之后,将首先进入 WrappedTransport.RoundTrip() 方法,这里生成新的 Span(Span2),开始记录 httpclient 请求的耗时情况,与前文一样,Start 方法内部会从 r.Context() 中提取出 Span1 的 SpanContext,并将其 spanid 作为当前 Span(Span2)的 ParentSpanId,从而建立了 Span 之间的嵌套关系,同时返回的 ctx 中保存的 SpanContext 将是新生成的 Span(Span2)的信息;tracer.Inject(ctx, r.Header)
:这一步的目的是将当前 SpanContext 中的 TraceId 和 SpanId 等信息写入到 r.Header 中,以便能够随着 http 请求发送到 serverB,之后在 serverB 中与当前 Span 建立关联;span.End()
:等待 httpclient 请求发送到 serverB 并收到响应以后,标记当前 Span 跟踪结束,设置 EndTime 并提交给 exporter 以上报到服务端。数据传播按照场景分为两类:进程内传播、跨进程间传播( Cross-Process-Tracing)。
进程内传播是指 Trace 在一个服务内部传递,监控了服务内部相互调用情况,相当比较简单。追踪系统最困难的部分就是在分布式的应用环境下保持追踪的正常工作。任何一个追踪系统,都需要理解多个跨进程调用间的因果关系,无论他们是通过 RPC 框架、发布-订阅机制、通用消息队列、HTTP 请求调用、UDP 传输或者其他传输模式。所以业界谈起 Tracing 技术 往往说的是跨进程间的分布式链路追踪(Distrubute Tracing)。
在 Trace 传递中有一个核心的概念 Carrier。它在 Trace 中把 Span 信息传递下去,在 HTTP 调用场景中,会有 HttpCarrier,在 RPC 的调用场景中会有 RpcCarrier 来搬运 SpanContext。Trace 通过 Carrier 可以把链路追踪状态从一个进程“搬运”到另一个进程里。
链路数据为了进行网络传输,需要数据进行序列化和反序列化。这个过程 Trace 通过一个负责数据序列化反序列化上下文的 Formatter 接口实现的。例如在 HttpCarrier 使用中通常就会有一个对应的 HttpFormatter。所以 Inject 注入是委托给 Formatter 将 SpanContext 进行序列化写入 Carrier。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCScq7qC-1681700005519)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6bd3cb35-42c2-418b-97d6-f4254caae089/Untitled.png)]
W3C 组织为 HTTP 支持链路追踪专门在 Headers 中定义了 Trace 标准:
Trace Context
跟踪系统的成本由两部分组成:
生成跟踪的开销是Dapper性能影响中最关键的部分,因为收集和分析可以更容易在紧急情况下被关闭。Dapper运行库中最重要的跟踪生成消耗在于创建和销毁 span 和 annotation,并记录到本地磁盘供后续的收集。
根span的创建和销毁需要损耗平均204纳秒的时间,而同样的操作在其他span上需要消耗176纳秒。时间上的差别主要在于需要在跟span上给这次跟踪分配一个全局唯一的ID。
在Dapper运行期写入到本地磁盘是最昂贵的操作,但是他们的可见损耗大大减少,因为写入日志文件和操作相对于被跟踪的应用系统来说都是异步的。
读出跟踪数据也会对正在被监控的负载产生干扰。
下表展示的是最坏情况下,Dapper收集日志的守护进程在高于实际情况的负载基准下进行测试时的cpu使用率。在生产环境下,跟踪数据处理中,这个守护进程从来没有超过0.3%的单核cpu使用率,而且只有很少量的内存使用(以及堆碎片的噪音)。
还限制了Dapper守护进程为内核scheduler最低的优先级,以防在一台高负载的服务器上发生cpu竞争。
Dapper也是一个带宽资源的轻量级的消费者,每一个span传输只占用了平均426的byte。作为网络行为中的极小部分,Dapper的数据收集在Google的生产环境中的只占用了0.01%的网络资源。
分布式会话跟踪的主要作用是分析分布式系统的调用行为,并不能很好地应用于业务逻辑的追踪。下图是一个审核业务场景的追踪案例,业务系统对外提供审核能力,待审对象的审核需要经过“初审”和“复审”两个环节(两个环节关联相同的taskId),因此整个审核环节的执行调用了两次审核接口。如图左侧所示,完整的审核场景涉及众多“业务逻辑”的执行,而分布式会话跟踪只是根据两次RPC调用生成了右侧的两条调用链路,并没有办法准确地描述审核场景业务逻辑的执行,问题主要体现在以下几个方面:
(1) 无法同时追踪多条调用链路
分布式会话跟踪仅支持单个请求的调用追踪,当业务场景包含了多个调用时,将生成多条调用链路;由于调用链路通过traceId串联,不同链路之间相互独立,因此给完整的业务追踪增加了难度。
例如当排查审核场景的业务问题时,由于初审和复审是不同的RPC请求,所以无法直接同时获取到2条调用链路,通常需要额外存储2个traceId的映射关系。
(2) 无法准确描述业务逻辑的全景
分布式会话跟踪生成的调用链路,只包含单次请求的实际调用情况,部分未执行的调用以及本地逻辑无法体现在链路中,导致无法准确描述业务逻辑的全景。
例如同样是审核接口,初审链路1包含了服务b的调用,而复审链路2却并没有包含,这是因为审核场景中存在“判断逻辑”,而该逻辑无法体现在调用链路中,还是需要人工结合代码进行分析。
(3) 无法聚焦于当前业务系统的逻辑执行
分布式会话跟踪覆盖了单个请求流经的所有服务、组件、机器等等,不仅包含当前业务系统,还涉及了众多的下游服务,当接口内部逻辑复杂时,调用链路的深度和复杂度都会明显增加,而业务追踪其实仅需要聚焦于当前业务系统的逻辑执行情况。
例如审核场景生成的调用链路,就涉及了众多下游服务的内部调用情况,反而给当前业务系统的问题排查增加了复杂度。
随着互联网产品的快速发展,商业环境和用户需求不断变化,带来了繁多复杂的业务需求。业务系统需要支持越来越广泛的业务场景,涵盖越来越多的业务逻辑,因此系统的复杂度也快速提升。同时,微服务架构的演进也导致业务逻辑的实现往往需要多个服务之间的共同协作。总之,业务系统的日益复杂已经成为一种常态。
业务系统往往面临着多样的日常客诉和突发问题,“业务追踪”就成为了关键的应对手段。业务追踪可以看做一次业务执行的现场还原过程,通过执行中的各种记录还原出原始现场,可用于业务逻辑执行情况的分析和问题的定位,是整个系统建设中重要的一环。
随着业务逻辑的日益复杂,上述方案越来越不适用于当下的业务系统。
传统的ELK方案是一种滞后的业务追踪,需要事后从大量离散的日志中搜集和筛选出需要的日志,并人工进行日志的串联分析,其过程必然耗时耗力。
分布式会话跟踪方案则是在调用执行的同时,实时地完成了链路的动态串联,但由于是会话级别且仅关注于调用关系等问题,导致其无法很好地应用于业务追踪。
实现聚焦于业务逻辑追踪的高效解决方案,将业务执行的日志以业务链路为载体进行高效组织和串联,并支持业务执行现场的还原和可视化查看,从而提升定位问题的效率,即可视化全链路日志追踪。
可视化全链路日志追踪考虑在前置阶段,即业务执行的同时实现业务日志的高效组织和动态串联,如下图所示,此时离散的日志数据将会根据业务逻辑进行组织,绘制出执行现场,从而可以实现高效的业务追踪。
新方案需要回答两个关键问题:如何高效组织业务日志,以及如何动态串联业务日志。下文将逐一进行回答。
问题1:如何高效组织业务日志?
为了实现高效的业务追踪,首先需要准确完整地描述出业务逻辑,形成业务逻辑的全景图,而业务追踪其实就是通过执行时的日志数据,在全景图中还原出业务执行的现场。
新方案对业务逻辑进行了抽象,定义出业务逻辑链路,下面还是以“审核业务场景”为例,来说明业务逻辑链路的抽象过程:
一次业务追踪就是逻辑链路的某一次执行情况的还原,逻辑链路完整准确地描述了业务逻辑全景,同时作为载体可以实现业务日志的高效组织。
问题2:如何动态串联业务日志?
业务逻辑执行时的日志数据原本是离散存储的,而此时需要实现的是,随着业务逻辑的执行动态串联各个逻辑节点的日志,进而还原出完整的业务逻辑执行现场。
由于逻辑节点之间、逻辑节点内部往往通过MQ或者RPC等进行交互,采用分布式会话跟踪提供的分布式参数透传能力实现业务日志的动态串联:
与分布式会话跟踪方案不同的是,当同时串联多次分布式调用时,新方案需要结合业务逻辑选取一个公共id作为标识,例如审核场景涉及2次RPC调用,为了保证2次执行被串联至同一条逻辑链路,此时结合审核业务场景,选择初审和复审相同的“任务id”作为标识,完整地实现审核场景的逻辑链路串联和执行现场还原。
明确日志的高效组织和动态串联这两个基本问题后,本文选取图4业务系统中的“逻辑链路1”进行通用方案的详细说明,方案可以拆解为以下步骤:
“链路定义”的含义为:使用特定语言,静态描述完整的逻辑链路,链路通常由多个逻辑节点,按照一定的业务规则组合而成,业务规则即各个逻辑节点之间存在的执行关系,包括串行、并行、条件分支。
DSL(Domain Specific Language)是为了解决某一类任务而专门设计的计算机语言,可以通过JSON或XML定义出一系列节点(逻辑节点)的组合关系(业务规则)。因此,本方案选择使用DSL描述逻辑链路,实现逻辑链路从抽象定义到具体实现。
逻辑链路1-DSL
[
{
"nodeName": "A",
"nodeType": "rpc"
},
{
"nodeName": "Fork",
"nodeType": "fork",
"forkNodes": [
[
{
"nodeName": "B",
"nodeType": "rpc"
}
],
[
{
"nodeName": "C",
"nodeType": "local"
}
]
]
},
{
"nodeName": "Join",
"nodeType": "join",
"joinOnList": [
"B",
"C"
]
},
{
"nodeName": "D",
"nodeType": "decision",
"decisionCases": {
"true": [
{
"nodeName": "E",
"nodeType": "rpc"
}
]
},
"defaultCase": [
{
"nodeName": "F",
"nodeType": "rpc"
}
]
}
]
“链路染色”的含义为:在链路执行过程中,通过透传串联标识,明确具体是哪条链路在执行,执行到了哪个节点。
链路染色包括两个步骤:
步骤一:确定串联标识
,当逻辑链路开启时,确定唯一标识,能够明确后续待执行的链路和节点。
步骤二:传递串联标识
,当逻辑链路执行时,在分布式的完整链路中透传串联标识,动态串联链路中已执行的节点,实现链路的染色。例如在“逻辑链路1”中:
“链路上报”的含义为:在链路执行过程中,将日志以链路的组织形式进行上报,实现业务现场的准确保存。
如上图8所示,上报的日志数据包括:节点日志和业务日志。其中节点日志的作用是绘制链路中的已执行节点,记录了节点的开始、结束、输入、输出;业务日志的作用是展示链路节点具体业务逻辑的执行情况,记录了任何对业务逻辑起到解释作用的数据,包括与上下游交互的入参出参、复杂逻辑的中间变量、逻辑执行抛出的异常。
“链路存储”的含义为:将链路执行中上报的日志落地存储,并用于后续的“现场还原”。上报日志可以拆分为链路日志、节点日志和业务日志三类:
下图就是链路存储的存储模型,包含了链路日志,节点日志,业务日志、链路元数据(配置数据),并且是如下图所示的树状结构,其中业务标识作为根节点,用于后续的链路查询。
支持众多服务统一的日志收集、处理和存储,能够很好地支撑大数据量下的日志追踪建设。
日志收集:各应用服务通过机器上部署的 log_agent 收集异步上报的日志数据,并统一传输至Kafka通道中,此外针对少量不支持log_agent的服务,搭建了如图所示的中转应用。
日志解析:收集的日志通过Kafka接入到Flink中,统一进行解析和处理,根据日志类型对日志进行分类和聚合,解析为链路日志、节点日志和业务日志。
日志存储:完成日志解析后,日志会按照树状的存储模型进行落地存储,结合存储的需求分析以及各个存储选项的特点,点评内容平台最终选择HBase作为存储选型。
需求分析 | 选型优点 |
---|---|
OLTP业务:逻辑链路的实时读写数据量很大:海量的记录数,且未来会持续增长写密集、读较少:日志上报峰值QPS较高业务场景简单:简单读写即可满足需求 | 存储特性:支持横向扩展、快速扩充字段查询特性:支持精确和前缀匹配查询,且支持快速随机的访问经济成本:存储介质成本低廉 |
整体而言,log_agent + Kafka + Flink + HBase的日志上报和存储架构能够很好地支持复杂的业务系统,天然支持分布式场景下众多应用的日志上报,同时适用于高流量的数据写入。
点评内容平台实现了“自定义日志工具包”(即下图13的TraceLogger工具包),屏蔽链路追踪中的上报细节,实现众多服务改造的成本最小化。TraceLogger工具包的功能包括:
下面是TraceLogger工具包分别进行业务日志和节点日志上报的使用案例,整体的改造成本较低。
业务日志上报:无学习成本,基本无改造成本。
// 替换前:原日志上报
LOGGER.error("updatestructfailed, param:{}", GsonUtils.toJson(structRequest), e);
// 替换后:全链路日志上报
TraceLogger.error("updatestructfailed, param:{}", GsonUtils.toJson(structRequest), e);
节点日志上报:支持API、AOP两种上报方式,灵活且成本低。
public ResponserealTimeInputLink(long contentId) {
// 链路开始:传递串联标识(业务标识 + 场景标识 + 执行标识)
TraceUtils.passLinkMark("contentId_type_uuid");
// ...// 本地调用(API上报节点日志)
TraceUtils.reportNode("contentStore", contentId, StatusEnums.RUNNING)
contentStore(contentId);
TraceUtils.reportNode("contentStore", structResp, StatusEnums.COMPLETED)
// ...// 远程调用
Response processResp = picProcess(contentId);
// ...
}
// AOP上报节点日志@TraceNode(nodeName="picProcess")
public ResponsepicProcess(long contentId) {
// 图片处理业务逻辑// 业务日志数据上报
TraceLogger.warn("picProcess failed, contentId:{}", contentId);
}
基于上述实践,点评内容平台实现了可视化全链路日志追踪,能够一键追踪任意一条内容所有业务场景的执行,并通过可视化的链路进行执行现场的还原,追踪效果如下图所示:
【链路查询功能】:根据内容id实时查询该内容所有的逻辑链路执行,覆盖所有的业务场景。
【链路展示功能】:通过链路图可视化展示业务逻辑的全景,同时展示各个节点的执行情况。
【节点详情查询功能】:支持展示任意已执行节点的详情,包括节点输入、输出,以及节点执行过程中的关键业务日志。
目前,可视化全链路日志追踪系统已经成为点评内容平台的“问题排查工具”,我们可以将问题排查耗时从小时级降低到5分钟内;同时也是“测试辅助工具”,利用可视化的日志串联和展示,明显提升了RD自测、QA测试的效率。最后总结一下可视化全链路日志追踪的优点: