在 Istio 中跟踪 gRPC

点击查看目录

Aspen Mesh 很喜欢用gRPC。Apen Mesh 面向公众的 API 和许多内部 API 大多都是使用 gRPC 构建的。如果您还没有听说过 gRPC(熟练掌握 gRPC 真的很难),那么我先为您简单的介绍下,它是一种新型、高效和优化的远程过程调用(RPC)框架。gRPC 基于protocol buffer序列化格式和HTTP/2网络协议。

使用HTTP/2协议,gRPC应用程序可以利用多路复用请求显著提高连接利用率,而且比起如HTTP/1.1等其他协议具有更多增强功能。此外,protocal buffer 是以二进制方式对结构化数据进行序列化,这比起基于文本的序列化方式更简单且可扩展,还可以显着提高性能。将这两个结果组合在一个低延迟和高度可扩展的 RPC 框架中,这实质上就是 gRPC。此外,不断增长的 gRPC 生态支持使用多种语言编写应用程序,例如(C ++、Java、Go 等),还包括大量第三方

除了上面列出的好处之外,gRPC 让我最喜欢的一点是可以让我以简单直观的方式指定 RPC(使用 protobuf IDL)以及客户端调用服务器端的方法,就好像是调用本地函数一样。很多代码(服务描述和处理程序、客户端方法等)都可以自动生成,这使得 gRPC 非常好用。

现在我已经介绍了 gRPC 的一些背景知识,我们再把注意力转回到博客的主题。在这里,我将介绍如何在基于 gRPC 的应用程序中添加跟踪,特别是如果您使用 Istio 或 Aspen Mesh。

跟踪(Tracing)非常适合于调试和理解应用程序的行为。理解所有跟踪数据的关键是能够关联来自与单个客户端请求相关的多个不同微服务的跨度(span)。

为了实现这一点,应用程序中的所有微服务应该传播跟踪 header。如果您使用的是像 Istio 或 Aspen Mesh 这样的服务网格,ingress 和 sidecar 代理会自动添加适当的跟踪 header,并将这些 span 报告给跟踪收集器后端,如 Jaeger 或 Zipkin。应用程序唯一要做的就是将传入请求(sidecar 或 ingress 代理添加的)的跟踪 header 传播到其对其他微服务的所有传出请求。

gRPC 到 grpc 请求传播 header

使用 gRPC,跟踪 header 传播的最简单方法是使用grpc opentracing middleware库的客户端拦截器。如果您的 gRPC 应用程序在收到传入请求时发出新的出站 gRPC 请求,则可以使用此功能。以下是将传入的跟踪 header 正确传播到传出的 gRPC 请求的示例代码:

  import (
    "golang.org/x/net/context"
    "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
    "ot "github.com/opentracing/opentracing-go"
  )

  // ctx is the incoming gRPC request's context
  // addr is the address for the new outbound request
  func createGRPCConn(ctx context.Context, addr string) (*grpc.ClientConn, error) {
  	var opts []grpc.DialOption
  	opts = append(opts, grpc.WithStreamInterceptor(
  		grpc_opentracing.StreamClientInterceptor(
  			grpc_opentracing.WithTracer(ot.GlobalTracer()))))
  	opts = append(opts, grpc.WithUnaryInterceptor(
  		grpc_opentracing.UnaryClientInterceptor(
  			grpc_opentracing.WithTracer(ot.GlobalTracer()))))
  	conn, err := grpc.DialContext(ctx, addr, opts...)
  	if err != nil {
  		glog.Error("Failed to connect to application addr: ", err)
  		return nil, err
  	}
  	return conn, nil
  }

很简单对吧?

添加 opentracing 客户端拦截器可确保在客户端连接上创建任何新的一元(unary)或流式 gRPC 请求注入正确的跟踪 header。如果传递的上下文中存在跟踪 header(如使用 Aspen Mesh 或 Istio 传入入站 gRPC 请求上下文),则新创建的 span 将作为传递的上下文中已存在的 span 的子 span。另外,如果上下文中没有跟踪信息,则会为出站 gRPC 请求创建新的根 span。

gRPC 到 HTTP 请求传播 header

我们再来看下这个场景,如果您的应用程序在收到一个新传入的 gRPC 请求时发出一个出站 HTTP/1.1 请求。以下是在此情况下完成 header 传播的示例代码:

  import (
    "net/http"
    "golang.org/x/net/context"
    "golang.org/x/net/context/ctxhttp"
    "ot "github.com/opentracing/opentracing-go"
  )

  // ctx is the incoming gRPC request's context
  // addr is the address of the application being requested
  func makeNewRequest(ctx context.Context, addr string) {
    if span := ot.SpanFromContext(ctx); span != nil {
      req, _ := http.NewRequest("GET", addr, nil)

      ot.GlobalTracer().Inject(
        span.Context(),
        ot.HTTPHeaders,
        ot.HTTPHeadersCarrier(req.Header))

      resp, err := ctxhttp.Do(ctx, nil, req)
      // Do something with resp
    }
  }

