服务跟踪、OpenTracing 和 Jaeger

服务跟踪、OpenTracing 和 Jaeger

我们在项目中使用微服务架构。 当出现性能瓶颈时,大量时间花费在监控和解析日志上。 将各个操作的计时记录到日志文件时,通常很难理解导致调用这些操作的原因、跟踪操作的顺序或不同服务中一个操作相对于另一个操作的时间偏移。

为了最大限度地减少体力劳动,我们决定使用其中一种跟踪工具。 关于如何以及为什么可以使用跟踪以及我们是如何做到的,将在本文中讨论。

溯源可以解决哪些问题

  1. 查找单个服务内以及所有参与服务之间的整个执行树中的性能瓶颈。 例如:
    • 服务之间的许多短的连续调用,例如地理编码或数据库。
    • 长时间 I/O 等待,例如网络传输或磁盘读取。
    • 长数据解析。
    • 需要CPU的长时间操作。
    • 获得最终结果不需要的代码部分可以被删除或延迟。
  2. 清楚地了解按什么顺序调用什么以及执行操作时会发生什么。
    服务跟踪、OpenTracing 和 Jaeger
    可以看到,比如Request来到WS服务->WS服务通过R服务补充数据->然后向V服务发送请求->V服务从R服务加载了很多数据R 服务 -> 转到 P 服务 -> P 服务再次转到服务 R -> 服务 V 忽略结果并转到服务 J -> 然后才将响应返回到服务 WS,同时继续计算其他内容的背景。
    如果没有整个过程的跟踪或详细文档,第一次查看代码时很难理解发生了什么,并且代码分散在不同的服务中并隐藏在一堆 bin 和接口后面。
  3. 收集有关执行树的信息以供后续延迟分析。 在执行的每个阶段,您可以将信息添加到该阶段可用的跟踪中,然后找出哪些输入数据导致了类似的场景。 例如:
    • 用户身份
    • 权利
    • 所选方法的类型
    • 日志或执行错误
  4. 将跟踪转换为指标的子集,并以指标的形式进行进一步分析。

可以记录什么痕迹。 跨度

在跟踪中存在跨度的概念,这类似于控制台的一个日志。 水疗中心拥有:

  • 名称,通常是执行的方法的名称
  • 生成跨度的服务的名称
  • 拥有唯一的ID
  • 某种元信息,其形式为已登录的键/值。 例如,方法参数或方法是否以错误结束
  • 此跨度的开始和结束时间
  • 父跨度 ID

每个跨度都会被发送到跨度收集器,以存储在数据库中,以便在执行完成后立即进行查看。 将来,您可以通过parent id连接来构建所有span的树。 例如,在分析时,您可以找到某个服务中花费超过一段时间的所有跨度。 此外,通过转到特定跨度,可以查看该跨度上方和下方的整个树。

服务跟踪、OpenTracing 和 Jaeger

Opentrace、Jagger 以及我们如何在项目中实施它

有一个共同的标准 开放跟踪,它描述了应该如何收集以及收集什么内容,而不受跟踪到任何语言的特定实现的束缚。 例如,在 Java 中,所有跟踪工作都是通过通用的 Opentrace API 进行的,在它下面,例如 Jaeger 或不执行任何操作的空默认实现可以被隐藏。
我们正在使用 作为 Opentrace 的实现。 它由几个组件组成:

服务跟踪、OpenTracing 和 Jaeger

  • Jaeger-agent 是一个本地代理,通常安装在每台机器上,服务通过本地默认端口登录到它。 如果没有代理,那么该机器上所有服务的痕迹通常都会被禁用
  • Jaeger-collector - 所有代理将收集的跟踪发送给它,并将它们放入选定的数据库中
  • 数据库是他们首选的 cassandra,但我们使用 Elasticsearch,还有一些其他数据库的实现以及不将任何内容保存到磁盘的内存中实现
  • Jaeger-query 是一项访问数据库并返回已收集的跟踪以供分析的服务
  • Jaeger-ui 是一个用于搜索和查看痕迹的 Web 界面,它转到 jaeger-query

服务跟踪、OpenTracing 和 Jaeger

一个单独的组件可以称为opentrace jaeger针对特定语言的实现,通过它将span发送到jaeger-agent。
在 Java 中连接 Jagger 归根结底是实现 io.opentracing.Tracer 接口,之后通过它的所有跟踪都将飞向真正的代理。

服务跟踪、OpenTracing 和 Jaeger

同样对于弹簧组件,您可以连接 opentracing-spring-cloud-starter 以及 Jaeger 的实施 opentracing-spring-jaeger-cloud-starter 它将自动配置对通过这些组件的所有内容的跟踪,例如对控制器的 http 请求、通过 jdbc 对数据库的请求等。

跟踪 Java 中的日志记录

