通过ASM实现gRPC链路追踪

可观测链路OpenTelemetry版为分布式应用的开发者提供了完整的调用链路还原、调用请求量统计、链路拓扑、应用依赖分析等工具。本文介绍如何通过Headers在ASM实现gRPC链路追踪。

前提条件

示例工程

gRPC的示例工程请参见hello-servicemesh-grpc,本文档中提到的目录都为hello-servicemesh-grpc下的目录。

GRPC协议Headers编程实践

服务端获取Headers

  • 基本方法

    • 使用Java语言通过服务端获取Headers实现基本方法。

      实现拦截器ServerInterceptor接口的interceptCall(ServerCall<ReqT, RespT> call,final Metadata m,ServerCallHandler<ReqT, RespT> h)方法,通过String v = m.get(k)获取header信息,get方法入参类型为Metadata.Key<String>

    • 使用Go语言通过服务端获取Headers实现基本方法。

      metadata.FromIncomingContext(ctx)(md MD, ok bool),MD是一个map[string][]string

    • 使用NodeJS语言通过服务端获取Headers实现基本方法。

      call.metadata.getMap(),返回值类型是[key: string]: MetadataValueMetadataValue类型定义为string/Buffer

    • 使用Python语言通过服务端获取Headers实现基本方法。

      context.invocation_metadata(),返回值类型为2-tuple数组,2-tuple的形式为('k','v'),使用m.key, m.value获取键值对。

  • Unary RPC

    • 使用Java语言通过服务端获取Headers实现Unary RPC。

      对Headers无感知。

    • 使用Go语言通过服务端获取Headers实现Unary RPC。

      在方法中直接调用metadata.FromIncomingContext(ctx),上下文参数ctx来自Talk的入参。

    • 使用NodeJS语言通过服务端获取Headers实现Unary RPC。

      在方法内直接调用call.metadata.getMap()

    • 使用Python语言通过服务端获取Headers实现Unary RPC。

      在方法内直接调用context.invocation_metadata()

  • Server streaming RPC

    • 使用Java语言通过服务端获取Headers实现Server streaming RPC。

      对Headers无感知。

    • 使用Go语言通过服务端获取Headers实现Server streaming RPC。

      在方法中直接调用metadata.FromIncomingContext(ctx),上下文参数ctx从TalkOneAnswerMore的入参stream中获取stream.Context()

    • 使用NodeJS语言通过服务端获取Headers实现Server streaming RPC。

      在方法内直接调用call.metadata.getMap()

    • 使用Python语言通过服务端获取Headers实现Server streaming RPC。

      在方法内直接调用context.invocation_metadata()

  • Client streaming RPC

    • 使用Java语言通过服务端获取Headers实现Client streaming RPC。

      对Headers无感知。

    • 使用Go语言通过服务端获取Headers实现Client streaming RPC。

      在方法中直接调用metadata.FromIncomingContext(ctx),上下文参数ctx从TalkMoreAnswerOne的入参stream中获取stream.Context()

    • 使用NodeJS语言通过服务端获取Headers实现Client streaming RPC。

      在方法内直接调用call.metadata.getMap()

    • 使用Python语言通过服务端获取Headers实现Client streaming RPC。

      在方法内直接调用context.invocation_metadata()

  • Bidirectional streaming RPC

    • 使用Java语言通过服务端获取Headers实现Bidirectional streaming RPC。

      对Headers无感知。

    • 使用Go语言通过服务端获取Headers实现Bidirectional streaming RPC。

      在方法中直接调用metadata.FromIncomingContext(ctx),上下文参数ctx从TalkBidirectional的入参stream中获取stream.Context()

    • 使用NodeJS语言通过服务端获取Headers实现Bidirectional streaming RPC。

      在方法内直接调用call.metadata.getMap()

    • 使用Python语言通过服务端获取Headers实现Bidirectional streaming RPC。

      在方法内直接调用context.invocation_metadata()

