本文主要内容
- 微服务结构比照
- 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装置
- 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文件中弥补自己的逻辑
-
全局中间件的注册