go-zore的api和rpc是怎么用的?负载均衡怎么实现?怎么把etcd替换consul?go-zore入门看这一篇就够了!

2023-04-26 17:53:08 424 林溪

本文主要内容

  • 微服务框架对比
  • 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 https://go-zero.dev https://github.com/zeromicro/go-zero 23.7K
go-kratos 2019 https://go-kratos.dev/ https://github.com/go-kratos/kratos 20.4K
tars-go 2018 https://tarscloud.gitbook.io/tarsdocs/ https://github.com/TarsCloud/TarsGo 3.2K
dubbo-go 2019 https://dubbo.apache.org/zh/docs/languages/golang/ https://github.com/apache/dubbo-go 4.4K
go-micro 2015 - https://github.com/asim/go-micro 20.3K
go-kit 2015 - https://github.com/go-kit/kit 24.8K
jupiter 2020 https://jupiter.douyu.com/ https://github.com/douyu/jupiter 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安装

  • 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目录下会有下面几个文件

http服务代码示例

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

  • 我们使用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.

  • 项目目录如下
  • 我们根据路由追踪到OrderInfo方法,进行简单修改
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目录 和api应用差不多,etc/goods.yaml文件定义了端口号和ip,还有etcd的配置,所以我们也看出来了,想要启动rpc,必须先开启etcd。etcd的安装教程

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

grpc和zrpc的关系
  • zrpc是基于grpc的一个rpc框架,内置了服务注册、负载均衡、拦截器等模块。这个我们后面会通过源码来说明。
  • zrpc实现了gRPC的resolver.Builder接口和balancer接口,自定义了resolver和balancer。
  • zrpc提供了丰富的拦截器功能,包括自适应降载、自适应熔断、权限验证、prometheus指标收集等。

接下来我们完善GetGoods方法

  • 重写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是怎么实现负载均衡的。 发现是通过zrpc.MustNewClient(c.GoodsRpc)这个方法生成的client,我们继续点进去看
  • go/pkg/mod/github.com/zeromicro/go-zero@v1.5.1/zrpc/internal/client.go这个包文件下有这样一段代码 可见zrpc是使用了p2c.Name,即p2c_ewma来实现的负载均衡。我们继续看下去。我们之前说过,zrpc是对grpc的封装,下面的代码截图也印证了我们说的。
在这里插入图片描述
在这里插入图片描述

p2c_ewma

  • p2c算法 p2c(Pick Of 2 Choices)二选一: 在多个节点中随机选择两个节点。计算它们的负载率load,选择负载率较低的进行请求。为了避免某些节点一直得不到选择导致不平衡,会在超过一定的时间后强制选择一次。 那么这个负载率是怎么计算的?就通过ewma
  • EWMA

EWMA(Exponentially Weighted Moving-Average)指数移动加权平均法: 是指各数值的加权系数随时间呈指数递减,越靠近当前时刻的数值加权系数就越大,体现了最近一段时间内的平均值。该算法相对于算数平均来说对于突然的网络抖动没有那么敏感,突然的抖动不会体现在请求的lag中,从而可以让算法更加均衡。

服务注册与发现

  • 我们再来看一下go-zore是怎么实现的服务注册和服务发现的

  • 服务注册 其中里面的listenOn就是服务的ip+端口号了

  • 服务发现 在方法NewClient里面有一个dial 而这里面的target其实就是etcd的信息即etcd协议头+ip+port+key

我们先拿到服务注册的信息,然后使用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看一下
  • 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-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
在这里插入图片描述
  • 根据路由的提示,我们把svc的代码补充完整

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

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

  • 全局中间件的注册