15-分布式链路追踪

一 什么是链路追踪?

分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

链路跟踪主要功能:

  • 故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
  • 链路性能可视化:各个阶段链路耗时、服务依赖关系可以通过可视化界面展现出来。
  • 链路分析:通过分析链路耗时、服务依赖关系可以得到用户的行为路径,汇总分析应用在很多业务场景。

二 链路追踪基本原理

链路追踪系统(可能)最早是由Goggle公开发布的一篇论文

《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》

被大家广泛熟悉,所以各位技术大牛们如果有黑武器不要藏起来赶紧去发表论文吧。

在这篇著名的论文中主要讲述了Dapper链路追踪系统的基本原理和关键技术点。接下来挑几个重点的技术点详细给大家介绍一下。

2.1 Trace

Trace的含义比较直观,就是链路,指一个请求经过所有服务的路径,可以用下面树状的图形表示。

image-20220528232118828

图中一条完整的链路是:chrome -> 服务A -> 服务B -> 服务C -> 服务D -> 服务E -> 服务C -> 服务A -> chrome。服务间经过的局部链路构成了一条完整的链路,其中每一条局部链路都用一个全局唯一的traceid来标识。

2.2 Span

在上图中可以看出来请求经过了服务A,同时服务A又调用了服务B和服务C,但是先调的服务B还是服务C呢?从图中很难看出来,只有通过查看源码才知道顺序。

为了表达这种父子关系引入了Span的概念。

同一层级parent id相同,span id不同,span id从小到大表示请求的顺序,从下图中可以很明显看出服务A是先调了服务B然后再调用了C。

上下层级代表调用关系,如下图服务C的span id为2,服务D的parent id为2,这就表示服务C和服务D形成了父子关系,很明显是服务C调用了服务D。

image-20220528232144023

总结:通过事先在日志中埋点,找出相同traceId的日志,再加上parent id和span id就可以将一条完整的请求调用链串联起来。

2.3 Annotations

Dapper中还定义了annotation的概念,用于用户自定义事件,用来辅助定位问题。

通**常**包含四个注解信息

cs:Client Start,表示客户端发起请求;

sr:ServerReceived,表示服务端收到请求;

ss:Server Send,表示服务端完成处理,并将结果发送给客户端;

cr:ClientReceived,表示客户端获取到服务端返回信息;

image-20220528232213014

上图中描述了一次请求和响应的过程,四个点也就是对应四个Annotation事件。

如下面的图表示从客户端调用服务端的一次完整过程。如果要计算一次调用的耗时,只需要将客户端接收的时间点减去客户端开始的时间点,也就是图中时间线上的T4 - T1。如果要计算客户端发送网络耗时,也就是图中时间线上的T2 - T1,其他类似可计算。

image-20220528232232400

2.4 带内数据与带外数据

链路信息的还原依赖于带内带外两种数据。

带外数据是各个节点产生的事件,如cs,ss,这些数据可以由节点独立生成,并且需要集中上报到存储端。通过带外数据,可以在存储端分析更多链路的细节。

带内数据如traceid,spanid,parentid,用来标识trace,span,以及span在一个trace中的位置,这些数据需要从链路的起点一直传递到终点。通过带内数据的传递,可以将一个链路的所有过程串起来。

2.5 采样

由于每一个请求都会生成一个链路,为了减少性能消耗,避免存储资源的浪费,dapper并不会上报所有的span数据,而是使用采样的方式。举个例子,每秒有1000个请求访问系统,如果设置采样率为1/1000,那么只会上报一个请求到存储端。

image-20220528232300747

通过采集端自适应地调整采样率,控制span上报的数量,可以在发现性能瓶颈的同时,有效减少性能损耗。

2.6 存储

image-20220528232319046

链路中的span数据经过收集和上报后会集中存储在一个地方,Dapper使用了BigTable数据仓库,常用的存储还有ElasticSearch, HBase, In-memory DB等。

三 常用链路追踪系统

Google Dapper论文发出来之后,很多公司基于链路追踪的基本原理给出了各自的解决方案,如Twitter的Zipkin,Uber的Jaeger,pinpoint,Apache开源的skywalking,还有国产如阿里的鹰眼,美团的Mtrace,滴滴Trace,新浪的Watchman,京东的Hydra,不过国内的这些基本都没有开源。

为了便于各系统间能彼此兼容互通,OpenTracing组织制定了一系列标准,旨在让各系统提供统一的接口。

下面对比一下几个开源组件,方便日后大家做技术选型。

image-20220528232427818

附各大开源组件的地址:

zipkin :Twitter团队开源,java中用的多
文档地址:https://zipkin.io/
github地址:https://github.com/openzipkin/zipkin

