MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

微服务架构中的服务调用链追踪

2021-04-185.4k 阅读

微服务架构中的服务调用链追踪

服务调用链追踪的背景与需求

在传统的单体应用架构中,系统的各个功能模块紧密耦合在一个进程中。当应用出现问题时,开发者可以相对容易地通过在代码中添加日志、断点调试等方式定位问题。例如,在一个简单的Java单体Web应用中,当用户请求某个页面出现异常时,开发者可以直接在处理该请求的Servlet或Spring Controller方法中添加日志语句,查看变量值,很快就能确定问题出在业务逻辑的哪一步。

然而,随着微服务架构的兴起,系统被拆分成多个小型的、独立部署的服务。每个服务可能由不同的团队开发、维护,并且使用不同的技术栈。例如,一个电商系统可能将用户服务用Java开发,商品服务用Python开发,订单服务用Go语言开发。当一个业务请求需要调用多个微服务时,问题的排查变得异常复杂。假设一个用户下单的操作,可能需要依次调用用户服务验证用户信息、商品服务检查库存、订单服务创建订单等多个微服务。如果下单失败,开发者很难快速确定是哪个服务出现了问题,是服务本身的逻辑错误,还是服务之间的调用出现了故障,亦或是网络问题导致部分服务不可达。

这就迫切需要一种机制能够清晰地记录和展示一个请求在各个微服务之间的调用路径和状态,服务调用链追踪技术应运而生。它能够帮助开发者快速定位微服务架构中的性能瓶颈和错误根源,提高系统的可维护性和可靠性。

服务调用链追踪的基本概念

  1. Trace(追踪)
    • Trace是对一次业务请求在整个微服务架构中的完整调用路径的记录。它从请求进入系统的入口开始,到请求最终返回响应结束,涵盖了该请求所涉及的所有微服务调用。例如,在一个用户登录并获取个人信息的场景中,请求先进入认证服务进行登录验证,然后调用用户信息服务获取个人信息,整个过程就构成一个Trace。
    • 在代码层面,一个Trace通常用一个唯一的标识符(如UUID)来标识。在Java中,可以使用java.util.UUID类生成唯一ID,示例代码如下:
    import java.util.UUID;
    public class TraceIdGenerator {
        public static String generateTraceId() {
            return UUID.randomUUID().toString();
        }
    }
    
  2. Span(跨度)
    • Span代表Trace中的一个基本工作单元,通常对应一次微服务调用。每个Span有自己的开始时间和结束时间,通过计算两者的差值可以得到该Span的执行时间,从而评估该微服务调用的性能。例如,上述用户登录场景中,认证服务的调用就是一个Span,用户信息服务的调用又是另一个Span。
    • Span也有唯一的标识符,并且它需要知道自己所属的Trace的ID,以便将自身关联到正确的Trace中。同时,Span还可以有父Span(如果该调用是由另一个Span发起的)。在Python中,可以使用如下简单的类来表示Span:
    import time
    
    class Span:
        def __init__(self, span_id, trace_id, parent_span_id=None):
            self.span_id = span_id
            self.trace_id = trace_id
            self.parent_span_id = parent_span_id
            self.start_time = time.time()
            self.end_time = None
    
        def end(self):
            self.end_time = time.time()
            return self.end_time - self.start_time
    
  3. Annotation(注解)
    • Annotation用于在Span中记录关键事件,比如请求的发送(CS - Client Send)、请求的接收(SR - Server Receive)、响应的发送(SS - Server Send)、响应的接收(CR - Client Receive)等。这些注解有助于更精确地分析服务调用的各个阶段,确定问题出在请求处理的哪个环节。例如,如果发现某个Span的SR和SS之间时间过长,可能意味着服务内部处理逻辑存在性能问题;如果CR和CS之间时间过长,可能是网络传输存在延迟。

