洞若观火:使用OpenTracing增强Istio的调用链跟踪-篇二

 
 

640?wx_fmt=jpeg

作者:赵化冰
审校:罗广明
原文链接:https://www.servicemesher.com/blog/using-opentracing-with-istio-part-2/

在上一篇文章中,我们通过一个网上商店的示例程序学习了如何使用Opentracing在Istio服务网格中传递分布式调用跟踪的上下文,以及如何将方法级的调用信息加入到Istio/Envoy生成的调用链中。采用Opentracing可以减少应用代码中传递HTTP header的重复代码;也可以根据需要在调用链中加入更细粒度的Span,以用于对系统性能瓶颈进行在线分析。

在实际项目中,除了同步调用之外,异步消息也是微服务架构中常见的一种通信方式。在本篇文章中,我将继续利用eshop demo程序来探讨如何通过Opentracing将Kafka异步消息也纳入到Istio的分布式调用跟踪中。

eshop 示例程序结构

如下图所示,demo程序中增加了发送和接收Kafka消息的代码。eshop微服务在调用inventory,billing,delivery服务后,发送了一个kafka消息通知,consumer接收到通知后调用notification服务的REST接口向用户发送购买成功的邮件通知。

640?wx_fmt=jpeg

将Kafka消息处理加入调用链跟踪

植入Kafka Opentracing代码

首先从github下载代码。

git clone git@github.com:zhaohuabing/istio-opentracing-demo.gitgit checkout kafka-tracking
git checkout kafka-tracking

可以直接使用 kafka-tracking 这个分支的代码,但建议跟随下面的步骤查看相关的代码,以了解各个步骤背后的原理。

根目录下分为了rest-service和kafka-consumer两个目录,rest-service下包含了各个REST服务的代码,kafka-consumer下是Kafka消息消费者的代码。

首先需要将spring kafka和Opentracing kafka的依赖加入到两个目录下的pom文件中。

<dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka</artifactId></dependency> <dependency>    <groupId>io.opentracing.contrib</groupId>    <artifactId>opentracing-kafka-client</artifactId>    <version>${version.opentracing.kafka-client}</version></dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
 <dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-kafka-client</artifactId>
    <version>${version.opentracing.kafka-client}</version>
</dependency>

在rest-service目录中的KafkaConfig.java中配置消息Producer端的Opentracing Instrument。TracingProducerInterceptor会在发送Kafka消息时生成发送端的Span。

@Beanpublic ProducerFactory<String, String> producerFactory() {    Map<String, Object> configProps = new HashMap<>();    configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);    configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);    configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);    configProps.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName());    return new DefaultKafkaProducerFactory<>(configProps);}
public ProducerFactory<String, String> producerFactory() {
    Map<String, Object> configProps = new HashMap<>();
    configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
    configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName());
    return new DefaultKafkaProducerFactory<>(configProps);
}

在kafka-consumer目录中的KafkaConfig.java中配置消息Consumer端的Opentracing Instrument。TracingConsumerInterceptor会在接收到Kafka消息是生成接收端的Span。

@Beanpublic ConsumerFactory<String, String> consumerFactory() {    Map<String, Object> props = new HashMap<>();    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);    props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);    props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName());    return new DefaultKafkaConsumerFactory<>(props);}
public ConsumerFactory<String, String> consumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName());
    return new DefaultKafkaConsumerFactory<>(props);
}

只需要这两步即可完成Spring程序的Kafka Opentracing代码植入。下面安装并运行示例程序查看效果。

安装Kafka集群

示例程序中使用到了Kafka消息,因此需要部署一个Kafka集群。可以参照 Kafka Quickstart 在Kubernetes集群外部署Kafka;也可以使用 Kafka Operator 直接将Kafka部署在Kubernetes集群中。

部署demo应用

修改Kubernetes yaml部署文件 k8s/eshop.yaml,设置Kafka bootstrap server,以用于demo程序连接到Kafka集群中。

