3.用go-kit整合链路追踪

本文详细描述了mainflux项目中如何使用go-kit架构体系,包括HTTP和gRPC客户端和服务端的链路追踪实现,以及如何通过opentracing和jaeger工具确保服务间调用的跟踪。重点介绍了SDK接口设计、endpoint和middleware的使用,以及在数据操作层如何整合追踪逻辑。
摘要由CSDN通过智能技术生成

mainflux开源项目是用go-kit架构体系,它的代码在各个服务之间的链路并没有用jeager完全打通,这里说明了如何把链路上各个节点串起来。

1.1 http客户端

1.1.1 项目流程介绍

项目中如果一个微服务要对其他服务暴露http api接口,需要把http client调用放到 pkg/sdk/go中实现。

添加任何一个接口都要加入到sdk.go中定义的SDK接口,如下:

// SDK contains Mainflux API.
type SDK interface {
	// CreateUser registers mainflux user.
	CreateUser(token string, user User) (string, error)
    
    // ...
}   

然后有个mfSDK的结构体实现了这个接口,每个方法其实就如下步骤:

  • 1、拼接请求数据
  • 2、构建http client
  • 3、发送请求
  • 4、处理响应数据

比如下面拿certs服务中实现的sdk举例子,代码在pkg/sdk/go/certs.go中。

