官方文档: https://micro.mu/docs/
1. etcd启动:
单机版etcd,直接下载etcd.exe点击启动就好,默认监听2379端口,环境变量设置set ETCDCTL_API 3, 这样etcdctl命令就使用v3和etcd server交互。
consul启动 :(go-micro目前版本已经不再支持consul)
consul.exe agent -server -bootstrap -ui -client 0.0.0.0 -bind 192.168.1.101 -data-dir=F:/consul_data
其中,-server 代表以服务端的方式启动;-boostrap 指定自己为leader,而不需要选举;-ui 启动一个内置web界面; -client指定客 户端可以访问的IP,设置为0.0.0.0表示可以任意访问,否则只是默认本机能访问。
如果用docker启动,则:
docker pull consul
docker run -d --name=cs -p 8500:8500 consul agent -server -boostrap -ui -client 0.0.0.1
如果启动正常,访问 http://localhost:8500 会跳转到consul管理页面。
2.go-micro启动一个grpc服务:
需要提前安装protoc工具用来编译pb文件,同时安装micro go插件:
安装protoc
https://github.com/google/protobuf/releases/tag/v3.4.1 下找到win32包 解压并配置环境变量
安装 gen-proto-go
go get -u github.com/golang/protobuf/protoc-gen-go
安装micro protobuf 插件
go get github.com/micro/protobuf/proto github.com/micro/protobuf/protoc-gen-go
proto生成文件
protoc -I . --go_out=plugins=micro:. greeter.proto
安装protco-go-inject-tag插件,用来作pb注解
go get github.com/favadi/protoc-go-inject-tag
编写pb文件:
syntax = "proto3";
package Service;
import "models.proto";
message ProdsReq {
// @inject_tag: json:"size", form:"size"
int32 size = 1;
// @inject_tag: uri:"pid"
int32 prod_id = 2;
}
message ProdListResp {
repeated ProdModel data = 1;
}
message ProdDetailtResp {
ProdModel data = 1;
}
service ProdService {
rpc GetProdsList(ProdsReq) returns (ProdListResp);
rpc GetProdsDetail(ProdsReq) returns (ProdDetailtResp);
}
编译pb:
protoc --micro_out=../ --go_out=../ models.proto
protoc --micro_out=../ --go_out=../ prodService.proto
protoc-go-inject-tag -input=../models.pb.go
protoc-go-inject-tag -input=../prodService.pb.go
编写项目工程:
package main
import (
"github.com/micro/go-micro"
"github.com/micro/go-micro/registry"
"github.com/micro/go-plugins/registry/etcd"
"go-micro-grpc/ServiceImpl"
Service "go-micro-grpc/Services"
)
func main() {
//consul作为注册中心
//consulReg := consul.NewRegistry(
// registry.Addrs("192.168.1.101:8500"),
// )
//etcd作为注册中心
etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
svc := micro.NewService(
micro.Name("prodservice"), //可以通过这个名字调用到此服务
micro.Address(":8011"),
micro.Registry(etcdReg),
)
svc.Init()
//protoc --micro_out=../ --go_out=../ prodService.proto编译出的grpc服务接口
Service.RegisterProdServiceHandler(svc.Server(), new(ServiceImpl.ProdService))
svc.Run()
}
实现服务接口:
package ServiceImpl
import (
"context"
"fmt"
Service "go-micro-grpc/Services"
"strconv"
"time"
)
type ProdService struct {
}
func (service *ProdService) GetProdsDetail(ctx context.Context, req*Service.ProdsReq, resp*Service.ProdDetailtResp) error {
time.Sleep(time.Second*3)
fmt.Printf("req.size:%d, req.prodid:%d\n",req.Size, req.ProdId)
resp.Data = newProd(req.ProdId, "测试商品详情")
return nil
}
func newProd(id int32, pname string) *Service.ProdModel {
return &Service.ProdModel{ProdId:id, ProdName:pname}
}
func (*ProdService)GetProdsList(ctx context.Context, req *Service.ProdsReq,resp *Service.ProdListResp) error {
time.Sleep(time.Second*3)
models := make([]*Service.ProdModel, 0)
var i int32
for i = 0; i < req.Size; i++ {
models = append(models, newProd(i+100, "prodName"+strconv.Itoa(int(i)+100)))
}
resp.Data = models
return nil
}
启动上面的服务就行了。下面来讲如何通过micro.exe工具检查我们启动的服务
安装micro.exe工具:
https://github.com/micro/micro
micro工具中带有一个etcd管理页面,启动etcd管理页面:
set MICRO_REGISTRY=etcd
set MICRO_REGISTRY_ADDRESS=127.0.0.1:2379
set MICRO_API_NAMESPACE=api.jtthink.com
micro web
默认监听8082端口
http://localhost:8082/registry
访问就能看到我们注册在里面的服务了。点击我们的服务还能看到各个方法的调用方法
我们通过micro工具调用:
列出服务
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 list services
go.micro.http.broker
go.micro.registry
go.micro.web
httpprodservice
prodservice
获取服务信息
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 get service prodservice
service prodservice
version 2020.03.21.14.29
ID Address Metadata
prodservice-8f83af4e-43d2-4c16-843b-0c61ef4decf5 192.168.56.1:8011 protocol=mucp,registry=etcd,server=mucp,transport=http,broker=http
Endpoint: ProdService.GetProdsDetail
Request: {
size int32
prod_id int32
}
Response: {
data ProdModel {
prod_id int32
prod_name string
}
}
Endpoint: ProdService.GetProdsList
Request: {
size int32
prod_id int32
}
Response: {
data []ProdModel
}
调用服务:
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 call prodservice ProdService.GetProdsList "{\"size\":3}"
{
"data": [
{
"prod_id": 100,
"prod_name": "prodName100"
},
{
"prod_id": 101,
"prod_name": "prodName101"
},
{
"prod_id": 102,
"prod_name": "prodName102"
}
]
}
3. go-micro启动一个http服务,此服务调用后端的grpc服务:
gin整合到go-micro中,此服务可以不注册到服务发现中心
此项目的pb要和后台的grpc服务pb一致,这样才能通过grpc去调用,生成pb之后编写项目:
package main
import (
"context"
"fmt"
"github.com/micro/go-micro"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/metadata"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/web"
"github.com/micro/go-plugins/registry/consul"
"github.com/micro/go-plugins/registry/etcd"
grpcSvc "go-micro-study/Services"
"go-micro-study/webLib"
)
type logWrapper struct {
client.Client
}
func (l *logWrapper)Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
fmt.Println("调用接口")
md, _ := metadata.FromContext(ctx)
fmt.Printf("[Log Wrapper] ctx: %v service: %s method: %s\n", md, req.Service(), req.Endpoint())
return l.Client.Call(ctx, req, rsp, opts...)
}
func NewLogWrapper(c client.Client) client.Client {
return &logWrapper{c}
}
func main() {
//consul已经不再使用
//需要导入 "github.com/micro/go-plugins/registry/consul" 这个路径才行,
//而不是使用go-micro内置的
// consulReg := consul.NewRegistry(
// registry.Addrs("192.168.1.101:8500"), //服务发现地址,也就是前
// 面启动的consul
// )
//使用etcd作为注册中心
etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
mySvc := micro.NewService(
micro.Name("prodservice.client"),
micro.WrapClient(NewLogWrapper), //包装log函数
)
//后端grpc服务的client
prodSvc := grpcSvc.NewProdService("prodservice", mySvc.Client())
httpSvc := web.NewService(
web.Name("httpprodservice"), //服务名称
web.Address(":8001"), //监听端口
//将gin引入, 里面包含gin注册的http路由,参数prodSvc用于调用后端grpc服务
web.Handler(webLib.NewGinRouter(prodSvc)),
web.Registry(etcdReg), //将服务注册中心引入
)
httpSvc.Init()
httpSvc.Run() //启动
}
gin路由(即包"go-micro-study/webLib"的内容):
func NewGinRouter(prodSvc grpcSvc.ProdService) *gin.Engine {
ginRouter := gin.Default() //gin web框架
ginRouter.Use(InitMiddleware(prodSvc))
v1Group := ginRouter.Group("/v1")
{
v1Group.Handle("POST", "/prods", GetProdList) //组成 POST /v1/prods 路由
}
return ginRouter
}
//middleware
//为了将grpccli封装到context中
func InitMiddleware(prodSvc grpcSvc.ProdService) gin.HandlerFunc {
return func(context *gin.Context) {
context.Keys = make(map[string]interface{})
context.Keys["prodservice"] = prodSvc
context.Next()
}
}
//统一错误处理
func ErrorMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
defer func() {
if r := recover(); r != nil {
context.JSON(500, gin.H{"status":fmt.Sprintf("%s", r)})
context.Abort()
}
context.Next()
}()
}
}
//handler
func defaultProds() (resp *grpcSvc.ProdListResp, err error) {
data := make([]*grpcSvc.ProdModel, 0)
for i := 0; i < 5; i++ {
data = append(data, &grpcSvc.ProdModel{ProdId:int32(20+i), ProdName:"prodname"+strconv.Itoa(i)})
}
resp = &grpcSvc.ProdListResp{Data:data} //调用grpc server
return
}
func GetProdList(ginCtx *gin.Context) {
var req grpcSvc.ProdsReq
var resp *grpcSvc.ProdListResp
prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
err := ginCtx.Bind(&req)
if err != nil {
ginCtx.JSON(500, gin.H{"status":err.Error()})
} else {
resp, err = prodSvc.GetProdsList(context.Background(), &req)
ginCtx.JSON(200, gin.H{"data": resp.Data})
return
//熔断代码改造,这是在每一个grpc调用钱包装熔断器,后面讲怎么封装
//1.配置configuration
configA := hystrix.CommandConfig{
Timeout: 1000,
}
//2.配置command
hystrix.ConfigureCommand("getprods", configA)
//3. 执行,使用Do方法
err = hystrix.Do("getprods", func() error {
resp, err = prodSvc.GetProdsList(context.Background(), &req)
return err
}, func(e error) error {
resp, err = defaultProds()
return e
})
resp, err = prodSvc.GetProdsList(context.Background(), &req)
if err != nil {
ginCtx.JSON(500, gin.H{"status":err.Error()})
}
ginCtx.JSON(200, gin.H{"data": resp.Data})
}
}
func PanicError(err error) {
if err != nil {
panic(err)
}
}
func GetProdDetail(ginCtx *gin.Context) {
var prodReq grpcSvc.ProdsReq //此pb结构体添加注解,表示要支持uri可变参数
PanicError(ginCtx.BindUri(&prodReq)) //这里支持uri带可变参数
prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
resp, _ := prodSvc.GetProdsDetail(context.Background(), &prodReq)
ginCtx.JSON(200, gin.H{"data": resp.Data})
}
3. protocol中使用http uri占位符:
——即protocol定义的结构体支持类似 /v1/prods/:pid 占位符。其中pid表示商品ID,根据实际请求变动
syntax = "proto3";
package Service;
import "models.proto";
message ProdsReq {
// @inject_tag: json:"size", form:"size" //注意这里的form表示支持uri带可变参数
int32 size = 1;
// @inject_tag: uri:"pid"
int32 prod_id = 2;
}
message ProdListResp {
repeated ProdModel data = 1;
}
message ProdDetailtResp {
ProdModel data = 1;
}
service ProdService {
rpc GetProdsList(ProdsReq) returns (ProdListResp);
rpc GetProdsDetail(ProdsReq) returns (ProdDetailtResp);
}
message ProdModel {
// @inject_tag: json:"pid" //protoc-go-inject-tag 这个工具会把有 @inject_tag 标志的pb tag 改成我们想要的
int32 prod_id = 1;
// @inject_tag: json:"prod_name", form:"size" //多种类型:json 和 form
string prod_name = 2;
}
使用protoc-go-inject-tag工具生成tag:
protoc --micro_out=../ --go_out=../ models.proto
protoc --micro_out=../ --go_out=../ prodService.proto
protoc-go-inject-tag -input=../models.pb.go
protoc-go-inject-tag -input=../prodService.pb.go //这个对注解进行处理
binduri
func GetProdDetail(ginCtx *gin.Context) {
var prodReq grpcSvc.ProdsReq
PanicError(ginCtx.BindUri(&prodReq)) //gin上下文中使用BindUri绑定带有uri注解的pb
prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
resp, _ := prodSvc.GetProdsDetail(context.Background(), &prodReq)
ginCtx.JSON(200, gin.H{"data": resp.Data})
}
这样就可以让pb支持http的uri占位符了
4. 熔断插件
在http调用后端的go-micro微服务时,我们在newMicro的时候包装熔断插件
熔断类及其方法,尤其是Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error 方法:
package main
...
func main() {
etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
mySvc := micro.NewService(
micro.Name("prodservice.client"),
micro.WrapClient(NewLogWrapper),
micro.WrapClient(wrappers.NewProdsWrapper), //这里注册了熔断插件
)
...
}
熔断插件:
package wrappers
import (
"context"
"github.com/afex/hystrix-go/hystrix"
"github.com/micro/go-micro/client"
grpcSvc "go-micro-study/Services"
"strconv"
)
type ProdsWrapper struct {
client.Client
}
func NewProdsWrapper(c client.Client) client.Client {
return &ProdsWrapper{c}
}
func (l *ProdsWrapper)Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
cmdName := req.Service()+"."+req.Endpoint()
//1.配置configuration
configA := hystrix.CommandConfig{
Timeout: 1000,
RequestVolumeThreshold: 2, //阈值:意思是有20个请求才进行错误百分比计算
ErrorPercentThreshold:50, //错误百分比 20% 的错误发生之后,就直接执行降级方法
SleepWindow: 5000, //过多少毫秒之后重新尝试后端被降级的服务
}
//2.配置command
hystrix.ConfigureCommand(cmdName, configA)
return hystrix.Do(cmdName,
func() error {
return l.Client.Call(ctx,req, rsp, opts...)
},
func(e error) error {
//defaultProds(rsp)
defaultData(rsp)
return nil
})
}
//通用降级方法
func defaultData(rsp interface{}) {
switch t := rsp.(type) {
case *grpcSvc.ProdListResp:
defaultProds(rsp)
case *grpcSvc.ProdDetailtResp:
t.Data = &grpcSvc.ProdModel{ProdId:int32(10), ProdName:"降级商品详情"+strconv.Itoa(10)}
default:
}
}
//商品列表详情
func defaultProds(rsp interface{}) {
data := make([]*grpcSvc.ProdModel, 0)
for i := 0; i < 5; i++ {
data = append(data, &grpcSvc.ProdModel{ProdId:int32(20+i), ProdName:"prodname"+strconv.Itoa(i)})
}
resp := rsp.(*grpcSvc.ProdListResp)
resp.Data = data
return
}
一般Call方法中使用hystrix的步骤是:
1)定义一个配置 configA
2) 调用hystrix.ConfigureCommand(cmdName, configA)将配置和一个名字绑定到hystrix包中
3)然后调用hystrixDo(cmdName),就可以调用我们定义的配置了,后边的两个回调函数分别是正常业务函数和降级函数
启动服务之后,就可以通过postMan调用http,http再去调用grpc服务了。