apiVersion: extensions/v1beta1kind: Deploymentmetadata:  name: eshop-v1  ......    spec:      containers:      - name: eshop        image: zhaohuabing/istio-opentracing-demo:kafka-opentracing        ports:        - containerPort: 8080        env:          ....          //在这里加入Kafka server地址          - name: KAFKA_BOOTSTRAP_SERVERS            value: "192.168.89.192:9092"---apiVersion: extensions/v1beta1kind: Deploymentmetadata:  name: kafka-consumer-v1  ......    spec:      containers:      - name: kafka-consumer        image: zhaohuabing/istio-opentracing-demo-kafka-consumer:kafka-opentracing        env:          ....          //在这里加入Kafka server地址          - name: KAFKA_BOOTSTRAP_SERVERS            value: "192.168.89.192:9092"
metadata:
  name: eshop-v1
  ......
    spec:
      containers:
      - name: eshop
        image: zhaohuabing/istio-opentracing-demo:kafka-opentracing
        ports:
        - containerPort: 8080
        env:
          ....
          //在这里加入Kafka server地址
          - name: KAFKA_BOOTSTRAP_SERVERS
            value: "192.168.89.192:9092"

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kafka-consumer-v1
  ......
    spec:
      containers:
      - name: kafka-consumer
        image: zhaohuabing/istio-opentracing-demo-kafka-consumer:kafka-opentracing
        env:
          ....
          //在这里加入Kafka server地址
          - name: KAFKA_BOOTSTRAP_SERVERS
            value: "192.168.89.192:9092"

然后部署应用程序,相关的镜像可以直接从dockerhub下载,也可以通过源码编译生成。

kubectl apply -f k8s/eshop.yaml

在浏览器中打开地址:http://${NODE_IP}:31380/checkout,以触发调用eshop示例程序的REST接口。然后打开Jaeger的界面 http://${NODE_IP}:30088 查看生成的分布式调用跟踪信息。 

640?wx_fmt=jpeg

从图中可以看到,在调用链中增加了两个Span,分布对应于Kafka消息发送和接收的两个操作。由于Kafka消息的处理是异步的,消息发送端不直接依赖接收端的处理。根据Opentracing对引用关系的定义,From_eshop_topic Span 对 To_eshop_topic Span 的引用关系是 FOLLOWS_FROM 而不是 CHILD_OF 关系。

将调用跟踪上下文从Kafka传递到REST服务

现在eshop代码中已经加入了REST和Kafka的Opentracing Instrumentation,可以在进行REST调用和发送Kafka消息时生成调用跟踪信息。但如果需要从Kafka的消息消费者的处理方法中调用一个REST接口呢?

我们会发现在eshop示例程序中,缺省生成的调用链里面并不会把Kafka消费者的Span和其发起的调用notification服务的REST请求的Span关联在同一个Trace中。

要分析导致该问题的原因,我们首先需要了解“Active Span”的概念。在Opentracing中,一个线程可以有一个Active Span,该Active Span代表了目前该线程正在执行的工作。在调用Tracer.buildSpan()方法创建新的Span时,如果Tracer目前存在一个Active Span,则会将该Active Span缺省作为新创建的Span的Parent Span。

Tracer.buildSpan 方法的说明如下:

Tracer.SpanBuilder buildSpan(String operationName)Return a new SpanBuilder for a Span with the given `operationName`.You can override the operationName later via BaseSpan.setOperationName(String).A contrived example:   Tracer tracer = ...   // Note: if there is a `tracer.activeSpan()`, it will be used as the target of an implicit CHILD_OF   // Reference for "workSpan" when `startActive()` is invoked.   // 如果存在active span,则其创建的新Span会隐式地创建一个 CHILD_OF 引用到该active span   try (ActiveSpan workSpan = tracer.buildSpan("DoWork").startActive()) {       workSpan.setTag("...", "...");       // etc, etc   }   // 也可以通过asChildOf方法指定新创建的Span的Parent Span   // It's also possible to create Spans manually, bypassing the ActiveSpanSource activation.   Span http = tracer.buildSpan("HandleHTTPRequest")                     .asChildOf(rpcSpanContext)  // an explicit parent                     .withTag("user_agent", req.UserAgent)                     .withTag("lucky_number", 42)                     .startManual();

   // Note: if there is a `tracer.activeSpan()`, it will be used as the target of an implicit CHILD_OF
   // Reference for "workSpan" when `startActive()` is invoked.
   // 如果存在active span,则其创建的新Span会隐式地创建一个 CHILD_OF 引用到该active span
   try (ActiveSpan workSpan = tracer.buildSpan("DoWork").startActive()) {
       workSpan.setTag("...""...");
       // etc, etc
   }

   // 也可以通过asChildOf方法指定新创建的Span的Parent Span
   // It's also possible to create Spans manually, bypassing the ActiveSpanSource activation.
   Span http = tracer.buildSpan("HandleHTTPRequest")
                     .asChildOf(rpcSpanContext)  // an explicit parent
                     .withTag("user_agent", req.UserAgent)
                     .withTag("lucky_number"42)
                     .startManual();