func request(method, jwt, url string, data []byte) (*http.Response, error) {
	req, err := http.NewRequest(method, url, bytes.NewReader(data))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", jwt)

	c := &http.Client{}
	res, err := c.Do(req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

1.1.2 打通追踪

一个http请求,如果想要打通opentracing链路,需要把链路的信息放在http的heade中发送过去,具体发送内容如下例子:

header中会有Uber-Trace-Id, 值为7e7d3e986a3b4497:27a096257f392f37:7e7d3e986a3b4497:1

其中值的部分,分别是 traceID、当前spanid、父spanid、标志位

所以我们在sdk中构建http request时要加入header,其内容来自context中获取的span上下文。

1、首先改造sdk接口,参数必须有ctx

我对于IssueCert这个接口调用想加上链路追踪,就得加上参数context.Context

// SDK contains Mainflux API.
type SDK interface {
	// CreateUser registers mainflux user.
	CreateUser(token string, user User) (string, error)
    
    // IssueCert issues a certificate for a thing required for mtls.
	IssueCert(ctx context.Context, thingID string, keyBits int, keyType, valid, token string) (Cert, error)
    
    // ...
}    

2、具体请求实现时,抽取span上下文到header

核心代码就一句:

kitot.ContextToHTTP(opentracing.GlobalTracer(), sdk.logger)(ctx, req)

下面时certs服务的sdk中,统一实现的request请求封装,可以看到就加了一行代码,即在里面加入链路追踪抽取上下文到header的逻辑。

func request(sdk mfSDK, ctx context.Context, method, jwt, url string, data []byte) (*http.Response, error) {
	req, err := http.NewRequest(method, url, bytes.NewReader(data))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", jwt)

	// 加入jaeger链路
	kitot.ContextToHTTP(opentracing.GlobalTracer(), sdk.logger)(ctx, req)

	c := &http.Client{}
	res, err := c.Do(req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

1.2 http服务端

1.2.1 项目流程介绍

项目采用了go-kit组件搭建,

  • 业务开发定义service接口,然后写个service实现。

  • 对每一个service接口方法创建一个函数类型的闭包,即endpoint。

  • 通过github.com\go-kit\kit\tracing\opentracing 中的TraceEndpoint()构建 endpoint.Middleware,从而实现对endpoint的代理。

  • 通过github.com/go-kit/kit/transport/http包提供的 NewServer,对endpint、decode、encode进行包装得到一个http handler,从而对接一个http路由, 这也是go-kit中transport层的实现。

对于go-kit的http服务,想要打通opentracing链路,需要在transport层对所有构建出的http handler加上统一拦截器,github.com/go-kit/kit/transport/http包中提供的前置和后置的ServerOption。

下面是一个路由处理的配置代码,opts...就可以定义前置后置拦截器。

	r.Post("/things", kithttp.NewServer(
		kitot.TraceServer(tracer, "create_thing")(createThingEndpoint(svc)),
		decodeThingCreation,
		encodeResponse,
		opts...,
	))

对面上面的 kithttp.NewServer参数中,第一个是endpoint闭包,但是这里使用了kitot.TraceServer(),它在一个endpoint基础上加上了middleware,这个就是对此进行了trace。有些项目比如provision没有用这个middleware,如果需要追踪就需要加上这个中间件。

1.2.2 打通追踪

全局tracer

在main.go中初始化jaeger时,把它加到全局单例中

authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger)
defer authCloser.Close()
opentracing.SetGlobalTracer(authTracer)

handler加上middleware

以cert服务的一个endpoint为例子

kitot.TraceServer(tracer, "mapping")(issueCert(svc))

加上前置拦截器

以things微服务为例子,上面说明要改造transport层,对于NewServer()方法提供的ServerOption我们只要实现一个beforeOption,即一个前置拦截器,在这里提供从http的header中抽取spancontext,然后基于它创建span,再把span存到ctx中。这样后续endpoint的关于opentracing的middleware会从ctx中拿到span,有span就会基于它创建子span,这样链路就打通了。

在包kitot "github.com/go-kit/kit/tracing/opentracing"中提供了一个实现好的,即里面的HTTPToContext()。

func MakeHandler(tracer opentracing.Tracer, svc things.Service, logger logger.Logger) http.Handler {
	opts := []kithttp.ServerOption{
		kithttp.ServerErrorEncoder(encodeError),

		// 添加opentracing处理,从http请求中拿到span上下文
		kithttp.ServerBefore(kitot.HTTPToContext(tracer, "HTTP things", logger)),
	}
	
    r := bone.New()

	r.Post("/mapping", kithttp.NewServer(
		kitot.TraceServer(tracer, "mapping")(doProvision(svc)),
		decodeProvisionRequest,
		encodeResponse,
		opts...,
	))
}    

  // ...
    return r
}

加上后置拦截器

在beforeOption中我们加了一个拦截器,但是对于创建出来的span并没有地方进行finish,所以还得需要创建一个后置拦截器,把最初创建的span从ctx中拿出来进行finish()操作。

前置拦截器中创建的span,得存到ctx中,可以定义一个key存到ctx里面,后置处理器按照相同的key去拿即可。

同时改造上面的前置处理器,最后实现如下

var firstSpanKey = firstSpanKeyStruct{}
type firstSpanKeyStruct struct {}

// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc provision.Service, logger logger.Logger) http.Handler {
	// 使用opentracing提供的全局单例
	tracer := opentracing.GlobalTracer()

	opts := []kithttp.ServerOption{
		kithttp.ServerErrorEncoder(encodeError),
		// 添加opentracing处理,从http请求中拿到span上下文
		kithttp.ServerBefore(func(ctx context.Context, request *http.Request) context.Context {
			ctx2 := kitot.HTTPToContext(tracer, "http-provision", logger)(ctx, request)
			// 拿出kitot.HTTPToContext中创建的span,按自己的定义的key写入context,为了在after中进行关闭
			span := opentracing.SpanFromContext(ctx2)
			ctx2 = context.WithValue(ctx2, firstSpanKey, span)
			return ctx2
		}),
		kithttp.ServerAfter(func(ctx context.Context, writer http.ResponseWriter) context.Context {
			span := ctx.Value(firstSpanKey).(opentracing.Span)
			if span != nil {
				span.Finish()
			}
			return ctx
		}),
	}

	r := bone.New()

	r.Post("/mapping", kithttp.NewServer(
		kitot.TraceServer(tracer, "mapping")(doProvision(svc)),
		decodeProvisionRequest,
		encodeResponse,
		opts...,
	))
    
    // ...
    return r
}    

如上总结下,核心就两个地方:

1、确保有beforeOption,即kithttp.ServerBefore(kitot.HTTPToContext(tracer, "HTTP things", logger)),它能从http的header中提取出spancontext,从而生成span写到context中,提供后续使用。

2、每个路由的handler中,对endpoint加上了middleware,即kitot.TraceServer(tracer, "mapping")(doProvision(svc))。它会从context中拿到span,从而为当前路由处理器创建一个子span,要是拿不到span就会创建root span。

1.3 grpc客户端

1.3.1 项目流程介绍

项目中auth、things都提供的grpc接口。

auth微服务的client.go中提供了客户端功能。

通过proto文件生成pb中已经提供了client调用方法,但是项目用的go-kit组件,所以用kit.grpc(github.com/go-kit/kit/transport/grpc)对客户端进行了一层代理,然后还用了github.com/go-kit/kit/tracing/opentracing提供的endpoint.Middlware。

在这个endpoint.Middlware中,就是从ctx里面拿span,拿到了才能穿起来链路。所以在这之前,少了一个逻辑,就是从协议即grpc的metadata中抽取spancontex,然后基于它能创建span。

// NewClient returns new gRPC client instance.
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) opencar.AuthServiceClient {
	return &grpcClient{
		issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
			conn,
			svcName,
			"Issue",
			encodeIssueRequest,
			decodeIssueResponse,
			opencar.UserIdentity{},
		).Endpoint()),
    }
}

