【GO-Micro】jaeger 分布式链路追踪

本贴最后更新于 1960 天前,其中的信息可能已经东海扬尘

github 完整代码地址

安装 jaeger

jaeger 提供一个 all in one 的 docker 镜像,可以快速搭建实验环境

docker run -d --name jaeger 
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 
-p 5775:5775/udp 
-p 6831:6831/udp 
-p 6832:6832/udp 
-p 5778:5778 
-p 16686:16686 
-p 14268:14268 
-p 9411:9411 
jaegertracing/all-in-one:1.6

OpenTracing

OpenTracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便的添加(或更换)追踪系统的实现。 OpenTracing 提供了用于运营支撑系统的和针对特定平台的辅助程序库。
jaeger 兼容 OpenTracing API,所以我们使用 OpenTracing 的程序库可以方便的替换追踪工具。
OpenTracing 中文文档

jaeger 使用

封住一下 jaeger 的初始化操作方便使用,详细用法可以查看 jaeger-client-go

// lib/tracer

// NewTracer 创建一个jaeger Tracer
func NewTracer(servicename string, addr string) (opentracing.Tracer, io.Closer, error) {
	cfg := jaegercfg.Configuration{
		ServiceName: servicename,
		Sampler: &jaegercfg.SamplerConfig{
			Type:  jaeger.SamplerTypeConst,
			Param: 1,
		},
		Reporter: &jaegercfg.ReporterConfig{
			LogSpans:            true,
			BufferFlushInterval: 1 * time.Second,
		},
	}

	sender, err := jaeger.NewUDPTransport(addr, 0)
	if err != nil {
		return nil, nil, err
	}

	reporter := jaeger.NewRemoteReporter(sender)
	// Initialize tracer with a logger and a metrics factory
	tracer, closer, err := cfg.NewTracer(
		jaegercfg.Reporter(reporter),
	)

	return tracer, closer, err
}

func main() {
t, io, err := tracer.NewTracer("tracer", "")
	if err != nil {
		log.Fatal(err)
	}
	defer io.Close()
	opentracing.SetGlobalTracer(t)
}

opentracing.SetGlobalTracer(t) 方法执行会将 jaeger tracer 注册到全局,接下来只需要使用 opentracing 的标准 API 便可以了。
如果不想使用 jaeger 了,想替换成其他分布式追踪工具,只需要工具支持 opentracing 标准,并将 main 函数的 SetGlobalTracer 操作替换即可,其他文件都不需要更改。

micro 链路追踪插件

micro 自带的 opentracing 插件

在 micro 自带的插件中已经有 opentracing 的插件了,包含 server,client 等,不过这个插件只能 go-micro 构建的微服务(api,srv)中使用。因为 micro 网关有一个独立的插件系统,但是并没有提供 opentracing 相关的插件。

micro/go-plugins/wrapper/trace/opentracing/opentracing.go

我们可以在构建服务的时候直接使用,只需要在服务初始化时增加一行函数就可以了。

service := micro.NewService(
		micro.Name(name),
		micro.Version("latest"),
		micro.WrapHandler(ocplugin.NewHandlerWrapper(opentracing.GlobalTracer())),
	)

srv/user/main.go 目录下的 user 服务是一个完整的使用实例。

为 micro 网关增加 opentracing 插件

实现原理

8491a458868a7ec946247c03da1af99e.png

外部 HTTP 请求首先经过 API 网关,网关生成第一个 SpanContexts 并且通过 HTTP 头传递到聚合层的 API 服务,这边需要我们实现一个插件去做这件事,原理很简单,拦截每一次请求添加信息就可以了。
查看 micro 自带的 opentracing 插件,可以发现是通过 golang 的 context 传递,micro 的 RPC 已经封装好了通过 context 在跨进程服务间传递 SpanContexts 机制,所以我们需要在 API 服务层实现一个插件,从 HTTP 头中取出 SpanContexts 并按照 micro 自带的方式注入 golang context。

// micro opentracing插件中wHandlerWrappe
// NewHandlerWrapper accepts an opentracing Tracer and returns a Handler Wrapper
func NewHandlerWrapper(ot opentracing.Tracer) server.HandlerWrapper {
	return func(h server.HandlerFunc) server.HandlerFunc {
		return func(ctx context.Context, req server.Request, rsp interface{}) error {
			name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
			ctx, span, err := traceIntoContext(ctx, ot, name)
			if err != nil {
				return err
			}
			defer span.Finish()
			return h(ctx, req, rsp)
		}
	}
}