Jaeger :Jaeger由Uber创建,并用Go语言编写,go中用的多
文档地址:https://www.jaegertracing.io/
github地址:https://github.com/jaegertracing/jaeger

Pinpoint :由韩国团队naver团队开源,针对大规模分布式系统用链路监控,使用java写的工具。灵感来自短小精悍,帮助分析系统的总体结构和内部组件如何被调用在分布式应用提供了一个很好的解决方案
文档地址:https://github.com/pinpoint-apm/pinpoint
github地址:https://github.com/pinpoint-apm/pinpoint

SkyWalking:2015年由个人吴晟(华为开发者)开源 , 2017年加入Apache孵化器
文档地址:http://skywalking.apache.org/
github地址:https://github.com/apache/skywalking

四 Jaeger使用

4.1 安装配置

// 参照地址:https://github.com/jaegertracing/jaeger/tree/main/examples/hotrod

// docker 安装
docker run -di --rm --name jaeger -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest

// 浏览器访问
http://10.0.0.102:16686/

4.2 架构

//https://www.jaegertracing.io/docs/1.21/architecture/
image-20220528234447228

4.3 Jaeger组成

Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。

Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。

Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。

Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。

Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。

分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示

4.4 opentracing标准

4.4.1 介绍

OpenTracing 于 2016 年 10 月加入 CNCF 基金会,是继 Kubernetes 和 Prometheus 之后,第三个加入 CNCF 的开源项目。
它是一个中立的(厂商无关、平台无关)分布式追踪的 API 规范,提供统一接口,可方便开发者在自己的服务中集成一种或多种分布式追踪的实现。

4.4.2 Trace 简介

一个 Trace 代表一个事务、请求或是流程在分布式系统中的执行过程。OpenTracing 中的一条 Trace 被认为是一个由多个 Span 组成的有向无环图( DAG 图),一个 Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 一般会有一个名称,一条 Trace 中 Span 是首尾连接的

image-20220528232118828

4.4.3 Span 简介

Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 之间通过嵌套或者顺序排列建立逻辑因果关系。Span有父子关系

每个 Span 中可以包含以下的信息:

操作名称:例如访问的具体 RPC 服务,访问的 URL 地址等;

起始时间;

结束时间;

Span Tag:一组键值对构成的 Span 标签集合,其中键必须为字符串类型,值可以是字符串、bool 值或者数字;

Span Log:一组 Span 的日志集合;

SpanContext:span 的全局上下文信息;

References:Span 之间的引用关系,下面详细说明 Span 之间的引用关系;

image-20220528232144023

4.4.4 SpanContext

SpanContext是Span的上下文对象,因为一条Trace由多个Span组成,SpanContext会记录Trace和Span的id,及父Span的信息等

一个Span可以与一个或者多个SpanContexts存在因果关系。OpenTracing目前定义了两种关系:ChildOf(父子--同步调用关系中) 和 FollowsFrom(跟随--异步调用关系中)。这两种关系明确的给出了两个父子关系的Span的因果模型

五 go 操作Jaeger

5.1 快速使用

// 地址: https://github.com/jaegertracing/jaeger-client-go
// demo参考:https://github.com/jaegertracing/jaeger-client-go/blob/master/config/example_test.go
package main

import (
	"fmt"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"time"
)
func main() {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()

	// 第三步:通过Trace启动Span,输入Span名字
	span:=tracer.StartSpan("span_root")
	// 第四步:模拟调用的延迟
	time.Sleep(2*time.Second)
	span.Finish()

}

image-20220529004313633

5.2 一个Trace下多个Span

// 没有父span,该方式生成的链路是分开的
package main

import (
	"fmt"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"time"
)
func main() {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()

	// 第三步:通过Trace启动Span,输入Span名字
	span:=tracer.StartSpan("funcA")
	// 第四步:模拟调用的延迟
	time.Sleep(2*time.Second)
	span.Finish()

	span2:=tracer.StartSpan("funcB")
	time.Sleep(1*time.Second)
	span2.Finish()

}

5.3 多级span

package main

import (
	"fmt"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"time"
)
func main() {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	defer closer.Close()

	// 第三步:通过Trace启动Span,输入Span名字
	spanRoot:=tracer.StartSpan("span-root")
	// 生成span1时,指定是来自于哪个父span
	span1:=tracer.StartSpan("funcA",opentracing.ChildOf(spanRoot.Context()))
	funcA()
	span1.Finish()

	time.Sleep(time.Second) //  加上这句,再看图
	// 生成span2时,指定是来自于哪个父span
	span2:=tracer.StartSpan("funcB",opentracing.ChildOf(spanRoot.Context()))
	funcB()
	span2.Finish()
}

