简介
jaeger是一个比较有名的分布式链路追踪系统,底层用golang实现,兼容opentracing标准。
文档地址:docs
github地址:github
官网:website
blog:blog
部署
我们用docker部署,集成整套环境all-in-one,docker地址:https://hub.docker.com/r/jaegertracing/all-in-one
注意: 在 all in one 模式下,jaeger 存储数据使用的是内存,因此重启 dockre 后就看不到之前的数据了。所以,该模式仅用于前期的 demo 或者测试验证,不可在生产环境中使用这种模式部署。
直接运行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:latest
执行完成后,用
命令:docker ps
看看运行起来没,这里看结果已经运行了:
访问jaeger的web界面:
localhost:16686
如果你是远程,这里的localhost可以换成你的服务器ip,或者你配置的域名。
在单体应用中实现Tracing
package main import ( "context" "fmt" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" "time" ) func main() { initJaeger("jager-test-demo") } func initJaeger(serviceName string) { cfg := jaegercfg.Configuration{ Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, LocalAgentHostPort: "122.51.156.172:6831", // 替换 122.51.156.172 }, } closer, err := cfg.InitGlobalTracer( serviceName, ) if err != nil { fmt.Printf("Could not initialize jaeger tracer: %s", err.Error()) return } var ctx = context.TODO() span1, ctx := opentracing.StartSpanFromContext(ctx, "span_1") time.Sleep(time.Second / 2) span11, _ := opentracing.StartSpanFromContext(ctx, "span_1-1") time.Sleep(time.Second / 2) span11.Finish() span1.Finish() defer closer.Close() }
go run test.go
然后在去jaeger UI上刷新查看,会出现记录:
可以发现有分层,时间耗时也明显,接口先后调用也很清晰。
点击上面的 jager-test-demo 进去,可以看到下面的这种调用情况:
通过Grpc中间件使用
在单体程序中, 父子Span通过context关联, 而context是在内存中的, 显而易见这样的方法在垮应用的场景下是行不通的
垮应用通讯使用的方式通常是"序列化", 在jaeger-client-go库中也是通过类似的操作去传递信息, 它们叫:Tracer.Inject() 与 Tracer.Extract()
其中inject方法支持将span系列化成几种格式:
Binary: 二进制
TextMap: key=>value
HTTPHeaders: Http头, 其实也是key=>value
正好grpc支持传递metadata
也是string的key=>value形式, 所以我们就能通过metadata
实现在不同应用间传递Span了
这段代码在github上有人实现了: https://github.com/grpc-ecosystem/go-grpc-middleware
题外话:上面的库使用到了grpc的Interceptor, 但grpc不支持多个Interceptor, 所以当你又使用到了其他中间件(如grpc_retry)的话就能导致冲突. 同样也可以使用这个库grpc_middleware.ChainUnaryClient解决这个问题.
在grpc服务端的中间件代码如下(已省略错误处理)
package main import ( "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" "google.golang.org/grpc" ) func main() { jcfg := jaegercfg.Configuration{ Sampler: &jaegercfg.SamplerConfig{ Type: "const", Param: 1, }, ServiceName: "serviceName", } report := jaegercfg.ReporterConfig{ LogSpans: true, LocalAgentHostPort: "locahost:6831", } reporter, _ := report.NewReporter(serviceName, jaeger.NewNullMetrics(), jaeger.NullLogger) tracer, closer, _ = jcfg.NewTracer( jaegercfg.Reporter(reporter), ) server := grpc.NewServer(grpc.UnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(tracer)))) }
在grpc客户端的中间件代码如下
conn, err := grpc.Dial(addr, grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor( grpc_opentracing.WithTracer(tracer), )))
现在服务端和客户端之间的调用情况就能被jaeger收集到了.
在业务代码中使用
有时候只监控一个"api"是不够的,还需要监控到程序中的代码片段(如方法),可以这样封装一个方法
package main import ( "context" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ) func main() { //使用demo newCtx, finish := tracer.Start("DoSomeThing", ctx) err := DoSomeThing(newCtx) finish(tracer.SpanWithError(err)) if err != nil { // } } type SpanOption func(span opentracing.Span) func SpanWithError(err error) SpanOption { return func(span opentracing.Span) { if err != nil { ext.Error.Set(span, true) span.LogFields(tlog.String("event", "error"), tlog.String("msg", err.Error())) } } } // example: // SpanWithLog( // "event", "soft error", // "type", "cache timeout", // "waited.millis", 1500) func SpanWithLog(arg ...interface{}) SpanOption { return func(span opentracing.Span) { span.LogKV(arg...) } } func Start(tracer opentracing.Tracer, spanName string, ctx context.Context) (newCtx context.Context, finish func(...SpanOption)) { if ctx == nil { ctx = context.TODO() } span, newCtx := opentracing.StartSpanFromContextWithTracer(ctx, tracer, spanName, opentracing.Tag{Key: string(ext.Component), Value: "func"}, ) finish = func(ops ...SpanOption) { for _, o := range ops { o(span) } span.Finish() } return }
最后能得到一个像这样的结果
可以看到在服务的调用过程中各个span的时间,这个span可以是一个微服务之间的调用也可以是某个方法的调用。
点开某个span也能看到额外的log信息。
通过Gin中间件中使用
在我的项目中使用http服务作为网关提供给前端使用,那么这个http服务层就是root span而不用关心父span了,编写代码就要简单一些。
封装一个gin中间件就能实现
jaeger.go
package middleware import ( "github.com/gin-gonic/gin" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" ) const defaultComponentName = "net/http" const JaegerOpen = 1 const AppName = "gin-api" const JaegerHostPort = "122.51.156.172:6831" func OpenTracing(serviceName string) gin.HandlerFunc { return func(c *gin.Context) { if JaegerOpen == 1 { var parentSpan opentracing.Span jcfg := jaegercfg.Configuration{ Sampler: &jaegercfg.SamplerConfig{ Type: "const", Param: 1, }, ServiceName: AppName, } report := jaegercfg.ReporterConfig{ LogSpans: true, LocalAgentHostPort: JaegerHostPort, QueueSize: 1000, //发送的Spans个数大于了QueueSize, 多余QueueSize的Spans可能会被丢弃, 可以通过配置 QueueSize: } reporter, _ := report.NewReporter(serviceName, jaeger.NewNullMetrics(), jaeger.NullLogger) tracer, closer, _ := jcfg.NewTracer( jaegercfg.Reporter(reporter), ) defer closer.Close() spCtx, err := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header)) if err != nil { parentSpan = tracer.StartSpan(c.Request.URL.Path) defer parentSpan.Finish() } else { parentSpan = opentracing.StartSpan( c.Request.URL.Path, opentracing.ChildOf(spCtx), opentracing.Tag{Key: string(ext.Component), Value: "HTTP"}, ext.SpanKindRPCServer, ) defer parentSpan.Finish() } c.Set("Tracer", tracer) c.Set("ParentSpanContext", parentSpan.Context()) } c.Next() } }
router.go
Router.Use(middleware.OpenTracing("gin-api"))
如果需要向下层传递context则这样获取context
func Api(gtx *gin.Context) { ctx = gtx.Get("ctx").(context.Context) }
总结
使用trace会入侵部分代码,特别是追踪一个方法,但这是不可避免的。
甚至需要每个方法都需要添加上ctx, 关于这点有兴趣的朋友可以读一下这篇文章: Golang Context 是好的设计吗?
(原文找不到了, 将就看一下)
但其实并不是整个系统的服务都需要追踪,可只针对于重要或者有性能问题的地方进行追踪。
参考
https://www.jaegertracing.io/docs/1.18/
https://medium.com/jaegertracing/
https://wu-sheng.gitbooks.io/opentracing-io/content/
https://blog.csdn.net/liyunlong41/article/details/87932953
- 本文固定链接: https://phpmianshi.com/?id=369
- 转载请注明: admin 于 PHP面试网 发表
《本文》有 0 条评论