客户端发送Headers

  • 基本方法

    • 使用Java语言通过客户端发送Headers实现基本方法。

      实现拦截器ClientInterceptor接口的interceptCall(MethodDescriptor<ReqT, RespT> m, CallOptions o, Channel c)方法,实现返回值类型ClientCall<ReqT, RespT>的start((Listener<RespT> l, Metadata h))方法,通过h.put(k, v)填充header信息,put方法入参k的类型为Metadata.Key<String>v的类型为String

    • 使用Go语言通过客户端发送Headers实现基本方法。

      metadata.AppendToOutgoingContext(ctx,kv ...) context.Context

    • 使用NodeJS语言通过客户端发送Headers实现基本方法。

      metadata=call.metadata.getMap()metadata.add(key, headers[key])

    • 使用Python语言通过客户端发送Headers实现基本方法。

      metadata_dict = {}变量填充metadata_dict[c.key] = c.value,最终转为list tuple类型list(metadata_dict.items())

  • Unary RPC

    • 使用Java语言通过客户端发送Headers实现Unary RPC。

      对Headers无感知。

    • 使用Go语言通过客户端发送Headers实现Unary RPC。

      在方法中直接调用metadata.AppendToOutgoingContext(ctx,kv)

    • 使用NodeJS语言通过客户端发送Headers实现Unary RPC。

      在方法内直接使用基本方法。

    • 使用Python语言通过客户端发送Headers实现Unary RPC。

      在方法内直接使用基本方法。

  • Server streaming RPC

    • 使用Java语言通过客户端发送Headers实现Server streaming RPC。

      对Headers无感知。

    • 使用Go语言通过客户端发送Headers实现Server streaming RPC。

      在方法中直接调用metadata.AppendToOutgoingContext(ctx,kv)

    • 使用NodeJS语言通过客户端发送Headers实现Server streaming RPC。

      在方法内直接使用基本方法。

    • 使用Python语言通过客户端发送Headers实现Server streaming RPC。

      在方法内直接使用基本方法。

  • Client streaming RPC

    • 使用Java语言通过客户端发送Headers实现Client streaming RPC。

      对Headers无感知。

    • 使用Go语言通过客户端发送Headers实现Client streaming RPC。

      在方法中直接调用metadata.AppendToOutgoingContext(ctx,kv)

    • 使用NodeJS语言通过客户端发送Headers实现Client streaming RPC。

      在方法内直接使用基本方法。

    • 使用Python语言通过客户端发送Headers实现Client streaming RPC。

      在方法内直接使用基本方法。

  • Bidirectional streaming RPC

    • 使用Java语言通过客户端发送Headers实现Bidirectional streaming RPC。

      对Headers无感知。

    • 使用Go语言通过客户端发送Headers实现Bidirectional streaming RPC。

      在方法中直接调用metadata.AppendToOutgoingContext(ctx,kv)

    • 使用NodeJS语言通过客户端发送Headers实现Bidirectional streaming RPC。

      在方法内直接使用基本方法。

    • 使用Python语言通过客户端发送Headers实现Bidirectional streaming RPC。

      在方法内直接使用基本方法。

propagate Headers

由于链路追踪需要将上游传递过来的链路元数据透传给下游,以形成同一条请求链路的完整信息,需要将服务端获取的Headers信息中,和链路追踪相关的Headers透传给向下游发起请求的客户端。

除了Java语言的实现,其他语言的通信模型方法都对Headers有感知,因此可以将服务端读取Headers-传递Headers-客户端发送Headers这三个动作有顺序地在4种通信模型方法内部实现。

Java语言读取和写入Headers是通过两个拦截器分别实现的,因此propagate Headers无法在一个顺序的流程里实现,且考虑到并发因素,以及只有读取拦截器知道链路追踪的唯一ID,所以无法通过最直觉的缓存方式搭建两个拦截器的桥梁。

Java语言的实现提供了一种Metadata-Context Propagation的机制。机制

在服务器拦截器读取阶段,通过ctx.withValue(key, metadata)Metadata/Header存入Context,其中Key是Context.Key<String>类型。然后在客户端拦截器中,通过key.get()Metadata从Context读出,get方法默认使用Context.current()上下文,这就保证了一次请求的Headers读取和写入使用的是同一个上下文。

有了propagate Headers的实现,基于GRPC的链路追踪就有了机制上的保证。

部署和验证网格拓扑

实现gRPC链路追踪之前,您需要部署和验证网格拓扑,确保网格拓扑是可以通信的。

进入示例工程的tracing目录,该目录下包含4种编程语言的部署脚本。以下以Go版本为例,部署和验证网格拓扑。

cd go
# 部署
sh apply.sh
# 验证
sh test.sh

如果没有出现异常信息,则说明网格拓扑可以正常通信。

部署后的服务网格拓扑如下图所示。网络拓扑

链路追踪

  1. 将链路追踪数据采集到阿里云可观测链路OpenTelemetry版。具体操作,请参见将链路追踪数据采集到阿里云可观测链路OpenTelemetry版

  2. 登录可观测链路OpenTelemetry版,在左侧导航栏,单击链路入口

  3. 链路入口页面,单击目标应用的应用拓扑

    可以看到完整的链路,包括本地请求端-Ingressgateway-grpc-server-svc1-grpc-server-svc2-grpc-server-svc3。链路追踪

  4. 全链路聚合页面,单击全链路聚合页签,查看全链路聚合。

    全链路聚合
  5. 全链路聚合页签,单击Span名称下的链路,查看调用链路。

    调用链路