func funcA()  {
	time.Sleep(2*time.Second)
}
func funcB()  {
	time.Sleep(1*time.Second)
}

image-20220529005310850 image-20220529005454788 image-20220529005713947

5.4 gRPC集成

5.4.1 通过拦截器自行实现

5.4.2 第三方实现

// 地址:https://github.com/grpc-ecosystem/grpc-opentracing/

// 它有python,java,go的实现,我们只要go即可
//https://github.com/grpc-ecosystem/grpc-opentracing/tree/master/go/otgrpc

// 下载下来,放到咱们项目中即可,不要忘了放入咱们项目中

客户端

package main

import (
   "context"
   "fmt"
   _ "github.com/mbobakov/grpc-consul-resolver" // It's important
   "github.com/opentracing/opentracing-go"
   "github.com/uber/jaeger-client-go"
   "github.com/uber/jaeger-client-go/config"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials/insecure"
   "grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
   "grpc_proto_demo/jeager_demo/otgrpc"
   "io"
   "log"

)

func getTracer()(opentracing.Tracer, io.Closer)   {
   // 第一步:初始化配置信息
   cfg := &config.Configuration{
      // 采样率暂配置,设置为1,全部采样
      // 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
      // 如:rateLimiting:每秒spans数
      Sampler: &config.SamplerConfig{
         Type:  "const",
         Param: 1,
      },
      Reporter: &config.ReporterConfig{
         LogSpans:           true,              // 是否打印日志
         LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
      },
      ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
   }

   // 第二步:通过配置,生成链路Trace
   // 传入服务名,和日志输出位置
   //cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
   tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
   if err != nil {
      panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
   }

   return tracer,closer

}
func main() {
   // 第一步:获取Tracer
   tracer,closer:=getTracer()
   defer closer.Close()
   // 第二步:把tracer设置为全局,以后使用opentracing.GlobalTracer()开启span即可或opentracing.StartSpan()开启spana
   opentracing.SetGlobalTracer(tracer)


   conn, err := grpc.Dial(
      "consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
      //grpc.WithInsecure(),
      grpc.WithTransportCredentials(insecure.NewCredentials()),
      grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
      // 加入一个拦截器,传入OpenTracingClientInterceptor的配置
      // opentracing.GlobalTracer()等同于上面的tracer
      grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
   )
   if err != nil {
      log.Fatal(err)
   }
   defer conn.Close()
   client := proto.NewGreeterClient(conn)
   // 测试默认值
   resp,err:=client.SayHello(context.Background(),&proto.HelloRequest{
      Name: "lqz",
      Age: 19,
   })
   if err!=nil {
      panic(err)
   }
   fmt.Println(resp.Reply)
}

proto

syntax = "proto3";
option go_package = ".;proto";

service Greeter{
  rpc SayHello (HelloRequest) returns (HelloResponse) {}

}

// 类似于go的结构体,可以定义属性
message HelloRequest {
  string name = 1; // 1 是编号,不是值
  int32 age = 2;

}
// 定义一个响应的类型
message HelloResponse {
  string reply =1;
}

生成go文件

protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto

服务端

package main

import (
	"context"
	"fmt"
	consulapi "github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"
	"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/proto"
	"grpc_proto_demo/grpc_gin_load_balance/grpc_srv/server/utils"
	"net"
)

type GreeterServer struct {
}

func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
	// 接收客户端发送过来的数据,打印出来
	fmt.Println("客户端传入的名字是:", in.Name)
	fmt.Println("客户端传入的年龄是:", in.Age)
	return &proto.HelloResponse{
		Reply: "gin-调用grpc,grpc给的回复",
	}, nil
}

// 服务端代码
func main() {

	// 定义段端口
	port,_:=utils.GetCanUsePort()
	// 第一步:new一个server
	g := grpc.NewServer()
	// 第二步:生成一个结构体对象
	s := GreeterServer{}
	// 第三步: 把s注册到g对象中
	proto.RegisterGreeterServer(g, &s)
	// 第四步:启动服务,监听端口
	lis, error := net.Listen("tcp", fmt.Sprintf("192.168.31.226:%d",port))
	if error != nil {
		panic("启动服务异常")
	}

	//******** 注册grpc服务和设置健康检查********
	// 1 设置健康检查
	//health.NewServer()具体实现grpc已经帮我们写好了
	grpc_health_v1.RegisterHealthServer(g,health.NewServer())
	// 2 注册grpc服务
	grpcId:=utils.GetUUId()
	RegisterConsul("192.168.31.226",port,"grpc_test",grpcId,[]string{"grpc","lqz"})

	g.Serve(lis)

}