1.3.2 打通追踪

以auth微服务为例子说明

grpc的client注入链路上下文

这个步骤就考虑基于kitgrpc.NewClient()方法支持前置后置options的特定,决定创建一个前置option,让它调用ContextToGRPC()方法,即把context中的span的spanContext内容写到metadata中,然后把metadata写到context中,做个grpc协议发送的context。

基于这个beforeOption, 后面再调用具体client grpc时,请求的md中就有spanctx信息。

// NewClient returns new gRPC client instance.
func NewClient(tracer opentracing.Tracer, conn *grpc.ClientConn, timeout time.Duration) opencar.AuthServiceClient {
    options := make([]kitgrpc.ClientOption, 0)
    
	// 从context中拿到jaeger的span上下文,组成grpc的md,返回带md的context
	option := kitgrpc.ClientBefore(func(ctx context.Context, md *metadata.MD) context.Context {
		kitot.ContextToGRPC(tracer, new(innerlogger))(ctx, md)
		ctx = metadata.NewOutgoingContext(ctx, *md)
		return ctx
	})
    
	options = append(options, option)
    
	return &grpcClient{
		issue: kitot.TraceClient(tracer, "issue")(kitgrpc.NewClient(
			conn,
			svcName,
			"Issue",
			encodeIssueRequest,
			decodeIssueResponse,
			opencar.UserIdentity{},
			options...,
		).Endpoint()),
    }
}

关于log的说明,由于kitot对应的包里面,有些方法依赖Logger接口,为了在项目中临时使用方便,我自己在NewClient代码所在go文件中定义了如下接口实现:

// 为了kitot.ContextToGRPC方法需要log接口,临时实现
type innerlogger struct {
}
func (l *innerlogger) Log(keyvals ...interface{}) error {
	log.Println(keyvals)
	return nil
}

实际情况为了保持日志统一,应该用如下可选方式:

  • 1、启动main中实例化kitot中logger接口的实现,通过参数方式传入NewClient中,然后传给kitot.TraceClient函数。

  • 2、定义全局单例log,让他实现kitot中定义的logger接口,然后项目中任意地方可用。

1.4 grpc服务端

1.4.1 项目流程介绍

auth的server获取链路上下文

auth微服务的server.go中提供了grpc服务端功能,通过proto文件生成的pb里面定义好了服务接口,我们只要实现服务,就能提供grpc能力。

但是因为要用go-kit,我们的服务又被go-kit代理的一层,是被kitgrpc "github.com/go-kit/kit/transport/grpc” 的NewServer方法,把我们实现的endpoint和decode、encode包装成了一个个handler。而且还用到了TraceServer(包"github.com/go-kit/kit/tracing/opentracing"),是一个endpoint.Middleware。

// NewServer returns new AuthServiceServer instance.
func NewServer(tracer opentracing.Tracer, svc auth.Service) opencar.AuthServiceServer {
	return &grpcServer{
		issue: kitgrpc.NewServer(
			kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
			decodeIssueRequest,
			encodeIssueResponse,
		),
	}
}

1.4.2 打通追踪

上面的这个kitot.TraceServer创建的endpoint.Middleware里面会从ctx中拿span。

但是ctx中也是没有span的,得有代码实现从context中的md中拿内容生成span,再写到context,这样后面的中间件才能拿。

所以也是基于NewServer方法提供了前置后置option,我们实现前置option从context中抽取span上下文生成span在写入context中,这样后面代码从context中能拿到span,这个span和上一个服务是穿起来的。

