本文主要内容

  • 微服务结构比照
  • goctl的装置和运用
  • go-zore的api服务
  • go-zore的rpc服务
  • 一探负载均衡的完成办法
  • 服务发现
  • 运用consul代替etcd完成服务发现
  • 中间件的完成
  • 相关代码已传送至gitee点击获取代码
  • 文中相关衔接无跳转请点击检查原文

go微服务结构比照

参阅文档 在 Go 语言中,有许多著名的结构,比方go-kit,go-karatos,go-zore,go-micro等。以下表格是截止2023年04月11日的数据统计。

结构名 开源时刻 官网/主文档 github github star
go-zero 2020 go-zero.dev github.com/zeromicro/g… 23.7K
go-kratos 2019 go-kratos.dev/ github.com/go-kratos/k… 20.4K
tars-go 2018 tarscloud.gitbook.io/tarsdocs/ github.com/TarsCloud/T… 3.2K
dubbo-go 2019 dubbo.apache.org/zh/docs/lan… github.com/apache/dubb… 4.4K
go-micro 2015 github.com/asim/go-mic… 20.3K
go-kit 2015 github.com/go-kit/kit 24.8K
jupiter 2020 jupiter.douyu.com/ github.com/douyu/jupit… 4.1K
  • go-zero go-zero全体上做为一个稍重的微服务结构,供给了微服务结构需求具备的通用能力,一起也只带一部分的强束缚,例如针对web和rpc服务需求按照其界说的DSL的协议格式进行界说,日志装备、服务装备、apm装备等都要按照结构界说的最佳实践来走。 社区建造: go-zero现已是CNCF项目,做为一个后起的微服务结构,不得不说在国内社区生态建造和保护上,完美适配国内开源的现状,在微信群、大众号、各种大会等多渠道进行推广,社区也时常有文章指导实践。

  • go-kratos go-kratos全体上做为一个轻量级的微服务结构,B站开源项目; web和rpc服务的 DSL协议直接选用protobuf和grpc进行界说,选用wire做依靠注入、主动生成代码 。 结构定坐落处理微服务的中心诉求。 社区建造:社区建造和保护上,算是做的中规中矩,官网更新一般,有大众号和微信群问题解答

  • tarsgo tarsgo做为tars这个大的C++重量级微服务结构下的go语言服务结构,腾讯开源项目; 关于有个好爹的这个工作,总是喜忧参半的;好处在于许多能力不必从头开始做起,直接依托母体;下风便是独立性相对较差,要选用这个tarsgo的条件,便是要先选用tars这个大的结构。 社区建造: Tars现已是linux根底会项目,社群上做的还算能够,究竟tars作为腾讯开源影响力最大的项目之一,有QQ、微信群。

  • dubbo go dubbogo做为dubbo这个大的Java重量级微服务结构下的go语言服务结构,阿里开源项目;好坏基本跟tarsgo一样 社区建造: dubbo现已是apache根底会项目,社群上做的还算能够,有钉钉群。

  • go-mirco go-micro是一个轻量级的微服务结构,做为一个在2015年就开源的项目,在当时那个市面上开源的微服务结构稀少的年代,它是为数不多的挑选。主要槽点便是作者重心做云服务去啦,相应的社区保护力度较弱。 社区建造:弱

  • go-kit go-kit从严厉意义上来说,并不能做为一个微服务结构,而应该是一个微服务的东西集,其官方界说上也是这么说,供给各种选项让你自由挑选。做为一个在2015年就开源的项目,也是当时许多go项目为数不多的挑选之一。 社区建造:弱

  • jupiter jupiter做为一个重量级的微服务结构,斗鱼开源项目;全体思路上跟tars和dubbo力图供给一个大一统的结构,更确切的说是一个微服务渠道,也带相似tars和dubbo那样的管理控制台,供给各种控制和metric的承继,这也无形中给选用此结构带来了不少代价,tars和dubbo自身是有前史沉淀和大厂布景的,许多腾讯系、阿里系公司会选用。 社区建造:弱,有钉钉群,活跃度不高

go-zore

go-zore参阅文档 go-zero 是一个集成了各种工程实践的 web 和 rpc 结构。经过弹性规划保障了大并发服务端的稳定性,经受了充沛的实战查验。 go-zero 包含极简的 API 界说和生成东西 goctl,能够依据界说的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。