func RegisterConsul(localIP string, localPort int, name string,id string, tags []string) error {
	// 创建连接consul服务配置
	config := consulapi.DefaultConfig()
	config.Address = "10.0.0.102:8500"
	client, err := consulapi.NewClient(config)
	if err != nil {
		fmt.Println("consul client error : ", err)
	}

	// 创建注册到consul的服务到
	registration := new(consulapi.AgentServiceRegistration)
	registration.ID = id
	registration.Name = name //根据这个名称来找这个服务
	registration.Port = localPort
	//registration.Tags = []string{"lqz", "gin_web"} //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Tags = tags //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Address = localIP

	// 增加consul健康检查回调函数
	check := new(consulapi.AgentServiceCheck)
	check.GRPC = fmt.Sprintf("192.168.31.226:%d",localPort)// 健康检查地址只需要写grpc服务地址端口即可,会自动检查
	check.Timeout = "5s"                         //超时
	check.Interval = "5s"                        //健康检查频率
	check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
	registration.Check = check
	// 注册服务到consul
	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		return err
	}
	return nil

}
package utils

import (
	"fmt"
	uuid "github.com/satori/go.uuid"
	"net"
)

func GetCanUsePort() (int, error) {
	// 解析地址
	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
	if err != nil {
		return 0, nil
	}
	// 利用 ListenTCP 方法的如下特性
	// 如果 addr 的端口字段为0,函数将选择一个当前可用的端口
	listen, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return 0, nil
	}
	// 关闭资源
	defer listen.Close()
	// 为了拿到具体的端口值,我们转换成 *net.TCPAddr类型获取其Port
	return listen.Addr().(*net.TCPAddr).Port, nil
}

func GetUUId() string {
	// 创建
	u1 := uuid.NewV4()
	//fmt.Println(u1.String())
	return u1.String()
}
func ParserUUID(u string) (*uuid.UUID, error) {
	// 解析
	//u2, err := uuid.FromString("f5394eef-e576-4709-9e4b-a7c231bd34a4")
	u2, err := uuid.FromString(u)
	if err != nil {
		fmt.Printf("Something gone wrong: %s", err)
		return nil, err
	}
	return &u2, nil
}

5.5 Gin中集成

// 目录结构
gin_grpc_jeager_demo
	-gin_web
		-middle
			-tracing.go
  	-main.go
	-otgrpc
	-grpc_server
		-main.go
	proto
		-hello.proto

tracing.go

package middle

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"io"
)

func getTracer()(opentracing.Tracer, io.Closer)   {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}

	return tracer,closer

}

func TrancerMiddleware(c *gin.Context)  {
	// 获取到tracer和closer
	tracer,closer:=getTracer()
	// 关闭closer
	defer closer.Close()
	// 使用当前url地址创建一个span
	startSpan:=tracer.StartSpan(c.Request.URL.Path)
	// 关闭span
	defer startSpan.Finish()
	// 放入ctx中
	c.Set("tracer",tracer)
	c.Set("startSpan",startSpan)
	// 继续往下执行
	c.Next()
	
}

gin_web/main.go

package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"grpc_proto_demo/gin_grpc_jeager_demo/gin_web/middle"
	"grpc_proto_demo/gin_grpc_jeager_demo/gin_web/otgrpc"
	"grpc_proto_demo/gin_grpc_jeager_demo/proto"
	"time"
)

func main() {
	r:=gin.Default()
	r.Use(middle.TrancerMiddleware)
	r.GET("/index", func(c *gin.Context) {
		time.Sleep(1*time.Second)
		// 第一步:连接服务端
		// 从ctx中取出tracer,放入到拦截器中
		tracer,_:=c.Get("tracer")
		tracerPath:=tracer.(opentracing.Tracer)
		conn, err := grpc.Dial("127.0.0.1:50052",
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracerPath)))
		if err != nil {
			fmt.Println(err)
			c.JSON(200,"连接grpc服务异常")
		}
		//defer 关闭
		defer conn.Close()
		// 第二步:创建客户端调用
		client := proto.NewGreeterClient(conn)
		// 因为每次请求都是一个新的span,取出父span,放入ctx中
		startSpan,_:=c.Get("startSpan")
		parentSpan:=startSpan.(opentracing.Span)
		// 这样放进去,在otgrpc中源码中通过opentracing.SpanFromContext(ctx)取出开,取到父span
		ctx:=opentracing.ContextWithSpan(context.Background(),parentSpan)
		resp,err:=client.SayHello(ctx,&proto.HelloRequest{
			Name: "lqz",
			Age: 19,
		})
		if err != nil {
			c.JSON(200,"服务器错误")
		}
		c.JSON(200,resp.Reply)

	})

	r.Run()
}

