本文主要内容
- 微服务框架对比
- 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文件中补充自己的逻辑
-
全局中间件的注册