go-zore入门,看这一篇就够了

经过上面比照咱们了解到,go-zore作为后起之秀,能够说是一路突飞猛进,现在排名第二。关于国内来说,能够说是首选结构。

go-zore装置

  • goctl装置 goctl是go-zore的一个东西,和beego里边的bee东西差不多。使咱们开发效率更高。 留意,golang有一些版别装置会报错package net/netip is not in GOROOT等相似的包不存在错误,因为低版别或许会短少某些包文件,升级到最新的go版别即可。相关装置地址https://go.dev/dl/。能够在https://sourcegraph.com/github.com/golang/go/-/tree/src/net/netip搜索包文件的缺失状况。
# Go 1.15 及之前版别
go get -u github.com/zeromicro/go-zero/tools/goctl@latest
# Go 1.16 及今后版别
go install github.com/zeromicro/go-zero/tools/goctl@latest
  • 装置成功后检查版别号
$ goctl -v
goctl version 1.5.1 darwin/amd64
  • 装置protoc请参阅文章https://m.acurd.com/blog-21/hs5a2z7664.html
  • 终究在咱们的$GOBIN目录下会有下面几个文件
    go-zore入门,看这一篇就够了

http服务代码示例

  • 敞开go modules GOPROXY=https://goproxy.cn,direct

    go-zore入门,看这一篇就够了

  • 咱们运用goctl树立一个单体应用,比方构建一个订单服务,api示例和相关用法

  • 咱们先来依据文档写一个api,接纳的是id,回来的是一个data,那么咱们这样写

 $ mkdir zore-order
 $ cd zore-order/
 $ go mod init zore-order

咱们创建一个目录zore-order,并在目录下新建一个order.api, goctl的详细运用

 $ touch order.api
 $ cat order.api 
    // api语法版别
    syntax = "v2"
    info(
        author: "技能小虫"
        date: "2023-04-21"
        desc: "订单api阐明"
    )
    type (
        OrderInfoReq {
            OrderId int64 `json:"order_id"`
        }
        OrderInfoResp {
            OrderId int64 `json:"order_id"` //订单id
            GoodsName string `json:"goods_name"`  //产品名称
        }
    )
    //界说了一个服务叫order-api
    service order-api {
        //获取接口的姓名叫获取用户信息
        @doc "获取订单信息"
        //对应的hanlder即controller是orderInfo
        @handler orderInfo
        //恳求办法是post,路径是/order/order_id,参数是OrderInfoReq,回来值是OrderInfoResp
        post /order/info (OrderInfoReq) returns (OrderInfoResp)
        //能够持续界说多个api
    }
# 依据当时目录下的api文件在当时目录生成api项目,
 $ goctl api go -api *.api -dir ./  --style=goZero
Done.
  • 项目目录如下
    go-zore入门,看这一篇就够了
  • 咱们依据路由追寻到OrderInfo办法,进行简略修正
    go-zore入门,看这一篇就够了
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
	order_id:=req.OrderId
	resp=new(types.OrderInfoResp)
	resp.GoodsName="雪茄"
	resp.OrderId=order_id
	return
}
  • 其间yaml文件界说了发动的端口号和ip,handler的routes.go 界说的路由。运用go run order.go -f etc/order-api.yaml 发动服务,运用默认端口8888。恳求oder/info接口。一个简略的api服务完成了。
 $ curl -X POST -H "Content-Type: application/json" http://localhost:8888/order/info -d '{"order_id":34}'
 {"order_id":34,"goods_name":"雪茄"}
  • 上面的产品姓名是咱们写死的,那其实咱们能够经过调用产品的rpc服务来获取产品信息。接下来咱们再来写一个go-zore的rpc服务。

rpc服务

参阅文档

  • 经过goctl生成服务
 app-go (master) $ mkdir zore-goods
 app-go (master) $ cd zore-goods/
 zore-goods (master) $ go mod init zore-goods
go: creating new go.mod: module zore-goods
 zore-goods (master) $ touch goods.proto
  • 编写一个proto文件用于自界说微服务