分析Kafka Opentracing Instrumentation的代码,会发现TracingConsumerInterceptor在调用Kafka消费者的处理方法之前已经把消费者的Span结束了,因此发起REST调用时tracer没有active span,不会将Kafka消费者的Span作为后面REST调用的parent span。

public static <K, V> void buildAndFinishChildSpan(ConsumerRecord<K, V> record, Tracer tracer,      BiFunction<String, ConsumerRecord, String> consumerSpanNameProvider) {    SpanContext parentContext = TracingKafkaUtils.extractSpanContext(record.headers(), tracer);    String consumerOper =        FROM_PREFIX + record.topic(); // <====== It provides better readability in the UI    Tracer.SpanBuilder spanBuilder = tracer        .buildSpan(consumerSpanNameProvider.apply(consumerOper, record))        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CONSUMER);    if (parentContext != null) {      spanBuilder.addReference(References.FOLLOWS_FROM, parentContext);    }    Span span = spanBuilder.start();    SpanDecorator.onResponse(record, span);    //在调用消费者的处理方法之前,该Span已经被结束。    span.finish();    // Inject created span context into record headers for extraction by client to continue span chain    //这个Span被放到了Kafka消息的header中    TracingKafkaUtils.inject(span.context(), record.headers(), tracer);  }static <K, V> void buildAndFinishChildSpan(ConsumerRecord<K, V> record, Tracer tracer,
      BiFunction<String, ConsumerRecord, String> consumerSpanNameProvider)
 
{
    SpanContext parentContext = TracingKafkaUtils.extractSpanContext(record.headers(), tracer);

    String consumerOper =
        FROM_PREFIX + record.topic(); // <====== It provides better readability in the UI
    Tracer.SpanBuilder spanBuilder = tracer
        .buildSpan(consumerSpanNameProvider.apply(consumerOper, record))
        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CONSUMER);

    if (parentContext != null) {
      spanBuilder.addReference(References.FOLLOWS_FROM, parentContext);
    }

    Span span = spanBuilder.start();
    SpanDecorator.onResponse(record, span);

    //在调用消费者的处理方法之前,该Span已经被结束。
    span.finish();

    // Inject created span context into record headers for extraction by client to continue span chain
    //这个Span被放到了Kafka消息的header中
    TracingKafkaUtils.inject(span.context(), record.headers(), tracer);
  }

此时TracingConsumerInterceptor已经将Kafka消费者的Span放到了Kafka消息的header中,因此从Kafka消息头中取出该Span,显示地将Kafka消费者的Span作为REST调用的Parent Span即可。

为MessageConsumer.java使用的RestTemplate设置一个TracingKafka2RestTemplateInterceptor。

@KafkaListener(topics = "eshop-topic")public void receiveMessage(ConsumerRecord<String, String> record) {    restTemplate            .setInterceptors(Collections.singletonList(new TracingKafka2RestTemplateInterceptor(record.headers())));    restTemplate.getForEntity("http://notification:8080/sendEmail", String.class);}"eshop-topic")
public void receiveMessage(ConsumerRecord<String, String> record) {
    restTemplate
            .setInterceptors(Collections.singletonList(new TracingKafka2RestTemplateInterceptor(record.headers())));
    restTemplate.getForEntity("http://notification:8080/sendEmail", String.class);
}

TracingKafka2RestTemplateInterceptor是基于Spring Opentracing Instrumentation的TracingRestTemplateInterceptor修改的,将从Kafka header中取出的Span设置为出向请求的Span的Parent Span。

@Overridepublic ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution xecution)        throws IOException {    ClientHttpResponse httpResponse;    SpanContext parentSpanContext = TracingKafkaUtils.extractSpanContext(headers, tracer);    Span span = tracer.buildSpan(httpRequest.getMethod().toString()).asChildOf(parentSpanContext)            .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();    ......}
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution xecution)
        throws IOException 
{
    ClientHttpResponse httpResponse;
    SpanContext parentSpanContext = TracingKafkaUtils.extractSpanContext(headers, tracer);
    Span span = tracer.buildSpan(httpRequest.getMethod().toString()).asChildOf(parentSpanContext)
            .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();
    ......
}

在浏览器中打开地址:http://${NODE_IP}:31380/checkout ,以触发调用eshop示例程序的REST接口。然后打开Jaeger的界面 http://${NODE_IP}:30088 查看生成的分布式调用跟踪信息。 