常见的服务调用链追踪工具

  1. OpenTelemetry
    • 概述:OpenTelemetry是一个开源的可观测性框架,旨在为云原生应用提供统一的追踪、度量和日志收集标准。它提供了丰富的SDK,支持多种编程语言,包括Java、Python、Go等,使得开发者可以方便地在不同技术栈的微服务中集成追踪功能。
    • 使用示例(以Java为例)
      • 首先,在Maven项目中添加OpenTelemetry依赖:
      <dependency>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-api</artifactId>
          <version>1.12.0</version>
      </dependency>
      <dependency>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-sdk</artifactId>
          <version>1.12.0</version>
      </dependency>
      
      • 然后,在代码中创建和使用Span:
      import io.opentelemetry.api.trace.Span;
      import io.opentelemetry.api.trace.Tracer;
      import io.opentelemetry.sdk.trace.SdkTracerProvider;
      import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
      import io.opentelemetry.sdk.trace.export.StdoutSpanExporter;
      
      public class OpenTelemetryExample {
          public static void main(String[] args) {
              SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
                     .addSpanProcessor(SimpleSpanProcessor.create(StdoutSpanExporter.create()))
                     .build();
              Tracer tracer = tracerProvider.get("my - tracer");
              Span span = tracer.spanBuilder("my - span").startSpan();
              try {
                  // 业务逻辑
                  System.out.println("Doing some work in the span");
              } finally {
                  span.end();
                  tracerProvider.shutdown();
              }
          }
      }
      
  2. Jaeger
    • 概述:Jaeger是Uber开源的分布式追踪系统,它受Dapper的启发,专注于解决微服务架构中的服务调用链追踪问题。Jaeger提供了一个易于使用的Web界面,用于展示追踪数据,方便开发者直观地查看调用链的结构、各Span的执行时间等信息。
    • 架构组成
      • Jaeger - Agent:部署在每个微服务实例所在的节点上,负责接收来自应用程序的追踪数据,并将其批量发送给Collector。它设计为轻量级,对应用程序的性能影响较小。
      • Jaeger - Collector:接收来自Agent的追踪数据,进行验证、转换和存储。它支持多种存储后端,如Cassandra、Elasticsearch等。
      • Jaeger - Query:提供Web界面,用于查询和展示追踪数据。用户可以通过该界面根据Trace ID、服务名称等条件搜索调用链,并查看详细的Span信息。
    • 使用示例(以Node.js为例)
      • 安装Jaeger客户端库:
      npm install jaeger - client
      
      • 在代码中初始化和使用Jaeger:
      const tracer = require('jaeger - client').initTracer({
          serviceName:'my - service',
          sampler: {
              type: 'const',
              param: 1
          }
      });
      
      const span = tracer.startSpan('my - operation');
      try {
          // 业务逻辑
          console.log('Doing some work in the span');
      } finally {
          span.finish();
          tracer.close();
      }
      
  3. Zipkin
    • 概述:Zipkin是Twitter开源的分布式追踪系统,它致力于提供一个通用的模型和工具来收集、存储和查询服务调用链数据。Zipkin具有良好的可扩展性,可以与多种服务发现和负载均衡工具集成。
    • 架构组成
      • Zipkin Collector:负责接收来自各个微服务的追踪数据,支持多种数据格式,如JSON、Thrift等。
      • Zipkin Storage:用于存储追踪数据,支持MySQL、Cassandra、Elasticsearch等多种存储后端。
      • Zipkin UI:提供Web界面,用于展示调用链的依赖关系、各服务的性能指标等信息。用户可以通过该界面进行追踪数据的查询和分析。
    • 使用示例(以Spring Boot为例)
      • pom.xml中添加Zipkin依赖:
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring - cloud - starter - sleuth</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring - cloud - starter - zipkin</artifactId>
      </dependency>
      
      • application.yml中配置Zipkin服务器地址:
      spring:
        zipkin:
          base - url: http://zipkin - server:9411
        sleuth:
          sampler:
            probability: 1.0
      
      • 这样,Spring Boot应用就会自动将追踪数据发送到Zipkin服务器。

