go-micro-微服务框架记录

go-micro-微服务框架记录

代码实践仓库: https://github.com/yangxuan0261/GoMicro

fork 官方 examples: https://github.com/yangxuan0261/examples


前篇


入门资料


go-micro的组件

架构图:

  • Registry组件:服务发现组件,提供服务发现机制:解析服务名字至服务地址。目前支持的注册中心有consul、etcd、 zookeeper、dns、gossip等

  • Selector组件:构建在Registry之上的客户端智能负载均衡组件,用于Client组件对Registry返回的服务进行智能选择。

  • Broker组件:发布/订阅组件,服务之间基于消息中间件的异步通信方式,默认使用http方式,线上通常使用消息中间件,如Kafka、RabbitMQ等。

  • Transport组件:服务之间同步通信方式。

  • Codec组件:服务之间消息的编码/解码。

  • Server组件:服务主体,该组件基于上面的Registry/Selector/Transport/Broker组件,对外提供一个统一的服务请求入口。

  • Client组件:提供访问微服务的客户端。类似Server组件,它也是通过Registry/Selector/Transport/Broker组件实现查找服务、负载均衡、同步通信、异步消息等功能。

    (一般分布式架构, 服务间的 rpc 都是通过 Server,Client 实现的)

常见的微服务架构图. 出处


go-micro 默认值

参考: micro 在 创建并运行之后 时,各个 options/plugin 的默认值是什么? - https://gitissue.com/issues/5a20634a9a954719b8dd694e

各个 option/plugin 的默认值是:

  • Service Option: 参见 micro/go-micro/options.go
  • broker: “http”
  • client: “rpcclient”
  • server: “rpcserver”
  • registry: “mdns”
  • transport: “http”
  • service id: servicename-uuid.NewUUID()
  • Service Server Option: 参见
    • address: “:0”
    • name: “go-server”
    • version: “1.0.0”
    • Id: uuid.NewUUID()
    • RegisterInterval: 1分钟(微服务自动向 registry 注册的心跳时间)

src/github.com/micro/go-micro/options.go 中可以看到默认值

1
2
3
4
5
6
7
8
9
10
11
12
func newOptions(opts ...Option) Options {
opt := Options{
Broker: broker.DefaultBroker,
Cmd: cmd.DefaultCmd,
Client: client.DefaultClient,
Server: server.DefaultServer,
Registry: registry.DefaultRegistry,
Transport: transport.DefaultTransport,
Context: context.Background(),
}
...
}

proto 生成 micro 所需文件

  1. 所需文件 protoc.exe, protoc-gen-go.exe, protoc-gen-micro.exe 可执行文件需要在环境变量中.

    protoc.exe 下载地址: https://github.com/protocolbuffers/protobuf/releases

    生成 protoc-gen-go.exe, protoc-gen-micro.exe

    1
    2
    go get -v -u github.com/golang/protobuf/protoc-gen-go // 安装protoc go插件
    go get -v -u github.com/micro/protoc-gen-micro // protoc micro插件, 用于通过.proto文件生成.micro.go代码文件
  2. 创建 proto 文件. user.proto

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    syntax = "proto3";

    service User {
    rpc Hello(Request) returns (Response) {}
    }

    message Request {
    string name = 1;
    }

    message Response {
    string msg = 1;
    }

    然后生成 user.micro.gouser.pb.go 文件

    1
    2
    F:\a_link_workspace\go\GoWinEnv_Test01\src\GoMicro (master -> origin)
    λ protoc -I ./protos/ --go_out=protoc-gen-go:./proto_gen --micro_out=./proto_gen ./protos/user.proto

    --micro_out 参数就是生成 xxx.micro.go 的参数


容错


Api 网关

使用流程

