第四课 Go容器化微服务系统实战-链路追踪观望台
tags:
- GO
- 慕课网
categories:
- 链路追踪jaeger
- 数据库外键
- 数据库事务
第一节 链路追踪简介
1.1 链路追踪jaeger基本介绍
- 微服务链路追(jaeger)踪作用: 它是用来监视和诊断基于微服务的分布式系统
- 用于服务依赖性分析,辅助性能优化
- 微服务链路追踪(jaeger)主要特性
- 高扩展性
- 原生支持OpenTracing
- 可观察性
1.2 jaeger-术语Span
- Span是Jaeger 中的逻辑工作单元
- Span具有操作名称,操作的开始时间和持续时间
- 它的跨度可以嵌套并排序以建立因果关系模型
- 微服务链路追踪(jaeger)-术语Span包含的对象
- Operation name:操作名称(也可以称作Span name )
- Start timestamp:起始时间
- Finish timestamp:结束时间
- Span tag :一组键值对构成的Span标签集合
- Span log :一组Span的日志集合
- SpanContext: span 上下文对象
- References ( Span间关系)︰相关的零个或者多个Span
1.3 jaeger的调用过程
- 一次调用链分析
1.4 jaeger的组件
-
Jaeger Client的调用原理。
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021070523530863.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FhMTg4NTU5NTMyMjk=,size_16,color_FFFFFF,t_70 -
微服务链路追踪(jaeger)的五个重要组件
- Jaeger-client(客户端库, 不同语言有不同库)
- Agent(客户端代理,是一个监听的守护程序。把client的数据传给Collector)
- Collector (数据收集处理)
- Data Store (数据存储)
- UI(数据查询与前端界面展示)
1.5 jaeger安装和常用端口
- 推荐博客:https://www.cnblogs.com/jiujuan/p/13235748.html
# 获取镜像all-in-one:docker地址:https://hub.docker.com/r/jaegertracing/all-in-one
docker pull jaegertracing/all-in-one:latest
# 启动容器
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
第二节 商品模块开发
2.1 商品模块目录建立
- 用上节课方法自动生成我们的目录文件。
sudo docker pull micro/micro
sudo docker run --rm -v $(pwd):$(pwd) -w $(pwd) micro/micro new product
- 新建domain文件夹,并在domain中创建model、repository、service三个文件夹。
- 删除go.mod,重新通过下面命令生成。
go mod init git.imooc.com/qnhyn/product
go mod tidy
- 新建common文件夹。
2.2 商品模块代码proto开发
- 编写proto。把product.proto文件放到product文件夹下。
- 编译生成go文件。
# 生成之后看下product.pb.micro.go 用到的版本 主要要是v2(v3会在编译时报错)
protoc *.proto --gofast_out=. --micro_out=.
syntax = "proto3";
package go.micro.service.product;
service Product {
rpc AddProduct(ProductInfo) returns (ResponseProduct){}
rpc FindProductByID(RequestID) returns (ProductInfo){}
rpc UpdateProduct(ProductInfo) returns (Response) {}
rpc DeleteProductByID(RequestID) returns (Response) {}
rpc FindAllProduct(RequestAll) returns (AllProduct){}
}
message ProductInfo {
int64 id = 1;
string product_name = 2;
string product_sku = 3;
double product_price = 4;
string product_description = 5;
int64 product_category_id = 6;
// repeated 结构体对象
repeated ProductImage product_image = 7;
// 商品的尺寸
repeated ProductSize product_size = 8;
// 搜索引擎优化的展示的信息
ProductSeo product_seo = 9;
}
message ProductImage {
int64 id = 1;
string image_name = 2;
// 保证插入数据的幂等性
string image_code = 3;
string image_url = 4;
}
message ProductSize{
int64 id = 1;
string size_name = 2;
string size_code = 3;
}
message ProductSeo {
int64 id = 1;
string seo_title = 2;
string seo_keywords = 3;
string seo_description =4;
string seo_code = 6;
}
message ResponseProduct {
int64 product_id = 1;
}
message RequestID {
int64 product_id = 1;
}
message Response {
string msg = 1;
}
message RequestAll{
}
message AllProduct{
repeated ProductInfo product_info =1;
}
2.3 商品模块domain的model开发
- product.go
package model
type Product struct{
ID int64 `gorm:"primary_key;not_null;auto_increment" json:"id"`
ProductName string `json:"product_name"`
ProductSku string `gorm:"unique_index:not_null" json:"product_sku"`
ProductPrice float64 `json:"product_price"`
ProductDescription string `json:"product_description"`
ProductImage []ProductImage `gorm:"ForeignKey:ImageProductID" json:"product_image"`
ProductSize []ProductSize `gorm:"ForeignKey:SizeProductID" json:"product_size"`
ProductSeo ProductSeo `gorm:"ForeignKey:SeoProductID" json:"product_seo"`
}
- product_image.go 字段ImageCode可以保证数据的幂等性。
package model
type ProductImage struct {
ID int64 `gorm:"primary_key;not_null;auto_increment" json:"id"`
ImageName string `json:"image_name"`
ImageCode string `gorm:"unique_index;not_null" json:"image_code"`
ImageUrl string `json:"image_url"`
ImageProductID int64 `json:"image_product_id"`
}
- product_size.go
package model
type ProductSize struct {
ID int64 `gorm:"primary_key;not_null;auto_increment" json:"id"`
SizeName string `json:"size_name"`
SizeCode string `gorm:"unique_index;not_null" json:"size_code"`
SizeProductID int64 `json:"size_product_id"`
}
- product_seo.go
package model
type ProductSeo struct {
ID int64 `gorm:"primary_key;not_null;auto_increment" json:"id"`
SeoTitle string `json:"seo_title"`
SeoKeywords string `json:"seo_keywords"`
SeoDescription string `json:"seo_description"`
SeoCode string `json:"seo_code"`
SeoProductID int64 `json:"seo_product_id"`
}
2.4 商品模块domain的repository开发
- product_repository.go 和前面不同。
- 这里初始化表,一下可以初始化四张表
- 删除时有开启事务回滚。
package repository
import (
"git.imooc.com/qnhyn/product/domain/model"
"github.com/jinzhu/gorm"
)
type IProductRepository interface{
InitTable() error
FindProductByID(int64) (*model.Product, error)
CreateProduct(*model.Product) (int64, error)
DeleteProductByID(int64) error
UpdateProduct(*model.Product) error
FindAll()([]model.Product,error)
}
// NewProductRepository 创建productRepository
func NewProductRepository(db *gorm.DB) IProductRepository {
return &ProductRepository{mysqlDb:db}
}
type ProductRepository struct {
mysqlDb *gorm.DB
}
// InitTable 初始化表 同时初始化四张表
func (u *ProductRepository)InitTable() error {
return u.mysqlDb.CreateTable(&model.Product{},&model.ProductSeo{},&model.ProductImage{},&model.ProductSize{}).Error
}
// FindProductByID 根据ID查找Product信息
func (u *ProductRepository)FindProductByID(productID int64) (product *model.Product,err error) {
product = &model.Product{}
return product, u.mysqlDb.Preload("ProductImage").Preload("ProductSize").Preload("ProductSeo").First(product,productID).Error
}
// CreateProduct 创建Product信息
func (u *ProductRepository) CreateProduct(product *model.Product) (int64, error) {
return product.ID, u.mysqlDb.Create(product).Error
}
// DeleteProductByID 根据ID删除Product信息
func (u *ProductRepository) DeleteProductByID(productID int64) error {
//开启事务
tx := u.mysqlDb.Begin()
defer func() {
if r:= recover();r !=nil {
tx.Rollback()
}
}()
if tx.Error !=nil {
return tx.Error
}
//删除
if err:=tx.Unscoped().Where("id = ?",productID).Delete(&model.Product{}).Error;err!=nil{
tx.Rollback()
return err
}
if err:=tx.Unscoped().Where("images_product_id = ?",productID).Delete(&model.ProductImage{}).Error;err!=nil{
tx.Rollback()
return err
}
if err:=tx.Unscoped().Where("size_product_id = ?", productID).Delete(&model.ProductSize{}).Error;err!=nil{
tx.Rollback()
return err
}
if err:=tx.Unscoped().Where("seo_product_id = ?",productID).Delete(&model.ProductSeo{}).Error;err!=nil{
tx.Rollback()
return err
}
return tx.Commit().Error
}
// UpdateProduct 更新Product信息
func (u *ProductRepository) UpdateProduct(product *model.Product) error {
return u.mysqlDb.Model(product).Update(product).Error
}
// FindAll 获取结果集
func (u *ProductRepository) FindAll()(productAll []model.Product,err error) {
return productAll, u.mysqlDb.Preload("ProductImage").Preload("ProductSize").Preload("ProductSeo").Find(&productAll).Error
}
2.5 商品模块domain的service开发
- product_data_service.go
package service
import (
"git.imooc.com/qnhyn/product/domain/model"
"git.imooc.com/qnhyn/product/domain/repository"
)
type IProductDataService interface {
AddProduct(*model.Product) (int64 , error)
DeleteProduct(int64) error
UpdateProduct(*model.Product) error
FindProductByID(int64) (*model.Product, error)
FindAllProduct() ([]model.Product, error)
}
// NewProductDataService 创建
func NewProductDataService(productRepository repository.IProductRepository) IProductDataService{
return &ProductDataService{ productRepository }
}
type ProductDataService struct {
ProductRepository repository.IProductRepository
}
// AddProduct 插入
func (u *ProductDataService) AddProduct(product *model.Product) (int64 ,error) {
return u.ProductRepository.CreateProduct(product)
}
// DeleteProduct 删除
func (u *ProductDataService) DeleteProduct(productID int64) error {
return u.ProductRepository.DeleteProductByID(productID)
}
// UpdateProduct 更新
func (u *ProductDataService) UpdateProduct(product *model.Product) error {
return u.ProductRepository.UpdateProduct(product)
}
// FindProductByID 查找
func (u *ProductDataService) FindProductByID(productID int64) (*model.Product, error) {
return u.ProductRepository.FindProductByID(productID)
}
// FindAllProduct 查找
func (u *ProductDataService) FindAllProduct() ([]model.Product, error) {
return u.ProductRepository.FindAll()
}
2.6 商品模块Handle开发
- product.go
- common/swap.go 这个模块后期会提出成一个服务。现在先和上个服务一样
package common
import (
"encoding/json"
)
// SwapTo 通过json tag 进行结构体赋值
func SwapTo(request, category interface{}) (err error) {
dataByte, err := json.Marshal(request)
if err != nil {
return
}
err = json.Unmarshal(dataByte, category)
return
}
- product.go业务逻辑如下:
package handler
import (
"context"
"fmt"
"git.imooc.com/qnhyn/product/common"
"git.imooc.com/qnhyn/product/domain/model"
"git.imooc.com/qnhyn/product/domain/service"
. "git.imooc.com/qnhyn/product/proto/product"
)
type Product struct{
ProductDataService service.IProductDataService
}
// AddProduct 添加商品
func (h *Product) AddProduct(ctx context.Context, request *ProductInfo, response *ResponseProduct) error {
productAdd := &model.Product{}
fmt.Println(request)
if err := common.SwapTo(request,productAdd);err!=nil{
return err
}
fmt.Println(productAdd)
productID,err:=h.ProductDataService.AddProduct(productAdd)
if err!=nil{
return err
}
response.ProductId = productID
return nil
}
// FindProductByID 根据ID查找商品
func (h *Product) FindProductByID(ctx context.Context, request *RequestID, response *ProductInfo) error {
productData,err := h.ProductDataService.FindProductByID(request.ProductId)
if err !=nil {
return err
}
if err := common.SwapTo(productData,response);err !=nil {
return err
}
return nil
}
// UpdateProduct 商品更新
func (h *Product) UpdateProduct(ctx context.Context, request *ProductInfo, response *Response) error {
productAdd := &model.Product{}
if err := common.SwapTo(request,productAdd);err!=nil{
return err
}
err := h.ProductDataService.UpdateProduct(productAdd)
if err != nil {
return err
}
response.Msg = "更新成功"
return nil
}
// DeleteProductByID 根据ID删除对应商品
func (h *Product) DeleteProductByID(ctx context.Context, request *RequestID, response *Response) error {
if err := h.ProductDataService.DeleteProduct(request.ProductId);err!=nil {
return err
}
response.Msg = "删除成功"
return nil
}
// FindAllProduct 查找所有商品
func (h *Product) FindAllProduct(ctx context.Context, request *RequestAll, response *AllProduct) error {
productAll,err := h.ProductDataService.FindAllProduct()
if err !=nil {
return err
}
for _,v := range productAll {
productInfo := &ProductInfo{}
err := common.SwapTo(v,productInfo)
if err != nil {
return err
}
response.ProductInfo = append(response.ProductInfo,productInfo)
}
return nil
}
2.7 main.go入口文件开发
-
main.go
- 第一步:配置中心
- 第二步:注册中心
- 第三步:链路追踪
- 第四步:数据库设置和初始化
- 第五步:服务初始化和运行
-
写一个链路追踪的创建common函数。jaeger.go
package common
import (
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"io"
"time"
)
// NewTracer 创建链路追踪实例
func NewTracer(serviceName string, addr string) (opentracing.Tracer, io.Closer, error) {
cfg := &config.Configuration{
ServiceName:serviceName,
Sampler:&config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &config.ReporterConfig{
BufferFlushInterval: 1*time.Second,
LogSpans: true,
LocalAgentHostPort: addr,
},
}
return cfg.NewTracer()
}
- http://192.168.242.142:8500/ 编写数据库配置 /micro/config/mysql
{
"host":"127.0.0.1",
"user":"root",
"pwd":"123456",
"database":"product",
"port":3306
}
- main.go的编写, 运行测试
package main
import (
"git.imooc.com/qnhyn/product/common"
"git.imooc.com/qnhyn/product/domain/repository"
service2 "git.imooc.com/qnhyn/product/domain/service"
"git.imooc.com/qnhyn/product/handler"
"github.com/jinzhu/gorm"
"github.com/micro/go-micro/v2"
log "github.com/micro/go-micro/v2/logger"
"github.com/micro/go-micro/v2/registry"
opentracing2 "github.com/micro/go-plugins/wrapper/trace/opentracing/v2"
consul2 "github.com/micro/go-plugins/registry/consul/v2"
"github.com/opentracing/opentracing-go"
_ "github.com/jinzhu/gorm/dialects/mysql"
product "git.imooc.com/qnhyn/product/proto/product"
)
func main() {
//配置中心
consulConfig,err := common.GetConsulConfig("127.0.0.1",8500,"/micro/config")
if err != nil {
log.Error(err)
}
//注册中心
consul := consul2.NewRegistry(func(options *registry.Options) {
options.Addrs = []string{
"127.0.0.1:8500",
}
})
//链路追踪
t,io,err:=common.NewTracer("go.micro.service.product","localhost:6831")
if err !=nil {
log.Fatal(err)
}
defer io.Close()
opentracing.SetGlobalTracer(t)
//数据库设置
mysqlInfo := common.GetMysqlFromConsul(consulConfig,"mysql")
db,err := gorm.Open("mysql",mysqlInfo.User+":"+mysqlInfo.Pwd+"@/"+mysqlInfo.Database+"?charset=utf8&parseTime=True&loc=Local")
if err !=nil {
log.Error(err)
}
defer db.Close()
//禁止副表
db.SingularTable(true)
//初始化
//repository.NewProductRepository(db).InitTable()
productDataService := service2.NewProductDataService(repository.NewProductRepository(db))
// 设置服务
service := micro.NewService(
micro.Name("go.micro.service.product"),
micro.Version("latest"),
micro.Address("127.0.0.1:8085"),
//添加注册中心
micro.Registry(consul),
//绑定链路追踪
micro.WrapHandler(opentracing2.NewHandlerWrapper(opentracing.GlobalTracer())),
)
// Initialise service
service.Init()
// Register Handler
product.RegisterProductHandler(service.Server(), &handler.Product{ProductDataService:productDataService})
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
第三节 链路追踪效果
3.1 创建验证文件
- 创建一个客户端的server, 同样给它注册到注册中心。productClient.go。给服务端发起一个请求。
package main
import (
"context"
"fmt"
"git.imooc.com/qnhyn/product/common"
go_micro_service_product "git.imooc.com/qnhyn/product/proto/product"
"github.com/micro/go-micro/v2"
"github.com/micro/go-micro/v2/registry"
consul2 "github.com/micro/go-plugins/registry/consul/v2"
opentracing2 "github.com/micro/go-plugins/wrapper/trace/opentracing/v2"
"github.com/opentracing/opentracing-go"
"log"
)
func main() {
//注册中心
consul := consul2.NewRegistry(func(options *registry.Options) {
options.Addrs = []string{
"127.0.0.1:8500",
}
})
//链路追踪
t,io,err:=common.NewTracer("go.micro.service.product.client","localhost:6831")
if err !=nil {
log.Fatal(err)
}
defer io.Close()
opentracing.SetGlobalTracer(t)
service := micro.NewService(
micro.Name("go.micro.service.product.client"),
micro.Version("latest"),
micro.Address("127.0.0.1:8085"),
//添加注册中心
micro.Registry(consul),
//绑定链路追踪 服务端绑定handle 客户端绑定client
micro.WrapClient(opentracing2.NewClientWrapper(opentracing.GlobalTracer())),
)
// 访问服务器端服务
productService:=go_micro_service_product.NewProductService("go.micro.service.product",service.Client())
productAdd := &go_micro_service_product.ProductInfo{
ProductName: "imooc",
ProductSku: "cap",
ProductPrice: 1.1,
ProductDescription: "imooc-cap",
ProductCategoryId: 1,
ProductImage: []*go_micro_service_product.ProductImage{
{
ImageName: "cap-image",
ImageCode: "capimage01",
ImageUrl: "capimage01",
},
{
ImageName: "cap-image02",
ImageCode: "capimage02",
ImageUrl: "capimage02",
},
},
ProductSize: []*go_micro_service_product.ProductSize{
{
SizeName: "cap-size",
SizeCode: "cap-size-code",
},
},
ProductSeo: &go_micro_service_product.ProductSeo{
SeoTitle: "cap-seo",
SeoKeywords: "cap-seo",
SeoDescription: "cap-seo",
SeoCode: "cap-seo",
},
}
response,err:=productService.AddProduct(context.TODO(),productAdd)
if err !=nil {
fmt.Println(err)
}
fmt.Println(response)
}
3.2 运行验证
- 先运行main。在运行两次客户端。刷新jaeger页面。看到两次信息。
- 第二次插入会报错重复问题。所以有错误。可以在jaeger上看报错日志。
3.3 链路追踪小节
- 链路追踪数据写入的过程中可以加入 kafaka 缓冲压力
- 我们可以通过链路追踪开发现我们是否有循环调用
- 在链路中如非必要尽量避免带入异步场景的span,异步不确定啥时候返回。
- 其他应用场景和异步链路如何实现可以自己补充学习一下。