服务调用链追踪在微服务架构中的实现

  1. 在服务内部实现追踪
    • 生成Trace和Span:在每个微服务的入口处,生成一个新的Trace ID(如果是请求的初始入口)或从请求头中提取已有的Trace ID。同时,为当前服务的处理过程创建一个新的Span。例如,在一个基于Spring Boot的微服务中,可以使用Spring Cloud Sleuth来自动完成这些操作。Spring Cloud Sleuth会在请求进入Controller时自动生成Trace和Span,并将相关信息注入到请求头中,方便后续的服务调用传递。
    • 记录Annotation:在服务处理过程的关键节点,如方法调用前后、数据库操作前后等,记录Annotation。例如,在调用另一个微服务之前记录CS注解,在收到另一个微服务的响应后记录CR注解。以Java代码为例,可以使用AOP(面向切面编程)来实现这种通用的注解记录。假设使用AspectJ框架,代码如下:
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import io.opentelemetry.api.trace.Span;
    import io.opentelemetry.api.trace.Tracer;
    import io.opentelemetry.context.Context;
    import io.opentelemetry.context.Scope;
    
    @Aspect
    public class TraceAspect {
        private final Tracer tracer;
    
        public TraceAspect(Tracer tracer) {
            this.tracer = tracer;
        }
    
        @Around("@annotation(traceable)")
        public Object traceMethod(ProceedingJoinPoint joinPoint, Traceable traceable) throws Throwable {
            Span span = tracer.spanBuilder(traceable.value()).startSpan();
            try (Scope scope = Context.current().with(span).makeCurrent()) {
                // 记录CS注解
                span.addEvent("Client Send");
                Object result = joinPoint.proceed();
                // 记录CR注解
                span.addEvent("Client Receive");
                return result;
            } finally {
                span.end();
            }
        }
    }
    
  2. 服务间传递追踪信息
    • 请求头传递:最常用的方式是通过HTTP请求头传递Trace ID、Span ID等追踪信息。当一个微服务调用另一个微服务时,将当前的Trace和Span信息添加到HTTP请求头中,接收方微服务从请求头中提取这些信息,从而将调用链延续下去。例如,在Python的Flask应用中,可以使用如下代码在请求头中传递追踪信息:
    from flask import Flask, request, make_response
    import requests
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        trace_id = request.headers.get('X - Trace - ID')
        span_id = request.headers.get('X - Span - ID')
        new_headers = {
            'X - Trace - ID': trace_id,
            'X - Span - ID': span_id
        }
        response = requests.get('http://another - service', headers = new_headers)
        return make_response(response.content, response.status_code)
    
    • 消息队列传递:在使用消息队列进行微服务间通信的场景中,同样需要将追踪信息包含在消息体中。例如,在使用Kafka作为消息队列时,生产者在发送消息前,将Trace和Span信息添加到消息的头部或自定义字段中,消费者在接收消息后,从消息中提取这些信息,继续构建调用链。

服务调用链追踪数据的分析与应用

  1. 性能分析
    • 找出性能瓶颈:通过分析服务调用链中各Span的执行时间,可以确定哪些微服务或哪些操作是性能瓶颈。例如,如果发现某个微服务的Span执行时间明显长于其他微服务,就需要深入该微服务内部,检查其业务逻辑、数据库查询等操作是否存在优化空间。假设在一个电商系统中,订单服务的某个Span执行时间长达500毫秒,而其他服务的Span执行时间大多在100毫秒以内,就需要重点关注订单服务的相关代码,可能是复杂的业务计算或者低效的数据库查询导致了性能问题。
    • 优化服务调用顺序:根据调用链中各Span的先后顺序和执行时间,可以评估是否可以优化服务调用的顺序以提高整体性能。例如,如果两个微服务的调用没有严格的先后依赖关系,且其中一个微服务执行时间较长,可以考虑并行调用这两个微服务,从而缩短整个请求的处理时间。
  2. 错误排查
    • 定位错误根源:当业务请求出现错误时,通过服务调用链追踪数据,可以快速定位到哪个微服务在处理过程中出现了错误。例如,如果一个用户注册请求失败,通过查看调用链,可以发现是用户信息服务在保存用户数据时抛出了数据库约束违反的异常,从而确定问题出在用户信息服务的数据库操作部分。
    • 分析错误传播路径:不仅可以定位错误发生的微服务,还可以分析错误是如何在服务调用链中传播的。这有助于了解错误对整个业务流程的影响范围,以及是否存在错误处理不当导致错误扩散的情况。例如,某个微服务在捕获异常后没有正确处理,而是将错误信息以不规范的格式返回给调用方,导致调用方无法正确解析错误,进一步引发其他问题。通过追踪数据可以清晰地看到这种错误传播路径,从而改进错误处理机制。
  3. 容量规划
    • 评估服务负载:通过分析服务调用链中各微服务的调用频率和执行时间,可以评估每个微服务的负载情况。例如,如果某个微服务在高峰时段被频繁调用,且每次调用的执行时间较长,说明该微服务可能面临较大的负载压力,需要考虑增加资源或者进行优化。
    • 预测未来需求:基于历史的服务调用链追踪数据,可以对未来的业务流量和服务负载进行预测。例如,如果发现某个业务功能的调用量随着业务的发展呈现明显的增长趋势,就可以提前规划相应微服务的资源扩展,以避免出现性能问题。