syntax = "proto3";
package goods;
// protoc-gen-go 版别大于1.4.0, proto文件需求加上go_package,否则无法生成
option go_package = "./goods";
//界说恳求体
message GoodsRequest {
  int64 goods_id = 1;
}
//界说响应体
message GoodsResponse {
  // 产品id
  int64 goods_id = 1;
  // 产品名称
  string name = 2;
}
service Goods {
  //rpc办法
  rpc getGoods(GoodsRequest) returns(GoodsResponse);
  //能够持续界说多个办法
}
  • 在当时目录下运用goctl生成一个rpc项目goctl rpc protoc *.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.

  • rpc目录

    go-zore入门,看这一篇就够了
    和api应用差不多,etc/goods.yaml文件界说了端口号和ip,还有etcd的装备,所以咱们也看出来了,想要发动rpc,必须先敞开etcd。etcd的装置教程

  • 咱们翻开goods.go文件看一下,发现go-zore用的是zrpc,那么zrpc是个什么东西呢?

    go-zore入门,看这一篇就够了

grpc和zrpc的关系
  • zrpc是基于grpc的一个rpc结构,内置了服务注册、负载均衡、拦截器等模块。这个咱们后面会经过源码来阐明。
  • zrpc完成了gRPC的resolver.Builder接口和balancer接口,自界说了resolver和balancer。
  • zrpc供给了丰富的拦截器功用,包含自适应降载、自适应熔断、权限验证、prometheus指标收集等。

接下来咱们完善GetGoods办法

go-zore入门,看这一篇就够了

  • 重写GetGoods办法
// rpc办法
func (l *GetGoodsLogic) GetGoods(in *goods.GoodsRequest) (res *goods.GoodsResponse,err error) {
	//依据订单id获取产品信息
	goodsId :=in.GoodsId
	res=new(goods.GoodsResponse)
	res.GoodsId= goodsId
	res.Name="茅台"
	return 
}
  • 经过go run goods.go -f etc/goods.yaml 发动rpc服务

api调用rpc服务

  • 不管是rpc之间的相互调用,还是api调用rpc,咱们都需求知道rpc的proto文件,这里有三种办法去获取rpc的proto文件。
  • 第一种是经过go.mod之前的引用。 比方在同层级目录下我这么引用
module zore-order
go 1.20
replace goods => ../zore-goods
require (
	goods v0.0.0
)
  • 第二种便是经过git托管文件,然后经过包的办法引进。
  • 或许直接把文件拷贝到对应的目录,但是每次文件更新比较费事
  • 修正zore-order/etc/order-api.yaml
Name: order-api
Host: 0.0.0.0
Port: 8888
#留意这个姓名和config文件中的姓名是对应的
GoodsRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: goods.rpc
  • 修正zore-order/internal/config/config.go文件
package config
import (
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
	rest.RestConf
	//界说rpc服务
	GoodsRpc zrpc.RpcClientConf
}
  • 修正zore-order/internal/svc/serviceContext.go
package svc
import (
	"github.com/zeromicro/go-zero/zrpc"
	"zore-order/goodsclient"
	"zore-order/internal/config"
)
type ServiceContext struct {
	Config config.Config
	//界说rpc类型
	Goods goodsclient.Goods
}
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		//引进gprc服务
		Goods:goodsclient.NewGoods(zrpc.MustNewClient(c.GoodsRpc)),
	}
}
  • 最终修正zore-order/internal/logic/orderInfoLogic.go
package logic
import (
	"context"
	"zore-order/internal/svc"
	"zore-order/internal/types"
	"zore-order/internal/types/goods"
	"github.com/zeromicro/go-zero/core/logx"
)
type OrderInfoLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}
func NewOrderInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderInfoLogic {
	return &OrderInfoLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
	orderId := req.OrderId
	goodRequest :=new(goods.GoodsRequest)
	goodRequest.GoodsId=25
	goodsInfo, err := l.svcCtx.Goods.GetGoods(l.ctx,goodRequest)
	if err != nil {
		return nil, err
	}
	resp = new(types.OrderInfoResp)
	resp.GoodsName = goodsInfo.Name
	resp.OrderId = orderId
	return
}

发动

  • 顺次发动etcd ,rpc和api,经过etcdctl检查服务注册状况
 zore-goods (master) $ etcd
 zore-goods (master) $ go run goods.go  -f etc/goods.yaml 
 $ etcdctl get "goods" --prefix --keys-only
