Zipkin简介
Zipkin是一个分布式跟踪系统,它可以帮助收集调用关系和调用时间数据,管理这些数据的收集和查找。Zipkin的设计是基于谷歌的Google Dapper论文。收集数据的方式采用每个应用程序向Zipkin报告定时数据,同时Zipkin UI呈现了一个依赖图表来展示多少跟踪请求经过了每个应用程序和调用耗时。效果如下图:
之所以选用Zipkin作为链路追踪工具,一是已有比较完善的go官方库,zipkin-go,实现了opentracing协议,并且已经实现数据收集、上报、采样等功能。二是Zipkin是单个服务,部署方便,包括四大功能,分别是数据采集、存储、查找、UI,部署简单。其中采集和存储又都可以灵活配置使用,采集方式包括Http, Kafka, Scribe三种可选,存储可使用Memory、Cassandra、ElasticSearch和MySQL
架构图
Zipkin在go中具体实践
对官方库zipkin-go进行了功能的扩展和封装,方便更好地用于业务中,代码位置:
https://git.xesv5.com/golib/gotools/tree/master/utils/traceutil
主要功能包括添加了在函数调用间的链路数据收集、适配xesContext、支持rpcx跨服务追踪、支持通过kafka消息进行追踪等。
一、 适配xesContext
链路追踪过程中,需要一个上下文来保存当前链路信息,zipkin-go中使用go原生的Context来保存,且存储链路信息的key为一个struct。而当前业务使用xesContext,是对gin.Context和原生Context的封装,不支持struct为key,所以采用xesContext存储链路信息,使用了新的字符串常量做key。
在服务内部使用xesContext传递,调用方法为:
span, ctx := traceutil.Trace(ctx, “main”)
if span != nil {
defer span.Finish()
}
Trace方法调用StartSpanFromContext,ctx内没有span,会生成链路的根,有的话则生成下一级span。
但由于xesContext存储的是指针,Trace方法在多层调用后,会出现混乱,最后在StartSpanFromContext方法结束前copy了xesContext解决。
二、 跨服务追踪
zipkin-go库已实现http调用和grpc调用的链路追踪,而业务使用rpc框架为rpcx,需要实现rpcx调用过程的链路追踪。具体参考http和grpc的实现,服务间收集的信息能够形成链路,就一定要将TraceId等关键信息传递过去,Zipkin中具体包括TraceID、SpanID、ParentSpanID、Sampled、Flags这五个。http将信息赋值给header,grpc将赋值到metadata中。rpcx调用时,也会带有metadata字段,同样地方法,将这五个字段插入到rpcx的metadata中,服务方再从metadata中解析出来。
在rpcx服务间进行链路追踪,client调用方法为:
span, ctx := traceutil.TraceRPCXInject(ctx, serviceMethod)
if span != nil {
defer span.Finish()
}
该方法将ctx中链路信息中的五个字段注入到request的metadata中
server调用方法为:
span, ctx := traceutil.TraceRPCXExtract(ctx, “CallMethod”)
if span != nil {
defer span.Finish()
}
该方法将metadata中信息解析,并形成新的span,链路信息存储到ctx中继续向后传。
Kafka异步调用过程中的链路同样需要记录,而这种调用方式信息传输的载体则是Kafka消息,消息是结构体编码的字符串,将链路关键信息注入到结构体中,消费端再从结构体中解出链路信息,则可完成链路追踪。最终实现了通过结构体传输链路的方法,使用不限于Kafka消息。
注入侧调用方法为:
span, ctx := traceutil.TraceMetadataInject(ctx,“sendkafka”, metadata)
if span != nil {
defer span.Finish()
}
该方法将ctx中链路信息中的五个字段注入到metadata中,metadata为map[string]string类型。
消费侧调用方法为:
span, ctx := traceutil.TraceMetadataExtract(ctx,“receivekafka”, metadata)
if span != nil {
defer span.Finish()
}
该方法将metadata中信息取出,生成span,并将链路信息存储在ctx里
三、 Kafka收集
Zipkin数据采集可以使用http、kafka、scribe三种方式,目前选用kafka作为收集方式,每个应用程序将采集到的数据发送到kafka中,有Zipkin来消费指定topic,转存到ES中。在前期测试中发现,采集速率较大时,转存到ES中会出现部分请求被拒绝,数据出现丢失,对比使用http则不会出现这个问题。原因在于zipkin-go在使用http采集时,对数据进行批量发送,而kafka采集则是每一条数据对于kafka的一条消息,Zipkin在消费后,请求ES频率过高,部分请求被拒绝。最后通过修改zipkin-go库中的kafka收集代码,将消息同样进行批量发送,问题得到解决。
Golang 链路跟踪实施方案
方案介绍
目前系统出现问题,需要大量的查询日志以及数据,定位问题成本高,并且问题出现的场景上线文很难快速定位,所以需要对请求进行链式跟踪监控,因此引入了链路跟踪方案,方案主要分为以下三部分:
- 基于网关日志的服务间链路跟踪
- 基于服务内部方法调用的离线日志链路跟踪
- 基于服务内部方法调用的实时数据流链路跟踪
架构图
链路跟踪组件traceutil使用
traceutil
组件说明
traceutil是对接开源链路跟踪组件zipkin的工具类库,可以控制采样粒度对服务内外进行链路跟踪,定位链路问题。
使用方法
http服务及rpc client使用
//生成span对象
//参数1 context为请求上下文,链路共用,存储链路信息
//参数2 submitCourseWareTest为节点名称
span, newCtx := traceutil.Trace(context, "submitCourseWareTest")
if span != nil {
//节点参数注入 可以从链路跟踪界面查看节点数据
logtraceutil.AppendKeyValue(newCtx, "zipkinTraceId", `"`+span.Context().TraceID.String()+`"`)
logtraceMap := logtraceutil.ExtractMetadataFromXesContext(newCtx)
span.Tag("x_trace_id", logtraceMap["x_trace_id"])
span.Tag("stuId", cast.ToString(stuId))
span.Tag("liveId", cast.ToString(liveId))
span.Tag("testPlan", cast.ToString(testPlan))
span.Tag("packageId", cast.ToString(packageId))
//切记要回收span
defer span.Finish()
}
rpc服务使用
//生成span对象
//参数1 context为请求上下文,链路共用,存储链路信息
//参数2 rpcNode为节点名称
span, ctx := traceutil.TraceRPCXExtract(context, "rpcNode")
if span != nil {
//回收对象
defer span.Finish()
}
异步发kafka使用
//生成span对象
//参数1 Context为请求上下文,链路共用
//参数2 testsendkafka为节点名称
//参数3 submitRecord.Metadata,将trace节点信息写入Metadata,传输到kafka下游
kafkaspan, _ := traceutil.TraceMetadataInject(this.Context, "testsendkafka", submitRecord.Metadata)
if kafkaspan != nil {
kafkaspan.Tag("testIds", submitRecord.TestIds)
defer kafkaspan.Finish()
}
//发送kafka消息
topic := confutil.GetConf("KafkaTopic", "test_topic")
logtraceutil.InjectMetadata(this.Context, &submitRecord.LogTraceMetadata)
var recordBuf bytes.Buffer
if err = msgp.Encode(&recordBuf, submitRecord); err != nil {
logger.Ex(this.Context, logTag, "msgp err:%v", err)
return nil, logger.NewError("msgp logrecord序列化失败")
}
if !this.Bench {
if err = sendKafka(topic, "logs", recordBuf.Bytes()); err != nil {
logger.Ex(this.Context, logTag, "Send2Proxy:%v", err)
return nil, logger.NewError("答题推送kafka失败")
}
}
组件配置
[Trace]
//kafka集群节点信息
kafka=public-log-kafka-1:9092 public-log-kafka-2:9092 public-log-kafka-3:9092 public-log-kafka-4:9092 public-log-kafka-5:9092 public-log-kafka-6:9092
//采样比例配置
sample=0.001
//服务名称
servername=xesApi