为了让我在前置option中创建的span能正常finish,我还把这个span专门存到context中的kv,然后实现一个后置option,在后置option中拿到这个span调用finish。

修改内容:

var firstSpanKey = firstSpanKeyStruct{}
type firstSpanKeyStruct struct {}

// NewServer returns new AuthServiceServer instance.
func NewServer(tracer opentracing.Tracer, svc auth.Service, logger logger.Logger) opencar.AuthServiceServer {
	options := make([]kitgrpc.ServerOption, 0)

	// 从grpc的的md中拿到spanContext信息,基于它创建一个根span(含链路信息),写入context
	beforeOption := kitgrpc.ServerBefore(func(ctx context.Context, md metadata.MD) context.Context {
		f := kitot.GRPCToContext(tracer, "grpc-auth", logger)
		c := f(ctx, md)

		// 拿到kitot.GRPCToContext中创建的span,按自己方式写入context,为了在after中进行关闭
		span := opentracing.SpanFromContext(c)
		c = context.WithValue(c, firstSpanKey, span)
		return c
	})

	// 从ctx中拿到beforeOption中填入的第一个span,执行finish
	afterOption := kitgrpc.ServerAfter(func(ctx context.Context, header *metadata.MD, trailer *metadata.MD) context.Context {
		span := ctx.Value(firstSpanKey).(opentracing.Span)
		if span != nil {
			span.Finish()
		}
		return ctx
	})

	options = append(options, beforeOption, afterOption)

	return &grpcServer{
		issue: kitgrpc.NewServer(
			kitot.TraceServer(tracer, "issue")(issueEndpoint(svc)),
			decodeIssueRequest,
			encodeIssueResponse,
			options...,
		),
    }
}

1.5 数据操作层

数据操作层已经实现了追踪的逻辑,只要确保http服务端客户端都打通了,数据操作层能从ctx中拿到span。

repository层

包含repository的接口定义和具体实现。

mainflux项目中每个微服务关于数据处理的代码在postgres目录中定义,有对数据进行操作的接口定义,即XXXXRepository, 比如下面的ThingRepository接口。

// ThingRepository specifies a thing persistence API.
type ThingRepository interface {
	// Save persists multiple things. Things are saved using a transaction. If one thing
	// fails then none will be saved. Successful operation is indicated by non-nil
	// error response.
	Save(ctx context.Context, ths ...Thing) ([]Thing, error)
}

基于ThingRepository接口,我们要有对应实现,比如构建结构体 thingRepository来实现接口所有方法。

type thingRepository struct {
	db Database
}

service层

每个微服务都是严格参照go-kit的体系标准,里面的service文件会定义service层的接口,以及提供service实现。

// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
	// CreateThings adds things to the user identified by the provided key.
	CreateThings(ctx context.Context, token string, things ...Thing) ([]Thing, error)
}	

service的实现,这个结构体会包含其依赖的Repository接口实现。

type thingsService struct {
	things       ThingRepository
}

也就是,当路由被go-kit代理后交给service处理,当需要处理数据层时,交给具体的Reposity接口实现处理。

repository代理

为了实现对repository层操作的链路追踪,项目采用了“代理模式”,即是创建一个代理结构体 xxxxRepositoryMiddleware 来包含 xxxxRepository, 代理结构体也实现了XXXXRepository接口。

例如 thingRepositoryMiddleware

type thingRepositoryMiddleware struct {
	tracer opentracing.Tracer
	repo   things.ThingRepository
}

func (trm thingRepositoryMiddleware) Save(ctx context.Context, ths ...things.Thing) ([]things.Thing, error) {
	span := createSpan(ctx, trm.tracer, saveThingsOp)
	defer span.Finish()
	ctx = opentracing.ContextWithSpan(ctx, span)

	return trm.repo.Save(ctx, ths...)
}

对于接口定义的每个方法,代理层都必须实现,它就是在调用被代理的repo之前做了如下代理工作:

  • 1、基于ctx创建,通过opentracing包提供的SpanFromContext(ctx) 来创建span。如果ctx中有span,就会创建出一个子span。

  • 2、执行defer span.Finish,即在被代理的repo执行完后,span结束。

1.6 展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值