goods.rpc/7587870084750251282
 $ go run order.go  -f etc/order-api.yaml
  • 恳求api
 $ curl -X POST -H "Content-Type: application/json" http://localhost:8888/order/info -d '{"order_id":34}'
{"order_id":34,"goods_name":"茅台"}
  • api调用rpc成功

  • 接下来咱们看一下go-zere搭配etcd完成负载均衡的功用

动态端口的获取

当咱们的机器上面跑了许多的服务,或许咱们不知道哪些端口是被占用的,哪些端口是可用用的,那么动态的获取端口,无疑便是一个好办法。那么咱们来封装一个这个办法。

func GetFreePort() (int, error) {
	// 动态获取可用端口
	addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, err
	}
	fmt.Println(addr.Port)  // 0
	l, err := net.Listen("tcp", addr.String())
	if err != nil {
		return 0, err
	}
	return l.Addr().(*net.TCPAddr).Port, nil
}
  • 咱们将代码添加到goods.go里边,并替换为动态接口(真实项目中能够封装到东西类里边)
package main
import (
	"flag"
	"fmt"
	"net"
	"zore-goods/internal/config"
	"zore-goods/internal/server"
	"zore-goods/internal/svc"
	"zore-goods/types/goods"
	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)
var configFile = flag.String("f", "etc/goods.yaml", "the config file")
func main() {
	flag.Parse()
	var c config.Config
	conf.MustLoad(*configFile, &c)
	//获取动态接口口
	port, _ := GetFreePort()
	//替换yaml里边的host和端口
	c.ListenOn = fmt.Sprintf("0.0.0.0:%d", port)
	ctx := svc.NewServiceContext(c)
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		goods.RegisterGoodsServer(grpcServer, server.NewGoodsServer(ctx))
		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	defer s.Stop()
	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}
func GetFreePort() (int, error) {
	// 动态获取可用端口
	addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, err
	}
	fmt.Println(addr.Port) // 0
	l, err := net.Listen("tcp", addr.String())
	if err != nil {
		return 0, err
	}
	return l.Addr().(*net.TCPAddr).Port, nil
}
  • 为了后面更直观的展现zore的负载均衡的功用,咱们把回来值也改成动态的,在zore-goods/internal/logic/getgoodslogic.go文件中修正回来值
// rpc办法
func (l *GetGoodsLogic) GetGoods(in *goods.GoodsRequest) (res *goods.GoodsResponse, err error) {
	//依据订单id获取产品信息
	goodsId := in.GoodsId
	res = new(goods.GoodsResponse)
	res.GoodsId = goodsId
	//动态回来信息+rpc的信息
	res.Name = "茅台"+l.svcCtx.Config.ListenOn
	return
}
  • 咱们顺次发动 etcd,rpc(发动三个)和api服务,然后拜访订单信息接口,回来信息如下
 $ etcdctl get "goods" --prefix --keys-only
goods.rpc/7587870123332811269
goods.rpc/7587870123332811272
goods.rpc/7587870123332811275

go-zore入门,看这一篇就够了

go-zore的负载均衡完成模式

  • 接下来咱们追寻看一下,go-zore是怎样完成负载均衡的。 发现是经过zrpc.MustNewClient(c.GoodsRpc)这个办法生成的client,咱们持续点进去看
    go-zore入门,看这一篇就够了
  • go/pkg/mod/github.com/zeromicro/go-zero@v1.5.1/zrpc/internal/client.go这个包文件下有这样一段代码
    go-zore入门,看这一篇就够了
    可见zrpc是运用了p2c.Name,即p2c_ewma来完成的负载均衡。咱们持续看下去。咱们之前说过,zrpc是对grpc的封装,下面的代码截图也印证了咱们说的。

go-zore入门,看这一篇就够了

go-zore入门,看这一篇就够了

p2c_ewma

  • p2c算法 p2c(Pick Of 2 Choices)二选一: 在多个节点中随机挑选两个节点。核算它们的负载率load,挑选负载率较低的进行恳求。为了避免某些节点一直得不到挑选导致不平衡,会在超过必定的时刻后强制挑选一次。 那么这个负载率是怎样核算的?就经过ewma
  • EWMA
    go-zore入门,看这一篇就够了