grpc_server/main.co

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"grpc_proto_demo/gin_grpc_jeager_demo/proto"
	"net"
)

type GreeterServer struct {
}

func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
	// 接收客户端发送过来的数据,打印出来
	fmt.Println("客户端传入的名字是:", in.Name)
	fmt.Println("客户端传入的年龄是:", in.Age)
	return &proto.HelloResponse{
		Reply: "gin-调用grpc,grpc给的回复",
	}, nil
}

// 服务端代码
func main() {
	// 第一步:new一个server
	g := grpc.NewServer()
	// 第二步:生成一个结构体对象
	s := GreeterServer{}
	// 第三步: 把s注册到g对象中
	proto.RegisterGreeterServer(g, &s)
	// 第四步:启动服务,监听端口
	lis, error := net.Listen("tcp", "127.0.0.1:50052")
	if error != nil {
		panic("启动服务异常")
	}
	g.Serve(lis)

}

proto/hello.proto

syntax = "proto3";
option go_package = ".;proto";

service Greeter{
  rpc SayHello (HelloRequest) returns (HelloResponse) {}

}

// 类似于go的结构体,可以定义属性
message HelloRequest {
  string name = 1; // 1 是编号,不是值
  int32 age = 2;

}
// 定义一个响应的类型
message HelloResponse {
  string reply =1;
}

生成go文件

protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto

5.6 gin-grpc-consul-负载均衡

// 目录结构
gin_grpc_jeager_consul_balance_demo
  gin_web
    -middle
      -tracing.go
    -main.go
  -otgrpc
  -grpc_server
    -utils
      -utils.go
    -main.go
  -proto
    hello.proto

gin_web/middle/tracing.go

package middle

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"io"
)

func getTracer()(opentracing.Tracer, io.Closer)   {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}

	return tracer,closer

}

func TrancerMiddleware(c *gin.Context)  {
	// 获取到tracer和closer
	tracer,closer:=getTracer()
	// 关闭closer
	defer closer.Close()
	// 使用当前url地址创建一个span
	startSpan:=tracer.StartSpan(c.Request.URL.Path)
	// 关闭span
	defer startSpan.Finish()
	// 放入ctx中
	c.Set("tracer",tracer)
	c.Set("startSpan",startSpan)
	// 继续往下执行
	c.Next()
	
}

gin_web/main.go

package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	_ "github.com/mbobakov/grpc-consul-resolver" // It's important
	"github.com/opentracing/opentracing-go"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/proto"
	"grpc_proto_demo/gin_grpc_jeager_demo/gin_web/middle"
	"grpc_proto_demo/gin_grpc_jeager_demo/gin_web/otgrpc"
	"time"
)