micro API 网关插件

lib/wrapper/tracer/opentracing/stdhttp/stdhttp.go

和实现 JWT 鉴权插件一样,实现一个 HTTP 中间件通过 mciro 的插件机制全局注册就可以实现拦截每次请求并处理。

// TracerWrapper tracer wrapper
func TracerWrapper(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		spanCtx, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
		sp := opentracing.GlobalTracer().StartSpan(r.URL.Path, opentracing.ChildOf(spanCtx))
		defer sp.Finish()

		if err := opentracing.GlobalTracer().Inject(
			sp.Context(),
			opentracing.HTTPHeaders,
			opentracing.HTTPHeadersCarrier(r.Header)); err != nil {
			log.Println(err)
		}

		sct := &status_code.StatusCodeTracker{ResponseWriter: w, Status: http.StatusOK}
		h.ServeHTTP(sct.WrappedResponseWriter(), r)

		ext.HTTPMethod.Set(sp, r.Method)
		ext.HTTPUrl.Set(sp, r.URL.EscapedPath())
		ext.HTTPStatusCode.Set(sp, uint16(sct.Status))
		if sct.Status >= http.StatusInternalServerError {
			ext.Error.Set(sp, true)
		}
	})
}

Tracer 相关的概念可以查看这个文档

  1. opentracing.GlobalTracer().Extract 方法提取 HTTP 头中的 spanContexts
  2. opentracing.ChildOf 方法基于提取出来的 spanContexts 生成新的 child spanContexts
  3. opentracing.GlobalTracer().StartSpan 方法生成一个新的 span
  4. github.com/opentracing/opentracing-go/ext 通过 ext 可以为追踪添加一些 tag 来展示更多信息,比如 URL,请求类型(GET,POST...), 返回码
  5. sp.Finish() 结束这一个 span

API 服务(使用 gin)插件

lib/wrapper/tracer/opentracing/gin2micro/gin2micro.go

// TracerWrapper tracer 中间件
func TracerWrapper(c *gin.Context) {
	md := make(map[string]string)
	spanCtx, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header))
	sp := opentracing.GlobalTracer().StartSpan(c.Request.URL.Path, opentracing.ChildOf(spanCtx))
	defer sp.Finish()

	if err := opentracing.GlobalTracer().Inject(sp.Context(),
		opentracing.TextMap,
		opentracing.TextMapCarrier(md)); err != nil {
		log.Log(err)
	}

	ctx := context.TODO()
	ctx = opentracing.ContextWithSpan(ctx, sp)
	ctx = metadata.NewContext(ctx, md)
	c.Set(contextTracerKey, ctx)

	c.Next()

	statusCode := c.Writer.Status()
	ext.HTTPStatusCode.Set(sp, uint16(statusCode))
	ext.HTTPMethod.Set(sp, c.Request.Method)
	ext.HTTPUrl.Set(sp, c.Request.URL.EscapedPath())
	if statusCode >= http.StatusInternalServerError {
		ext.Error.Set(sp, true)
	}
}

// ContextWithSpan 返回context
func ContextWithSpan(c *gin.Context) (ctx context.Context, ok bool) {
	v, exist := c.Get(contextTracerKey)
	if exist == false {
		ok = false
		return
	}

	ctx, ok = v.(context.Context)
	return
}

基本操作流程和给 micro 编写的插件相同,但是有两点不同。其一,因为我使用 gin 开发 API 服务,所以基于 gin 的 API。其二,因为 micro 内部提供通过 golang context 传递 spanContexts 的机制,所以将这边会将 child spanContexts 注入到 gin 的 context,在 API 服务通过 micro 提供 RPC 接口(生成的 XX.micro.go 文件中调用函数第一个参数都是 context)调用其他服务时传入提取的 context,如下:

...
ctx, ok := gin2micro.ContextWithSpan(c)
	if ok == false {
		log.Log("get context err")
	}

	res, err := s.helloC.Call(ctx, &helloS.Request{Name: "xuxu"})
...

完整的实现细节可以查看,github 仓库中 lib/wrapper/tracer/opentracing, 这里

完整体验

a9a2b4a65c424f4a198e930cbd09abe2.png34e3293430a69f2d7e8da9fa177ad5a3.png

  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    497 引用 • 1387 回帖 • 294 关注
  • jaeger
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...