EWMA(Exponentially Weighted Moving-Average)指数移动加权均匀法: 是指各数值的加权系数随时刻呈指数递减,越靠近当时时刻的数值加权系数就越大,体现了最近一段时刻内的均匀值。该算法相关于算数均匀来说关于忽然的网络颤动没有那么灵敏,忽然的颤动不会体现在恳求的lag中,然后能够让算法愈加均衡。

服务注册与发现

  • 咱们再来看一下go-zore是怎样完成的服务注册和服务发现的

  • 服务注册

    go-zore入门,看这一篇就够了
    go-zore入门,看这一篇就够了
    其间里边的listenOn便是服务的ip+端口号了

  • 服务发现 在办法NewClient里边有一个dial

    go-zore入门,看这一篇就够了
    而这里边的target其实便是etcd的信息即etcd协议头+ip+port+key

go-zore入门,看这一篇就够了
咱们先拿到服务注册的信息,然后运用p2c负载均衡算法选出来可用的服务。

经过上面的源码,其实也能够将etcd替换为consul
  • 咱们经过docker 发动consul docker run -d -p 8500:8500 -p 8300:8309 -p 8301:8301 -p8302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0
  • 删去原yaml文件中etcd的装备,并添加consul的装备
Name: goods.rpc
ListenOn: 0.0.0.0:8080
#和config中保持一致
Consul:
  Host: 127.0.0.1:8500
  Key: goods.rpc
  • 导入zrpc的consul包go get -u github.com/zeromicro/zero-contrib/zrpc/registry/consul
  • 在conf文件中参加consul的装备
package config
import "github.com/zeromicro/go-zero/zrpc"
import "github.com/zeromicro/zero-contrib/zrpc/registry/consul"
type Config struct {
	zrpc.RpcServerConf
	Consul consul.Conf
}
  • 在main完成服务初始化之后注册到consul
//获取动态接口口
	port, _ := GetFreePort()
	//替换yaml里边的host和端口
	c.ListenOn = fmt.Sprintf("0.0.0.0:%d", port)
	ctx := svc.NewServiceContext(c)
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		goods.RegisterGoodsServer(grpcServer, server.NewGoodsServer(ctx))
		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	//把服务信息注册到consul
	_ = consul.RegisterService(c.ListenOn, c.Consul)
  • 咱们重启goods的rpc服务,经过http://127.0.0.1:8500/拜访consul看一下
    go-zore入门,看这一篇就够了
  • api里边咱们修正yaml文件
Name: order-api
Host: 0.0.0.0
Port: 8888
#留意这个姓名和config文件中的姓名是对应的 goods.rpc是key的姓名
GoodsRpc:
  Target: consul://192.168.4.28:8500/goods.rpc?wait=14s
  • 发动api服务并拜访
    go-zore入门,看这一篇就够了

中间件

在go-zero中,中间件能够分为路由中间件和全局中间件,路由中间件是指某一些特定路由需求完成中间件逻辑,其和jwt相似,没有放在jwt:xxx下的路由不会运用中间件功用, 而全局中间件的服务范围则是整个服务。

  • 咱们以路由中间件为例,咱们在获取产品信息的时分判断一下是否登录
  • 咱们在order.api下面添加一个中间件的声明
type (
	OrderInfoReq {
		OrderId int64 `json:"order_id"`
	}
	OrderInfoResp {
		OrderId   int64  `json:"order_id"`   //订单id
		GoodsName string `json:"goods_name"` //产品名称
	}
)
@server(
	login:IsLogIn
	middleware:Login // 路由中间件声明
)
  • 执行goctl生成中间件$ goctl api go -api *.api -dir ./ --style=goZero,在internal下面就会多出一个middleware
  • 咱们翻开路由文件,发现order的路由现已被参加了middleware

go-zore入门,看这一篇就够了

  • 依据路由的提示,咱们把svc的代码弥补完整

    go-zore入门,看这一篇就够了

  • 在middleware文件中弥补自己的逻辑

  • 在middleware文件中弥补自己的逻辑

    go-zore入门,看这一篇就够了

  • 全局中间件的注册

    go-zore入门,看这一篇就够了