服务调用链追踪面临的挑战与解决方案

  1. 性能开销
    • 挑战:在微服务中添加服务调用链追踪功能,不可避免地会带来一定的性能开销。生成Trace和Span、记录Annotation、传递追踪信息等操作都会占用一定的CPU、内存和网络资源,可能对微服务的性能产生影响,尤其是在高并发场景下。
    • 解决方案
      • 采样:通过设置采样率,只对部分请求进行完整的追踪。例如,可以设置1%的采样率,即每100个请求中只对1个请求进行详细的追踪记录。这样可以在一定程度上减少性能开销,同时又能获取到足够的样本数据用于分析。大多数追踪工具都支持灵活的采样策略,如OpenTelemetry可以通过配置不同的采样器来实现不同的采样方式。
      • 优化代码实现:在实现追踪功能时,尽量优化代码,减少不必要的计算和资源消耗。例如,使用高效的ID生成算法来生成Trace和Span ID,避免复杂的字符串操作;在记录Annotation时,只记录关键信息,避免过多的日志记录。
  2. 数据存储与管理
    • 挑战:随着微服务数量的增加和业务流量的增长,服务调用链追踪产生的数据量会迅速膨胀。如何高效地存储、查询和管理这些数据成为一个挑战。如果存储方案不合理,可能导致查询性能下降,甚至数据丢失。
    • 解决方案
      • 选择合适的存储后端:根据数据的特点和查询需求选择合适的存储后端。对于需要快速查询和分析的场景,Elasticsearch是一个不错的选择,它具有高效的全文检索和聚合分析能力;对于需要高可用和大规模数据存储的场景,Cassandra等分布式存储系统更为合适。例如,Jaeger支持将追踪数据存储在Cassandra或Elasticsearch中,用户可以根据实际情况进行选择。
      • 数据压缩与清理:对追踪数据进行定期的压缩和清理,减少存储空间的占用。可以根据数据的重要性和时效性制定清理策略,例如,只保留最近一周的详细追踪数据,更早的数据进行压缩存储或删除。
  3. 跨语言与跨框架集成
    • 挑战:在微服务架构中,不同的微服务可能使用不同的编程语言和框架开发。要实现统一的服务调用链追踪,需要在各种技术栈之间进行有效的集成,确保追踪信息能够准确传递和处理。
    • 解决方案
      • 使用通用标准:采用通用的追踪标准,如OpenTelemetry,它提供了跨语言的SDK,使得在不同编程语言的微服务中实现追踪功能变得相对容易。无论微服务是用Java、Python还是Go开发,都可以基于OpenTelemetry的标准进行集成。
      • 封装与适配:对于一些特定的框架,可以开发封装层或适配器,简化集成过程。例如,对于Spring Boot应用,可以使用Spring Cloud Sleuth来方便地集成Zipkin或其他追踪工具,Spring Cloud Sleuth对底层的追踪实现进行了封装,提供了统一的编程模型,降低了开发者的集成成本。