这是序列化传入请求(HTTP 或 gRPC)上下文中跟踪 header 的标准方式。

很好,至此我们已经能够使用库或标准实用程序代码来实现我们想要的功能。

使用 grpc-gateway 时传播 header

gRPC 应用程序中有一个常用的库grpc-gateway,可以将 gRPC 服务作为 RESTful JSON API 暴露出来。当您想要了解 gRPC 或维护 RESTful 架构,使用 curl、web 浏览器等客户端时,这非常有用。有关如何使用grpc-gateway从 gRPC 中暴露 RESTful API 的更多细节请参考这个博客。如果您对此架构不熟悉,我强烈建议您阅读。

当您开始使用grpc-gateway并想传播跟踪 header 时,有一些值得一提的非常有趣的交互。 grpc-gateway 文档指出,作为 gRPC 请求 header,所有 IANA(互联网号码分配局)永久 HTTP header 都以grpcgateway-作为前缀并添加。这很好,但是像x-b3-traceidx-b3-spanid等跟踪 header 不是 IANA 认可的永久 HTTP header,当grpc-gateway代理 HTTP 请求时,它们不会被复制到 gRPC 请求中。这意味着只要将grpc-gateway添加到您的应用程序中,header 传播逻辑就会停止工作。

这是个特例吗?添加一个东西打断了当前的工作。不用担心,我为您解决问题!

这是一种确保使用grpc-gateway在 HTTP 和 gRPC 之间进行代理时不会丢失跟踪信息的方法:

  import (
    "net/http"
    "golang.org/x/net/context"
    "google.golang.org/grpc/metadata"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
  )

  const (
  	prefixTracerState  = "x-b3-"
  	zipkinTraceID      = prefixTracerState + "traceid"
  	zipkinSpanID       = prefixTracerState + "spanid"
  	zipkinParentSpanID = prefixTracerState + "parentspanid"
  	zipkinSampled      = prefixTracerState + "sampled"
  	zipkinFlags        = prefixTracerState + "flags"
  )

  var otHeaders = []string{
  	zipkinTraceID,
  	zipkinSpanID,
  	zipkinParentSpanID,
  	zipkinSampled,
  	zipkinFlags}

  func injectHeadersIntoMetadata(ctx context.Context, req *http.Request) metadata.MD {
  	pairs := []string{}
  	for _, h := range otHeaders {
  		if v := req.Header.Get(h); len(v) > 0 {
  			pairs = append(pairs, h, v)
  		}
  	}
  	return metadata.Pairs(pairs...)
  }

  type annotator func(context.Context, *http.Request) metadata.MD

  func chainGrpcAnnotators(annotators ...annotator) annotator {
  	return func(c context.Context, r *http.Request) metadata.MD {
  		mds := []metadata.MD{}
  		for _, a := range annotators {
  			mds = append(mds, a(c, r))
  		}
  		return metadata.Join(mds...)
  	}
  }

  // Main function of your application. Insert tracing headers into gRPC
  // metadata using annotators
  func run() {
    ...
	  annotators := []annotator{injectHeadersIntoMetadata}

	  gwmux := runtime.NewServeMux(
		  runtime.WithMetadata(chainGrpcAnnotators(annotators...)),
	  )
    ...
  }

在上面的代码中,我使用了grpc-gateway库中的runtime.WithMetadata。该 API 从 HTTP 请求中读取属性并将其添加到 gRPC 元数据中,这一点非常有用,这正是我们想要的!虽然多了一步,但仍然使用库提供的 API。

injectHeadersIntoMetadata注解器在 HTTP 请求中查找跟踪 header 并将其附加到 gRPC 元数据中,从而确保跟踪 header 可以使用前面部分中提到的技术从 gRPC 进一步传播到出站请求。

您可能观察到的另一个有趣的事情是chainGrpcAnnotators包装函数。runtime.WithMetadata API 只允许添加一个注释器,这可能不足以满足所有场景。在我们的例子中,我们有一个跟踪注释器(如上面的一个示例)和一个认证注释器,它将来自 HTTP 请求的认证数据附加到 gRPC 元数据。使用chainGrpcAnnotators允许您添加多个注释器,并且包装函数将来自各种注释器的元数据加入到 gRPC 请求的单个元数据中。

宋净超

宋净超

Tetrate 布道师、云原生社区创始人

编辑本页