func main() {
	r:=gin.Default()
	r.Use(middle.TrancerMiddleware)
	r.GET("/index", func(c *gin.Context) {
		time.Sleep(1*time.Second)
		// 第一步:连接服务端


		// 从ctx中取出tracer,放入到拦截器中
		tracer,_:=c.Get("tracer")
		tracerPath:=tracer.(opentracing.Tracer)
		// 负载均衡
		conn, err := grpc.Dial(
			"consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
			grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracerPath)),
		)

		if err != nil {
			fmt.Println(err)
			c.JSON(200,"连接grpc服务异常")
		}
		//defer 关闭
		defer conn.Close()
		// 第二步:创建客户端调用
		client := proto.NewGreeterClient(conn)
		// 因为每次请求都是一个新的span,取出父span,放入ctx中
		startSpan,_:=c.Get("startSpan")
		parentSpan:=startSpan.(opentracing.Span)
		// 这样放进去,在otgrpc中源码中通过opentracing.SpanFromContext(ctx)取出开,取到父span
		ctx:=opentracing.ContextWithSpan(context.Background(),parentSpan)
		resp,err:=client.SayHello(ctx,&proto.HelloRequest{
			Name: "lqz",
			Age: 19,
		})
		if err != nil {
			c.JSON(200,"服务器错误")
		}
		c.JSON(200,resp.Reply)

	})
	r.GET("/index2", func(c *gin.Context) {
		time.Sleep(1*time.Second)
		// 第一步:连接服务端


		// 从ctx中取出tracer,放入到拦截器中
		tracer,_:=c.Get("tracer")
		tracerPath:=tracer.(opentracing.Tracer)
		// 负载均衡
		conn, err := grpc.Dial(
			"consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
			grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracerPath)),
		)

		if err != nil {
			fmt.Println(err)
			c.JSON(200,"连接grpc服务异常")
		}
		//defer 关闭
		defer conn.Close()
		// 第二步:创建客户端调用
		client := proto.NewGreeterClient(conn)
		// 因为每次请求都是一个新的span,取出父span,放入ctx中
		startSpan,_:=c.Get("startSpan")
		parentSpan:=startSpan.(opentracing.Span)
		// 这样放进去,在otgrpc中源码中通过opentracing.SpanFromContext(ctx)取出开,取到父span
		ctx:=opentracing.ContextWithSpan(context.Background(),parentSpan)
		resp,err:=client.SayHello(ctx,&proto.HelloRequest{
			Name: "lqz",
			Age: 19,
		})
		if err != nil {
			c.JSON(200,"服务器错误")
		}
		c.JSON(200,resp.Reply)

	})
	r.GET("/index3", func(c *gin.Context) {
		time.Sleep(1*time.Second)
		// 第一步:连接服务端

		// 从ctx中取出tracer,放入到拦截器中
		tracer,_:=c.Get("tracer")
		tracerPath:=tracer.(opentracing.Tracer)
		// 负载均衡
		conn, err := grpc.Dial(
			"consul://10.0.0.102:8500/grpc_test?wait=14s&tag=lqz",
			grpc.WithTransportCredentials(insecure.NewCredentials()),
			grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
			grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracerPath)),
		)

		if err != nil {
			fmt.Println(err)
			c.JSON(200,"连接grpc服务异常")
		}
		//defer 关闭
		defer conn.Close()
		// 第二步:创建客户端调用
		client := proto.NewGreeterClient(conn)
		// 因为每次请求都是一个新的span,取出父span,放入ctx中
		startSpan,_:=c.Get("startSpan")
		parentSpan:=startSpan.(opentracing.Span)
		// 这样放进去,在otgrpc中源码中通过opentracing.SpanFromContext(ctx)取出开,取到父span
		ctx:=opentracing.ContextWithSpan(context.Background(),parentSpan)
		resp,err:=client.SayHello(ctx,&proto.HelloRequest{
			Name: "lqz",
			Age: 19,
		})
		if err != nil {
			c.JSON(200,"服务器错误")
		}
		c.JSON(200,resp.Reply)

	})
	r.Run()
}

grpc_server/main.co

package main

import (
	"context"
	"fmt"
	consulapi "github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/grpc_server/utils"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/proto"
	"net"
)

type GreeterServer struct {
}

func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {
	// 接收客户端发送过来的数据,打印出来
	fmt.Println("客户端传入的名字是:", in.Name)
	fmt.Println("客户端传入的年龄是:", in.Age)
	return &proto.HelloResponse{
		Reply: "gin-调用grpc,grpc给的回复",
	}, nil
}

// 服务端代码
func main() {
	// 定义段端口
	port,_:=utils.GetCanUsePort()
	// 第一步:new一个server
	g := grpc.NewServer()
	// 第二步:生成一个结构体对象
	s := GreeterServer{}
	// 第三步: 把s注册到g对象中
	proto.RegisterGreeterServer(g, &s)
	// 第四步:启动服务,监听端口
	lis, error := net.Listen("tcp", fmt.Sprintf("192.168.31.226:%d",port))
	if error != nil {
		panic("启动服务异常")
	}

	//******** 注册grpc服务和设置健康检查********
	// 1 设置健康检查
	//health.NewServer()具体实现grpc已经帮我们写好了
	grpc_health_v1.RegisterHealthServer(g,health.NewServer())
	// 2 注册grpc服务
	grpcId:=utils.GetUUId()
	RegisterConsul("192.168.31.226",port,"grpc_test",grpcId,[]string{"grpc","lqz"})


	g.Serve(lis)

}


func RegisterConsul(localIP string, localPort int, name string,id string, tags []string) error {
	// 创建连接consul服务配置
	config := consulapi.DefaultConfig()
	config.Address = "10.0.0.102:8500"
	client, err := consulapi.NewClient(config)
	if err != nil {
		fmt.Println("consul gin_web error : ", err)
	}

	// 创建注册到consul的服务到
	registration := new(consulapi.AgentServiceRegistration)
	registration.ID = id
	registration.Name = name //根据这个名称来找这个服务
	registration.Port = localPort
	//registration.Tags = []string{"lqz", "gin_web"} //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Tags = tags //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Address = localIP

	// 增加consul健康检查回调函数
	check := new(consulapi.AgentServiceCheck)
	check.GRPC = fmt.Sprintf("192.168.31.226:%d",localPort)// 健康检查地址只需要写grpc服务地址端口即可,会自动检查
	check.Timeout = "5s"                         //超时
	check.Interval = "5s"                        //健康检查频率
	check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
	registration.Check = check
	// 注册服务到consul
	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		return err
	}
	return nil

}