参考: 官方 example: examples\greeter

  1. micro 启动 api 网关

    micro api 即可启动 api 一个网关,默认的端口是8080
    可以通过 命令行参数 --address=0.0.0.0:8081 或者 设置环境 MICRO_API_ADDRESS=0.0.0.0:8081 来修改监听端口

    1
    2
    3
    $ micro api --handler=api --address=0.0.0.0:8081
    2019/10/11 19:50:55 Registering API Request Handler at /
    2019/10/11 19:50:55 HTTP API Listening on [::]:8081

    启动 api 时一定指定 --handler=api 参数, 不然请求时会报错: go.micro.api 500. 参考: Micro API使用 - https://blog.csdn.net/benben_2015/article/details/92675678

  2. 启动 api 服务. (可以启动多个 api 服务, 会 负载均衡)

    参考: src/GoMicro/test_api/api/api.go.

    [api 服务 注意事项](#api 服务 注意事项)

    1
    2
    f:\go\GoWinEnv_new\src\GoMicro\test_api\api (master -> origin)
    $ go run api.go
  3. 命令行 curl 请求测试一下. ( 使用的是 bash.exe 命令行工具, cmder 有问题 )

    1
    2
    3
    4
    5
    6
    7
    // get
    $ curl http://localhost:8081/example/call?name=john
    {"message":"我们已经收到你的请求,john"}

    // post
    $ curl -H 'Content-Type: application/json' -d '{data:123}' http://localhost:8081/example/foo/bar
    收到消息:{data:123}
  4. api 服务 ( API gateway, 一般用来验证请求是否合法, 也就是 token 是否合法) 是对外访问的接口, api 服务之后是 后端不对外访问的的各种 微服务. 参考 examples\greeter, 访问流程:


api 服务 注意事项

参考: src/GoMicro/test_api/api/api.go

  1. 使用的 proto 要引用 micro 的 go.api.Request/go.api.Response ( github.com/micro/go-micro/api/proto/api.proto ) . 如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    syntax = "proto3";

    import "github.com/micro/go-micro/api/proto/api.proto";

    service Example {
    rpc Call(go.api.Request) returns(go.api.Response) {};
    }

    service Foo {
    rpc Bar(go.api.Request) returns(go.api.Response) {};
    }
  2. 服务命名空间

    • 如果启动 api 网关是没有指定 --namespace, 默认使用的 namespacego.micro.api, 所以 api 服务中的命名前缀就必须是 go.micro.api, 如:

      1
      2
      3
      4
      5
      6
      func main() {
      service := micro.NewService(
      micro.Name("go.micro.api.example"), // 请求路径: http://localhost:8081/example/xxx/xxx
      )
      ...
      }
    • 假如 api 网关指定命名空间参数为 --namespace=wilker

      1
      $ micro api --handler=api --address=0.0.0.0:8081 --namespace=wilker

      那么对应的服务

      1
      2
      3
      4
      5
      6
      func main() {
      service := micro.NewService(
      micro.Name("wilker.yang"), // 请求路径: http://localhost:8081/yang/xxx/xxx
      )
      ...
      }

RESTful api 风格

  • 参考: examples\greeter\api\rest\rest.go

Service

Service 是 Client 和 Server 的封装,他包含了一系列的方法使用初始值去初始化 Service 和 Client.


Registry

服务的注册和发现,目前实现的 consul,mdns,etcd,etcdv3,zookeeper,kubernetes 等等,

简单来说,就是 Service 进行 Register,来进行注册,Client 使用watch方法进行监控,当有服务加入或者删除时这个方法会被触发,以提醒客户端更新 Service 信息。

如: etcdv3


Selector

参考: 官方 example: github.com/micro/examples/client/dc_selector/dc_selector.go 中的代码

演示了 用于 负载均衡, 选择节点

  1. 启动 3 个 server

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // server1
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\server\wrapper (master -> origin)
    $ go run main.go
    2019/10/10 15:59:03 Transport [http] Listening on [::]:63115

    // server2
    $ go run main.go
    2019/10/10 15:59:03 Transport [http] Listening on [::]:63086

    // server3
    $ go run main.go
    2019/10/10 15:59:03 Transport [http] Listening on [::]:63115
  2. 启动 client, 根据 自定义的选择算法, 选择节点调用.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\client\dc_selector (master -> origin)
    $ go run dc_selector.go
    --- Select, service:go.micro.srv.example
    --- node, Address:192.168.1.190:63115, Id:go.micro.srv.example-e58aab01-9350-4397-bd72-8e5b8bcf0f12
    --- node, Address:192.168.1.190:63028, Id:go.micro.srv.example-5f24a905-c356-409a-9f81-8a1ac09e5869
    --- node, Address:192.168.1.190:63086, Id:go.micro.srv.example-31ba7a47-f5d2-4946-a9f7-8384f2550d26
    --- Select, len(nodes):3
    --- Select, call node:1
    Call: 0 rsp: 5f24a905-c356-409a-9f81-8a1ac09e5869: Hello John

filter

selector 中可以自定义 filter 选出自己想要的节点

参考: 官方 example:

  • 简单版 - src/github.com/micro/examples/client/dc_filter/dc_filter.go
  • 复杂版 - src/github.com/micro/examples/client/dc_selector/dc_selector.go

在 rpc 的 Call (请求->响应) 或者 Stream (send->recv) 都可以添加 filter 过滤出需要的节点. 添加一个 自定义的 wrapper 丢到 micro 的 ClientWrapper

参考: src/GoSrv/test/srv_template/server/stream.go


Wrapper

请求拦截, 鉴权等, 合法才调用对应的请求

参考: 官方 example: src/github.com/micro/examples/wrapper

  • 源码 github.com/micro/go-micro/options.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Wrappers are applied in reverse order so the last is executed first.
    func WrapClient(w ...client.Wrapper) Option {
    return func(o *Options) {
    // apply in reverse
    for i := len(w); i > 0; i-- {
    o.Client = w[i-1](o.Client)
    }
    }
    }

    实际测试分一下两种情况

    • wrapper 分多出调用 WrapClient 传进去

      1
      2
      micro.WrapClient(logWrap1),
      micro.WrapClient(logWrap2),

      输出则为 逆序

      1
      2
      [Call2] 客户端请求服务:greeter,方法:Greeter.Hello
      [Call1] 客户端请求服务:greeter,方法:Greeter.Hello
    • wrapper 一次调用 WrapClient 传进去

      1
      micro.WrapClient(logWrap1, logWrap2),

      输出则为 顺序

      1
      2
      [Call1] 客户端请求服务:greeter,方法:Greeter.Hello
      [Call2] 客户端请求服务:greeter,方法:Greeter.Hello

Client or Server

具体是拦截 客户端req, 还是 服务端handler, 可以从源码 github.com/micro/go-micro/options.go 中看到.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func WrapClient(w ...client.Wrapper) Option {
o.Client = w[i-1](o.Client)
}

func WrapCall(w ...client.CallWrapper) Option {
o.Client.Init(client.WrapCall(w...))
}

func WrapHandler(w ...server.HandlerWrapper) Option {
o.Server.Init(wrappers...)
}

func WrapSubscriber(w ...server.SubscriberWrapper) Option {
o.Server.Init(wrappers...)
}

Codec

消息的 编码/解码


Transport

内置支持的几种传输方式在 github.com/micro/go-micro/transport 目录下

默认使用的是 http 作为传输层, 直接 run 一个空服务就可以看出来

1
2
3
4
5
f:\go\GoWinEnv_Test01\src\GoMicro\test001\srv (master -> origin)
$ go run service.go
2019/10/08 19:46:32 Transport [http] Listening on [::]:58351 // 这里可以看出来
2019/10/08 19:46:32 Broker [http] Connected to [::]:58352
2019/10/08 19:46:32 Registry [mdns] Registering node: user-0f5e2541-bfb2-47a4-a645-e2199a33a2c0

使用 grpc

  • 参考: test_transport_grpc

要求 client - server 必须同时 grpc 方式传输, 否则调用不到

proto 文件生成 go 文件必须用 grpc 插件生成. 参考: go-入坑笔记.md 中的 grpc

1
$ protoc --go_out=plugins=grpc:. your_pb.proto

使用非常简单

1
2
3
4
5
6
7
8
9
10
import (
"github.com/micro/go-micro/transport/grpc" // 引入 grpc
)

func main() {
service := micro.NewService(
...
micro.Transport(grpc.NewTransport()), // Transport 指定为 grpc
)
}
  • 启动是就可以看到 transport 使用的 grpc

    1
    2
    3
    4
    5
    $ go run service.go
    2019/10/08 19:53:58 Transport [grpc] Listening on [::]:58927 // grpc
    2019/10/08 19:53:58 Broker [http] Connected to [::]:58928
    2019/10/08 19:53:58 Registry [mdns] Registering node: user-0c12feb1-56b4-4496-a743-f22cc4ebd8ac
    --- req, name:World ^_^

Broker

Broker 是消息发布和订阅的接口。因为服务的节点是不固定的,如果有需要修改所有服务行为的需求,可以使服务订阅某个主题,当有信息发布时,所有的监听服务都会收到信息,根据你的需要做相应的行为。

Broker 默认的实现方式是http方式,但是这种方式不要在生产环境用。go-plugins里有很多成熟的消息队列实现方式,有 kafka、nsq、rabbitmq、redis,等等

参考: pubsub

其他参考

使用 grpc

proto 文件生成 go 文件必须用 grpc 插件生成. 参考: go-入坑笔记.md 中的 grpc

1
$ protoc --go_out=plugins=grpc:. your_pb.proto

参考: test_broker_grpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import (
"github.com/micro/go-plugins/broker/grpc"
)

var grpcBroker broker.Broker
func main() {
cmd.Init()

grpcBroker = grpc.NewBroker()

if err := grpcBroker.Init(); err != nil {
log.Fatalf("Broker Init error: %v", err)
}
if err := grpcBroker.Connect(); err != nil {
log.Fatalf("Broker Connect error: %v", err)
}

go sub()

service := micro.NewService(
micro.Name("test_broker_grpc_consumer"),
micro.Broker(grpcBroker),
)
...
}


cmd.Init()

用来读取 启动参数环境变量

1
2
3
4
5
6
7
8
import {
"github.com/micro/go-micro/config/cmd"
}

func main() {
cmd.Init() // 在最开头使用
...
}

TLS

  • 设置TLS证书

    1
    2
    3
    micro --enable_tls=true --tls_cert_file=/path/to/cert --tls_key_file=/path/to/key api
    // 或者环境变量中设置证书参数
    MICRO_ENABLE_TLS=true MICRO_TLS_CERT_FILE=/path/to/cert MICRO_TLS_KEY_FILE=/path/to/key micro api

方式一: 使用内置

github.com/micro/go-micro/transport/options.go 中有说明, 如果没有指定 TLSConfig, 可以通过这个接口来使用 tls.

1
2
3
4
5
6
7
// Use secure communication. If TLSConfig is not specified we
// use InsecureSkipVerify and generate a self signed cert
func Secure(b bool) Option {
return func(o *Options) {
o.Secure = b
}
}

参考: 官方 example: github.com\micro\examples\secure

演示了 简单使用 tls 安全验证的调用

  1. 启动 server

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\secure\srv (master -> origin)
    $ go run main.go
  2. 启动 client

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\secure\cli (master -> origin)
    $ go run main.go

方式二: 自定义 tls 证书

参考: go-micro-安全tls.md 对应的代码: test_secure_tls


重新编译带 tls 的 micro

一旦微服务加入 tls, 相应的 micro 也需要加入 tls, 才能正常的使用.

加入 tls 的方式和 registry 加入 etcdv3 类似.

参考: https://github.com/micro/examples/tree/master/secure


Cli-Srv 数据交互

参考: 官方 example: github.com\micro\examples\server\codegen

或者 github.com/micro/examples/stream

演示了 请求->响应, 服务端推送, 客户端请求->服务端推送 三种 rpc 调用方式

codegen

形参直接可以 Recv

1
2
3
func (e *Example) PingPong(ctx context.Context, stream example.Example_PingPongStream) error {
req, err := stream.Recv()
}
  1. 启动 server

    1
    go run src\github.com\micro\examples\server\codegen\codegen.go
  2. 启动 client

    1
    go run src/github.com/micro/examples/client/codegen/codegen.go

handler

Recv 需要解码. 易用性不如 codegen,

Send 一样

1
2
3
4
5
6
func (e *Example) PingPong(ctx context.Context, stream server.Stream) error {
req := &example.Ping{}
if err := stream.Recv(req); err != nil {
return err
}
}

pubsub

参考: 官方 example: github.com\micro\examples\pubsub

演示了 服务间的 异步 通信, 使用的是 broker 而不是 transport.

broker 默认使用的是 http, 可以替换为 grpc

  1. 启动 server

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\pubsub\srv (master -> origin)
    $ go run main.go
  2. 启动 client

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\pubsub\cli (master -> origin)
    $ go run main.go

metadata

参考: 官方 example: github.com\micro\examples\metadata

演示了 自定义的元数据 ( metadata )发送到一个服务

  1. 启动 server

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\metadata\srv (master -> origin)
    $ go run main.go
  2. 启动 client

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\metadata\cli (master -> origin)
    $ go run main.go

Bot

micro bot 是一只藏在微服务中的小马蝇,有了它,我们可以在 Slack、HipChat、XMPP 等等聊天程序中与它对话,通过它来操控服务。

参考: go-micro-机器人bot.md


调用服务

  • 启动官方 examples 的一个服务 src/github.com/micro/examples/greeter/srv/main.go

    1
    2
    f:\go\GoWinEnv_MicroExamples\src\github.com\micro\examples\greeter\srv (master -> origin)
    $ go run main.go

命令行 micro 工具

  • 使用 call 命令. (注意需要转义 " )

    1
    2
    3
    4
    $ micro call go.micro.srv.greeter Say.Hello {\"name\": \"wolegequ\"}
    {
    "msg": "Hello wolegequ"
    }

web 界面

client 中调用服务

  1. 启动 web 界面.

    1
    2
    $ micro web
    2019/10/11 18:45:00 HTTP API Listening on [::]:8082
  2. 访问: http://localhost:8082/client , 这里的 request 不需要转义 ", 如: {"name": "wolegequ"}

slack 界面 ( bot )

其实也就是 micro 工具包的 call 命令

  1. 启动 bot

    1
    2
    3
    $ micro bot --inputs=slack --slack_token=xoxb-123-123-asdasd
    2019/10/10 19:25:06 [bot] starting
    2019/10/10 19:25:06 [bot] starting input slack
  2. 在 Slack 就可以输入 call 命令. 不需要转义 ", 如: {"name": "wolegequ"}


无 proto 的调用

参考: 官方 example src/github.com/micro/examples/noproto/main.go

适用于 cmd 指令.


RESTful API

简单说就是把一些参数丢到 url 中去获取.

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

TODO:

  • 消息 编码,解码
  • 熔断, 限流

踩坑

  • build 或 run 报错: go: xxx requires

    需要在 GOPATH 目录下使用命令 build 或 run

    1
    2
    E:\ws_go\GoWinEnv (master -> origin)
    $ go build -o mainWithEtcdv3.exe ./src\github.com\micro\micro/main.go ./src\github.com\micro\micro/plugins.go
  • verifying ‘protobuf’ malformed record data

    错误: verifying github.com/gogo/protobuf@v0.0.0-20190410021324-65acae22fc9/go.mod: github.com/gogo/protobuf@v0.0.0-20190410021324-65acae22fc9/go.mod: malformed record data

    可能引用的版本不对. 参考: https://github.com/golang/go/issues/34394

    go.mod 文件中, 替换合法的版本

    1
    replace github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9 => github.com/gogo/protobuf v0.0.0-20190723190241-65acae22fc9d
  • cmder 中 curl post json 请求报错: [globbing] unmatched close brace/bracket in column

    两种办法解决

    1. cmder 中的请求的 " 都是用转义 \"
    2. bash.exe 命名行工具去 curl 请求. ( 推荐 )

网段为 10.192.75.100 导致调用接口408或500问

原因是用了个没用的网卡, 禁用掉就行了

参考: go-micro 调用接口408或500问题排查 - https://segmentfault.com/a/1190000020126702 (困扰了一天的问题, tmd)

所以造成这个问题的原因就是go-micro默认使用的网络连接问题了,只要将有问题的禁用掉即可
那不方便禁用网络连接的情况下怎么解决呢,只需要指定–server_address即可,如下

1
go run main.go --server_address=localhost:8999

经测试不禁用网络连接的情况下,指定–server_address,服务也可正常调用

micro有很多参数可以通过命令行或代码中option来设置,具体可以micro -h 查看