640?wx_fmt=jpeg

从上图可以看到,调用链中出现了Kafka消费者调用notification服务的sendEmail REST接口的Span。从图中可以看到,由于调用链经过了Kafka消息,sendEmail Span的时间没有包含在checkout Span中。

640?wx_fmt=jpeg

在Jaeger UI上将图形切换为trace graph,可以更清晰地表示出各个Span之间的调用关系。

总结

Istio服务网格通过分布式调用跟踪来提高微服务应用的可见性。我们可以使用Opentracing Instrumentation来代替应用编码传递分布式跟踪的相关http header;还可以将方法级的调用跟踪和Kafka消息的调用跟踪加入到Istio生成的调用跟踪链中,以提供更细粒度的调用跟踪信息。

该方案可以达到分布式调用跟踪的目的,但需要在代码框架层进行一定的改动,以植入调用跟踪的相关代码。理想的方案是由服务网格基础设施层来完成所有调用跟踪的数据收集和生成,这样应用代码只需关注业务逻辑,而不用处理调用跟踪信息的生成。可以在Envoy中加入插件来为Kafka消息生成调用跟踪信息,但目前看来服务网格还没有很好的办法在上下游服务之前传递调用跟踪上下文。

参考资料

  1. 本文中eshop示例程序的源代码

  2. Distributed Tracing with Apache Kafka and Jaeger

  3. [OpenTracing Apache Kafka Client Instrumentation](https://github.com/opentracing-contrib/java-kafka-client
    TracingRestTemplateInterceptor.java)

  4. Kafka quick start

  5. 参考代码:


第 6 届 Service Mesh Meetup Topic 征集

Hi ServiceMeshers,让大家久等了,继去年的五届 Service Mesh Meetup 后沉寂了半年,现在 2019 年度的第一场 Service Mesh Meetup 将于 8 月 11 日(星期日)再度登临广州,还是老地方广电平云广场,欢迎报名提交议题(请直接微信联系 jimmysongio),也欢迎大家届时报名参加!




 推荐阅读 

洞若观火:使用OpenTracing增强Istio的调用链跟踪-篇一

使用Envoy和Jaeger实现分布式追踪

Vistio—使用Netflix的Vizceral可视化Istio service mesh

Envoy功能点详解之异常点检测

640?wx_fmt=jpeg

点击 阅读原文 查看更多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习是一种人工智能(AI)的子领域,致力于研究如何利用数据和算法让计算机系统具备学习能力,从而能够自动地完成特定任务或者改进自身性能。机器学习的核心思想是让计算机系统通过学习数据中的模式和规律来实现目标,而不需要显式地编程。 机器学习应用非常广泛,包括但不限于以下领域: 图像识别和计算机视觉: 机器学习在图像识别、目标检测、人脸识别、图像分割等方面有着广泛的应用。例如,通过深度学习技术,可以训练神经网络来识别图像中的对象、人脸或者场景,用于智能监控、自动驾驶、医学影像分析等领域。 自然语言处理: 机器学习在自然语言处理领域有着重要的应用,包括文本分类、情感分析、机器翻译、语音识别等。例如,通过深度学习模型,可以训练神经网络来理解和生成自然语言,用于智能客服、智能助手、机器翻译等场景。 推荐系统: 推荐系统利用机器学习算法分析用户的行为和偏好,为用户推荐个性化的产品或服务。例如,电商网站可以利用机器学习算法分析用户的购买历史和浏览行为,向用户推荐感兴趣的商品。 预测和预测分析: 机器学习可以用于预测未来事件的发生概率或者趋势。例如,金融领域可以利用机器学习算法进行股票价格预测、信用评分、欺诈检测等。 医疗诊断和生物信息学: 机器学习在医疗诊断、药物研发、基因组学等领域有着重要的应用。例如,可以利用机器学习算法分析医学影像数据进行疾病诊断,或者利用机器学习算法分析基因数据进行疾病风险预测。 智能交通和物联网: 机器学习可以应用于智能交通系统、智能城市管理和物联网等领域。例如,可以利用机器学习算法分析交通数据优化交通流量,或者利用机器学习算法分析传感器数据监测设备状态。 以上仅是机器学习应用的一部分,随着机器学习技术的不断发展和应用场景的不断拓展,机器学习在各个领域都有着重要的应用价值,并且正在改变我们的生活和工作方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值