grpc_server/utils/utils.go

package utils

import (
	"fmt"
	uuid "github.com/satori/go.uuid"
	"net"
)

func GetCanUsePort() (int, error) {
	// 解析地址
	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
	if err != nil {
		return 0, nil
	}
	// 利用 ListenTCP 方法的如下特性
	// 如果 addr 的端口字段为0,函数将选择一个当前可用的端口
	listen, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return 0, nil
	}
	// 关闭资源
	defer listen.Close()
	// 为了拿到具体的端口值,我们转换成 *net.TCPAddr类型获取其Port
	return listen.Addr().(*net.TCPAddr).Port, nil
}

func GetUUId() string {
	// 创建
	u1 := uuid.NewV4()
	//fmt.Println(u1.String())
	return u1.String()
}
func ParserUUID(u string) (*uuid.UUID, error) {
	// 解析
	//u2, err := uuid.FromString("f5394eef-e576-4709-9e4b-a7c231bd34a4")
	u2, err := uuid.FromString(u)
	if err != nil {
		fmt.Printf("Something gone wrong: %s", err)
		return nil, err
	}
	return &u2, nil
}

proto/hello.proto

syntax = "proto3";
option go_package = ".;proto";

service Greeter{
  rpc SayHello (HelloRequest) returns (HelloResponse) {}

}

// 类似于go的结构体,可以定义属性
message HelloRequest {
  string name = 1; // 1 是编号,不是值
  int32 age = 2;

}
// 定义一个响应的类型
message HelloResponse {
  string reply =1;
}

生成go文件

protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto

5.7 在rpc的服务中获取父span

// 启动服务时使用拦截器
// 第一步:new一个server,增加jeager的服务端拦截器
//定义一个新的trace,以后从service中就可以取出父的span
tracer,closer:=getTracer()
defer closer.Close()
// 设置成全局tracer
opentracing.SetGlobalTracer(tracer)
g := grpc.NewServer(grpc.UnaryInterceptor(otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer())))
	


// 在service中使用
// **********子span使用开始**********
// 取出父span
parentSpan:=opentracing.SpanFromContext(ctx)
// 取出全局tracer
tracer:=opentracing.GlobalTracer()
span2:=tracer.StartSpan("funcB",opentracing.ChildOf(parentSpan.Context()))
funcB()
span2.Finish()
// **********子span使用结束**********

完整的grpc服务端代码

package main

import (
	"context"
	"fmt"
	consulapi "github.com/hashicorp/consul/api"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/gin_web/otgrpc"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/grpc_server/utils"
	"grpc_proto_demo/gin_grpc_jeager_consul_balance_demo/proto"
	"io"
	"net"
	"time"
)

type GreeterServer struct {
}
func funcB()  {
	fmt.Println("调用了funcB")
	time.Sleep(1*time.Second)
}
func (h GreeterServer) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error) {

	// **********子span使用开始**********
	// 取出父span
	parentSpan:=opentracing.SpanFromContext(ctx)
	// 取出全局tracer
	tracer:=opentracing.GlobalTracer()
	span2:=tracer.StartSpan("funcB",opentracing.ChildOf(parentSpan.Context()))
	funcB()
	span2.Finish()
	// **********子span使用结束**********
	// 接收客户端发送过来的数据,打印出来
	fmt.Println("客户端传入的名字是:", in.Name)
	fmt.Println("客户端传入的年龄是:", in.Age)
	return &proto.HelloResponse{
		Reply: "gin-调用grpc,grpc给的回复",
	}, nil
}