在顶层的某个地方,必须创建第一个 Span,这可以自动完成,例如,在收到请求时由 spring 控制器完成,或者如果没有请求则手动完成。 然后通过下面的范围进行传输。 如果下面的任何方法想要添加一个 Span,它会从 Scope 中获取当前的 activeSpan,创建一个新的 Span 并表示其父级是生成的 activeSpan,并使新的 Span 处于活动状态。 当调用外部服务时,当前活动的跨度被传递给它们,并且这些服务参考这个跨度创建新的跨度。
所有工作都经过Tracer实例,可以通过DI机制获取,如果DI机制不起作用的话,也可以将GlobalTracer.get()作为全局变量。 默认情况下,如果跟踪器尚未初始化,NoopTracer 将返回,但不执行任何操作。
进一步,通过 ScopeManager 从跟踪器中获取当前作用域,从当前作用域创建一个新作用域并绑定新的作用域,然后关闭创建的作用域,即关闭创建的作用域并将之前的作用域返回到活跃状态。 Scope 是绑定在一个线程上的,所以在多线程编程时,一定不要忘记将活动的 Span 转移到另一个线程,以便参考这个 Span 进一步激活另一个线程的 Scope。

io.opentracing.Tracer tracer = ...; // GlobalTracer.get()

void DoSmth () {
   try (Scope scope = tracer.buildSpan("DoSmth").startActive(true)) {
      ...
   }
}
void DoOther () {
    Span span = tracer.buildSpan("someWork").start();
    try (Scope scope = tracer.scopeManager().activate(span, false)) {
        // Do things.
    } catch(Exception ex) {
        Tags.ERROR.set(span, true);
        span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex, Fields.MESSAGE, ex.getMessage()));
    } finally {
        span.finish();
    }
}

void DoAsync () {
    try (Scope scope = tracer.buildSpan("ServiceHandlerSpan").startActive(false)) {
        ...
        final Span span = scope.span();
        doAsyncWork(() -> {
            // STEP 2 ABOVE: reactivate the Span in the callback, passing true to
            // startActive() if/when the Span must be finished.
            try (Scope scope = tracer.scopeManager().activate(span, false)) {
                ...
            }
        });
    }
}

对于多线程编程,还有TracedExecutorService和类似的包装器,它们在异步任务启动时自动将当前span转发到线程:

private ExecutorService executor = new TracedExecutorService(
    Executors.newFixedThreadPool(10), GlobalTracer.get()
);

对于外部http请求有 追踪HttpClient

HttpClient httpClient = new TracingHttpClientBuilder().build();

我们面临的问题

  • 如果跟踪器未在服务或组件中使用,则 Bean 和 DI 并不总是有效,那么 自动接线 Tracer 可能无法工作,您必须使用 GlobalTracer.get()。
  • 如果注释不是组件或服务,或者从同一类的相邻方法调用该方法,则注释不起作用。 您必须小心检查哪些有效,如果 @Traced 不起作用,请使用手动跟踪创建。 您还可以为 java 注释附加一个额外的编译器,那么它们应该可以在任何地方工作。
  • 在旧的spring和spring boot中,由于DI中的bug,opentraing spring cloud自动配置不起作用,那么如果你想让spring组件中的trace自动工作,你可以类推 github.com/opentracing-contrib/java-spring-jaeger/blob/master/opentracing-spring-jaeger-starter/src/main/java/io/opentracing/contrib/java/spring/jaeger/starter/JaegerAutoConfiguration.java
  • Try with resources 在groovy 中不起作用,您必须使用try finally。
  • 每个服务必须有自己的 spring.application.name,跟踪记录将在该名称下记录。 销售和测试有什么不同的名称,以免它们放在一起造成干扰。
  • 如果您使用 GlobalTracer 和 tomcat,那么在该 tomcat 中运行的所有服务都有一个 GlobalTracer,因此它们都将具有相同的服务名称。
  • 向方法添加跟踪时,需要确保不会在循环中多次调用该方法。 有必要为所有调用添加一个公共跟踪,这保证了总的工作时间。 否则,将会产生过大的负载。
  • 有一次在 jaeger-ui 中,对大量跟踪发出了过大的请求,由于没有等待响应,所以又做了一次。 结果,jaeger-query开始吃掉大量内存并减慢弹性。 通过重新启动 jaeger-query 来帮助

采样、存储和查看痕迹

共有三种类型 采样痕迹:

  1. Const 发送并保存所有跟踪。
  2. 概率性,以给定的概率过滤痕迹。
  3. 速率限制,限制每秒的跟踪数。 您可以在客户端、jaeger-agent 或收集器上配置这些设置。 现在我们在赋值器堆栈中使用 const 1,因为请求不是很多,但需要很长时间。 将来,如果这会对系统造成过大的负载,您可以对其进行限制。

如果您使用 cassandra,那么默认情况下它仅存储两天的痕迹。 我们正在使用 elasticsearch 并且痕迹会一直保存,不会被删除。 每天都会创建一个单独的索引,例如 jaeger-service-2019-03-04。 以后需要配置自动清理旧痕迹。

为了查看您需要的痕迹:

  • 选择要用来过滤跟踪的服务,例如,tomcat7-default 表示在 tomcat 中运行且不能有自己的名称的服务。
  • 然后选择操作、时间间隔和最短操作时间,例如从 10 秒开始,以便只执行长时间执行。
    服务跟踪、OpenTracing 和 Jaeger
  • 走到其中一条痕迹处,看看那里有什么东西在减速。
    服务跟踪、OpenTracing 和 Jaeger

另外,如果某个请求 id 已知,那么您可以通过标签搜索找到该 id 的跟踪(如果该 id 记录在跟踪范围中)。

Документация

用品

视频

来源: habr.com

添加评论