链路追踪:Jaeger
原创大约 6 分钟
技术实现对比
zipkin/sleuth | jaeger | skywalking | |
---|---|---|---|
OpenTracing兼容 | 是 | 是 | 是 |
客户端支持语言 | java, c#, go, php, python等 | java, c#, go, php, python等 | java, .net, nodejs, php, python |
存储 | es, mysql, cassandra, 内存 | es, kafka, cassandra, 内存 | es, h2, mysql, tidb, shardsphere |
传输协议支持 | http, MQ | udp/http | gRPC |
UI丰富程度 | 低 | 中 | 中 |
实现方式(代码侵入性) | 拦截请求,侵入 | 拦截请求,侵入 | 字节码注入,无侵入 |
扩展性 | 高 | 高 | 高 |
trace查询 | 支持 | 支持 | 支持 |
性能损失 | 中 | 中 | 低 |
集成&开发
环境和脚本
> go get -u github.com/uber/jaeger-client-go
> go get -u github.com/grpc-ecosystem/grpc-opentracing
# 将 grpc-opentracing 中的 /go/otgrpc 源码拷贝到本地项目中
# 部署Jaeger UI
> docker run --rm --name jaeger -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:1.57
# 运行docker后发现,请求始终无法显示在`Jaeger UI`中
# 将 jaegertracing/all-in-one:latest 改为 jaegertracing/all-in-one:1.57 ,却 pull 不下来
# 因此改为用二进制包的方式部署
> wget https://github.com/jaegertracing/jaeger/releases/download/v1.57.0/jaeger-1.57.0-linux-amd64.tar.gz
> tar -xvf jaeger-1.57.0-linux-amd64.tar.gz
> cd jaeger-1.57.0-linux-amd64
> ./jaeger-1.57.0-linux-amd64
通过jaeger-client发送单个span
package main
import (
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegerConfig "github.com/uber/jaeger-client-go/config"
)
func main() {
// 配置Jaeger trace
cfg := jaegerConfig.Configuration{
ServiceName: "go-microservice",
Sampler: &jaegerConfig.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegerConfig.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: "127.0.0.1:6831",
},
}
tracer, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
if err != nil {
panic(err)
}
defer closer.Close()
// 创建全局trace
opentracing.SetGlobalTracer(tracer)
// 创建一个新的span
span := tracer.StartSpan("user-srv")
time.Sleep(1 * time.Second)
span.Finish()
}
通过jaeger-client发送多个span
package main
import (
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegerConfig "github.com/uber/jaeger-client-go/config"
)
func main() {
// 配置Jaeger trace
cfg := jaegerConfig.Configuration{
ServiceName: "go-microservice",
Sampler: &jaegerConfig.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegerConfig.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: "127.0.0.1:6831",
},
}
tracer, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
if err != nil {
panic(err)
}
defer closer.Close()
// 创建全局trace
opentracing.SetGlobalTracer(tracer)
// 创建父span
parentSpan := tracer.StartSpan("grpc_func_main")
// 创建一个子span
span1 := tracer.StartSpan("grpc_func_1", opentracing.ChildOf(parentSpan.Context()))
time.Sleep(1 * time.Second)
span1.Finish()
// 再创建一个子span
span2 := tracer.StartSpan("grpc_func_2", opentracing.ChildOf(parentSpan.Context()))
time.Sleep(2 * time.Second)
span2.Finish()
parentSpan.Finish()
}
集成到GRPC
proto文件
syntax = "proto3";
option go_package="./;helloworld";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string reply = 1;
}
GRPC服务端源码
package main
import (
"context"
helloworld "go-project/jaeger/proto"
"google.golang.org/grpc"
"net"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
return &helloworld.HelloReply{
Reply: "hello " + in.Name,
}, nil
}
func main() {
// 1. 实例化一个server
g := grpc.NewServer()
// 2. 注册处理逻辑
helloworld.RegisterGreeterServer(g, &Server{})
// 3. 启动server
var listener, _ = net.Listen("tcp", ":1234")
_ = g.Serve(listener)
}
GRPC客户端源码
package main
import (
"context"
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegerConfig "github.com/uber/jaeger-client-go/config"
"go-project/jaeger/otgrpc"
helloworld "go-project/jaeger/proto"
"google.golang.org/grpc"
"time"
)
func main() {
// 配置Jaeger trace
cfg := jaegerConfig.Configuration{
ServiceName: "go-microservice",
Sampler: &jaegerConfig.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegerConfig.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: "127.0.0.1:6831",
},
}
trace, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
if err != nil {
panic(err)
}
defer closer.Close()
// 创建全局trace
opentracing.SetGlobalTracer(tracer)
//// 返回一个span
//span := opentracing.StartSpan("user-srv")
//time.Sleep(1 * time.Second)
//span.Finish()
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure(),
grpc.WithUnaryInterceptor(
otgrpc.OpenTracingClientInterceptor(
opentracing.GlobalTracer())))
if err != nil {
panic(err)
}
defer conn.Close()
client := helloworld.NewGreeterClient(conn)
r, err := client.SayHello(context.Background(), &helloworld.HelloRequest{
Name: "xiangwang",
})
if err != nil {
panic(err)
}
fmt.Println(r.Reply)
}
集成到Gin服务接口
1. 新增一个组件
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegerConfig "github.com/uber/jaeger-client-go/config"
"go.uber.org/zap"
"gomicroservice/userapi/global"
"time"
)
/*
Tracing 链路追踪器,专门用于各个接口的链路追踪
*/
func Trace() gin.HandlerFunc {
return func(ctx *gin.Context) {
zap.S().Infof("开启链路追踪......")
// 配置trace
cfg := jaegerConfig.Configuration{
ServiceName: global.ServerConfig.Name,
Sampler: &jaegerConfig.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegerConfig.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: fmt.Sprintf("%s:%d",
global.ServerConfig.TracerInfo.Host, global.ServerConfig.TracerInfo.Port),
},
}
trace, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
if err != nil {
zap.S().Infof("生成链路追踪失败:%s", err.Error())
}
// 生成全局追踪器
opentracing.SetGlobalTracer(trace)
defer closer.Close()
// 生成起始链路
startSpan := trace.StartSpan(ctx.Request.URL.Path)
defer startSpan.Finish()
// 设置起始调用链
ctx.Set(global.ServerConfig.TracerInfo.Trace, trace)
// 设置父Span
ctx.Set(global.ServerConfig.TracerInfo.ParentSpan, startSpan)
ctx.Next()
}
}
2. 修改服务初始化连接
......
// 负载均衡连接用户grpc服务
dialString := fmt.Sprintf("consul://%s:%d/%s?wait=10s",
global.ServerConfig.ConsulInfo.Host,
global.ServerConfig.ConsulInfo.Port,
global.ServerConfig.UserSrvInfo.Name)
conn, err := grpc.Dial(dialString,
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
// 在轮询时加入链路追踪,但没有父Span,需要修改OpenTracingClientInterceptor源码
grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
)
......
3. 拷贝并修改otgrpc源码
将grpc-opentracing拷贝到微服务项目的
utils
包。修改其中的
OpenTracingClientInterceptor()
方法。
......
var err error
var parentCtx opentracing.SpanContext
if parent := opentracing.SpanFromContext(ctx); parent != nil {
parentCtx = parent.Context()
}
// 修改源码 - BEGIN:用gin的context替换掉原本的context
ginContext := ctx.Value(global.ServerConfig.TracerInfo.GinContext)
switch ginContext.(type) {
case *gin.Context:
// 先替换原有的tracer
if iTracer, ok := ginContext.(*gin.Context).Get(global.ServerConfig.TracerInfo.Trace); ok {
tracer = iTracer.(opentracing.Tracer)
}
// 再替换掉原来的parentCtx
if iParentSpan, ok := ginContext.(*gin.Context).Get(global.ServerConfig.TracerInfo.ParentSpan); ok {
parentCtx = iParentSpan.(*jaegerClient.Span).Context()
}
}
// 修改源码 - END
if otgrpcOpts.inclusionFunc != nil &&
!otgrpcOpts.inclusionFunc(parentCtx, method, req, resp) {
return invoker(ctx, method, req, resp, cc, opts...)
}
......
4. 修改服务接口代码
用context.WithValue(context.Background(), global.ServerConfig.TracerInfo.GinContext, ctx)
替换掉每个需要链路追踪接口中原先的context.Background()
参数。
......
// 调用接口
/*
注意:使用jaeger-client,在每个接口中都要用 context.WithValue(context.Background(), global.ServerConfig.TracerInfo.GinContext, ctx) 替换掉 context.Background()
这里原来是:users, err2 := global.UserSrvClient.List(context.Background(), ...)
*/
users, err2 := global.UserSrvClient.List(context.WithValue(context.Background(), global.ServerConfig.TracerInfo.GinContext, ctx),
&proto.Page{
Page: uint32(pageInt),
Size: uint32(sizeInt),
})
if err2 != nil {
zap.S().Infof("调用【获取用户列表服务】失败:%s", err2.Error())
TranslateGrpcErrorCode2HttpCode(err2, ctx)
return
}
......
5. 对路由进行访问拦截
......
// 给所有用户路由都加入链路追踪
userRouter := router.Group("/users").Use(middleware.Tracing())
......
6. 修改配置
- 在
config
包中增加链路追踪的配置表单。
......
type TracerConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Trace string `mapstructure:"trace" json:"trace"`
ParentSpan string `mapstructure:"parentSpan" json:"parentSpan"`
GinContext string `mapstructure:"ginContext" json:"ginContext"`
}
type ServerConfig struct {
Env string `mapstructure:"env" json:"env"`
Name string `mapstructure:"name" json:"name"`
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
UserSrvInfo UserSrv `mapstructure:"usersrv" json:"usersrv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
AliSmsInfo AliSmsConfig `mapstructure:"sms" json:"sms"`
RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
// 这里别忘了加
TracerInfo TracerConfig `mapstructure:"tracer" json:"tracer"`
LoggerInfo LoggerConfig `mapstructure:"logger" json:"logger"`
}
......
在Nacos配置文件中增加链路追踪配置。
......
"tracer": {
"host": "127.0.0.1",
"port": 6831,
"trace": "trace",
"parentSpan": "parentSpan",
"ginContext": "ginContext"
},
......
以上六步,在每个其他微服务中都如法炮制。
Gin与GRPC链路串联
1. 拷贝并修改utils/otgrpc
将Gin服务接口中的utils/otgrpc
包拷贝到GRPC服务实现中。
注意将引用路径修改为GRPC项目下的包,否则GRPC服务无法启动。
2. 修改GRPC服务启动代码
......
// 初始化jaeger
cfg := jaegerConfig.Configuration{
ServiceName: global.ServerConfig.Name,
Sampler: &jaegerConfig.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegerConfig.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
LocalAgentHostPort: fmt.Sprintf("%s:%d",
global.ServerConfig.TracerInfo.Host, global.ServerConfig.TracerInfo.Port),
},
}
tracer, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
if err != nil {
zap.S().Errorf("初始化jaeger失败:" + err.Error())
}
opentracing.SetGlobalTracer(tracer)
// 将 grpc.UnaryInterceptor(otgrpc.OpenTracingServerInterceptor(tracer)) 拷贝到 NewServer() 中
server := grpc.NewServer(grpc.UnaryInterceptor(otgrpc.OpenTracingServerInterceptor(tracer)))
......
// 8. 优雅退出
quitSignal := make(chan os.Signal)
signal.Notify(quitSignal, syscall.SIGINT, syscall.SIGTERM)
<-quitSignal
// 服务注销时也关闭jaeger
defer func(closer io.Closer) {
err := closer.Close()
if err != nil {
zap.S().Errorf("注销时关闭Jaeger失败:" + err.Error())
}
}(closer)
......
3. 修改GRPC服务实现代码
package handler
......
// List 获取用户的列表
func (server *UserServiceServer) List(ctx context.Context, page *proto.Page) (*proto.UserList, error) {
// 拿到Jaeger的parentSpan
parentSpan := opentracing.SpanFromContext(ctx)
// 实例化
var users []model.User
var userList = &proto.UserList{}
// 拿到parentSpan之后,就可以对每一次操作做链路追踪
// 开始链路追踪
querySpan := opentracing.GlobalTracer().StartSpan("query_user_list", opentracing.ChildOf(parentSpan.Context()))
// 获取全局的数据库连接
result := global.DB.Find(&users)
if result.Error != nil {
return nil, status.Errorf(codes.Unknown, "查询用户列表失败")
}
// 结束链路追踪
querySpan.Finish()
// 查询到的用户数量
userList.Total = uint32(result.RowsAffected)
// 开始链路追踪
pageSpan := opentracing.GlobalTracer().StartSpan("page_user_list", opentracing.ChildOf(parentSpan.Context()))
// 分页
global.DB.Scopes(Paginate(int(page.Page), int(page.Size))).Find(&users)
// 填充返回值
for _, user := range users {
userInfo := Model2Response(user)
userList.Data = append(userList.Data, &userInfo)
}
// 结束链路追踪
pageSpan.Finish()
return userList, nil
}
......
感谢支持
更多内容,请移步《超级个体》。