func getTracer()(opentracing.Tracer, io.Closer)   {
	// 第一步:初始化配置信息
	cfg := &config.Configuration{
		// 采样率暂配置,设置为1,全部采样
		// 如果每个请求都保存到jeager中,压力会大,所以可以设置采集速率
		// 如:rateLimiting:每秒spans数
		Sampler: &config.SamplerConfig{
			Type:  "const",
			Param: 1,
		},
		Reporter: &config.ReporterConfig{
			LogSpans:           true,              // 是否打印日志
			LocalAgentHostPort: "10.0.0.102:6831", // jeager默认端口是6831
		},
		ServiceName: "lqz-service", // 服务名字,也可以在下面NewTracer的时候传入,不过弃用了
	}

	// 第二步:通过配置,生成链路Trace
	// 传入服务名,和日志输出位置
	//cfg.New("lqz-service", config.Logger(jaeger.StdLogger))  // 弃用了
	tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}

	return tracer,closer

}
// 服务端代码
func main() {
	// 定义段端口
	port,_:=utils.GetCanUsePort()
	// 第一步:new一个server,增加jeager的服务端拦截器
	//定义一个新的trace,以后从service中就可以取出父的span
	tracer,closer:=getTracer()
	defer closer.Close()
	// 设置成全局tracer
	opentracing.SetGlobalTracer(tracer)
	g := grpc.NewServer(grpc.UnaryInterceptor(otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer())))


	// 第二步:生成一个结构体对象
	s := GreeterServer{}
	// 第三步: 把s注册到g对象中
	proto.RegisterGreeterServer(g, &s)
	// 第四步:启动服务,监听端口
	lis, error := net.Listen("tcp", fmt.Sprintf("192.168.31.226:%d",port))
	if error != nil {
		panic("启动服务异常")
	}

	//******** 注册grpc服务和设置健康检查********
	// 1 设置健康检查
	//health.NewServer()具体实现grpc已经帮我们写好了
	grpc_health_v1.RegisterHealthServer(g,health.NewServer())
	// 2 注册grpc服务
	grpcId:=utils.GetUUId()
	RegisterConsul("192.168.31.226",port,"grpc_test",grpcId,[]string{"grpc","lqz"})


	g.Serve(lis)

}


func RegisterConsul(localIP string, localPort int, name string,id string, tags []string) error {
	// 创建连接consul服务配置
	config := consulapi.DefaultConfig()
	config.Address = "10.0.0.102:8500"
	client, err := consulapi.NewClient(config)
	if err != nil {
		fmt.Println("consul gin_web error : ", err)
	}

	// 创建注册到consul的服务到
	registration := new(consulapi.AgentServiceRegistration)
	registration.ID = id
	registration.Name = name //根据这个名称来找这个服务
	registration.Port = localPort
	//registration.Tags = []string{"lqz", "gin_web"} //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Tags = tags //这个就是一个标签,可以根据这个来找这个服务,相当于V1.1这种
	registration.Address = localIP

	// 增加consul健康检查回调函数
	check := new(consulapi.AgentServiceCheck)
	check.GRPC = fmt.Sprintf("192.168.31.226:%d",localPort)// 健康检查地址只需要写grpc服务地址端口即可,会自动检查
	check.Timeout = "5s"                         //超时
	check.Interval = "5s"                        //健康检查频率
	check.DeregisterCriticalServiceAfter = "30s" // 故障检查失败30s后 consul自动将注册服务删除
	registration.Check = check
	// 注册服务到consul
	err = client.Agent().ServiceRegister(registration)
	if err != nil {
		return err
	}
	return nil

}

去掉grpc健康检查的链路-otgrpc/server.go

// 修改源码如下
func OpenTracingServerInterceptor(tracer opentracing.Tracer, optFuncs ...Option) grpc.UnaryServerInterceptor {
	otgrpcOpts := newOptions()
	otgrpcOpts.apply(optFuncs...)
	return func(
		ctx context.Context,
		req interface{},
		info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler,
	) (resp interface{}, err error) {
		//在此修改 加入判断,如果是监控检测,就不记录追踪
		if info.FullMethod == "/grpc.health.v1.Health/Check" {
			resp, err = handler(ctx, req)
			return resp, err
		} else {
			spanContext, err := extractSpanContext(ctx, tracer)
			if err != nil && err != opentracing.ErrSpanContextNotFound {
				// TODO: establish some sort of error reporting mechanism here. We
				// don't know where to put such an error and must rely on Tracer
				// implementations to do something appropriate for the time being.
			}
			if otgrpcOpts.inclusionFunc != nil &&
				!otgrpcOpts.inclusionFunc(spanContext, info.FullMethod, req, nil) {
				return handler(ctx, req)
			}
			serverSpan := tracer.StartSpan(
				info.FullMethod,
				ext.RPCServerOption(spanContext),
				gRPCComponentTag,
			)
			defer serverSpan.Finish()

			ctx = opentracing.ContextWithSpan(ctx, serverSpan)
			if otgrpcOpts.logPayloads {
				serverSpan.LogFields(log.Object("gRPC request", req))
			}
			resp, err = handler(ctx, req)
			if err == nil {
				if otgrpcOpts.logPayloads {
					serverSpan.LogFields(log.Object("gRPC response", resp))
				}
			} else {
				SetSpanTags(serverSpan, err, false)
				serverSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
			}
			if otgrpcOpts.decorator != nil {
				otgrpcOpts.decorator(serverSpan, info.FullMethod, req, resp, err)
			}
			return resp, err
		}

	}
}

posted @ 2022-05-29 01:25  刘清政  阅读(641)  评论(0编辑  收藏  举报