阅读 1420

牌类游戏使用微服务重构笔记(三): micro框架简介 go-micro

博客目录 牌类游戏使用微服务重构笔记
上期博客 牌类游戏使用微服务重构笔记(二): micro框架简介:micro toolkit

micro与go-micro

上文讲过,micro是个toolkit工具包,主要用于开发、调试、部署、运维、api网关等,而go-micro才是我们代码中经常使用到的项目

之前的helloworld example里我们已经使用过go-micro了

package main

import (
	"context"
	"log"

        # here
	"github.com/micro/go-micro"
	// 引用上面生成的proto文件
	proto "micro-blog/helloworld/proto"
)
复制代码

服务发现

服务发现用于解析服务名与地址。服务发现是微服务开发中的核心。当服务A要与服务B协作时,它得知道B在哪里。micro 0.17.0默认的服务发现系统是Consul,0.22.0默认的服务发现系统是Mdns, 其中Mdns不依赖其他组件,可以当做本地开发的服务发现方式

  • 更改服务发现

    启动微服务时追加参数--registry=consul --registry_address=localhost:8500或配置环境MICRO_REGISTRY=consul MICRO_REGISTRY_ADDRESS=localhost:8500即可, 如果更改了服务发现方式,需要重启micro api网关,参数一致,否则无法读取服务列表

  • 自定义服务发现

    micro中服务发现是很好拓展的,可以使用插件实现自己的服务发现方式,例如:etcd, kubernetes, zookeeper, nats, redis等,可参照 micro/go-plugins 库

Protobuf

微服务中有个关键需求点,就是接口的强定义。Micro使用protobuf来完成这个需求。下面定义一个微服务Greeter,有一个Hello方法。它有HelloRequest入参对象及HelloResponse出参对象,两个对象都有一个字符串类型的参数。

安装protoc micro插件

go get github.com/micro/protoc-gen-micro
复制代码

编写proto

syntax = "proto3";

service Greeter {
	rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
	string name = 1;
}

message HelloResponse {
	string greeting = 2;
}
复制代码

编译proto, 不要忘记micro插件

protoc -I . --go_out=. --micro_out=. proto/greeter.proto 
复制代码

编译成功后会生成两个文件, greeter.micro.go、greeter.pb.go, 其中greeter.pb.go 是protoc原本会生成的文件,而greeter.micro.go是针对go-micro额外生成的,相当于额外做了一些包装, 我们的微服务需要实现其中的Handler 接口, 查看greeter.micro.go 可以发现

// Server API for Greeter service

type GreeterHandler interface {
	Hello(context.Context, *HelloRequest, *HelloResponse) error
}
复制代码

编写服务代码, 实现接口

package main

import (
	"context"
	"fmt"

	micro "github.com/micro/go-micro"
	proto "github.com/micro/examples/service/proto"
)

type Greeter struct{}

func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

func main() {
	// 创建新的服务,这里可以传入其它选项。
	service := micro.NewService(
		micro.Name("greeter"),
	)

	// 初始化方法会解析命令行标识
	service.Init()

	// 注册处理器
	proto.RegisterGreeterHandler(service.Server(), new(Greeter))

	// 运行服务
	if err := service.Run(); err != nil {
		fmt.Println(err)
	}
}
复制代码

执行

go run main.go
复制代码

输出

2016/03/14 10:59:14 Listening on [::]:50137
2016/03/14 10:59:14 Broker Listening on [::]:50138
2016/03/14 10:59:14 Registering node: greeter-ca62b017-e9d3-11e5-9bbb-68a86d0d36b6
复制代码

这样一个简单的微服务就完成了

客户端

若要访问其他微服务, 就要使用微服务客户端, 上面我们生成的proto原型文件中包含了客户端部分,这样可以减少模板代码量。在创建客户端时,有许多其他选项如选择器(selector)、过滤(filter)、传输(transport)、编码(codec)、负载均衡(Load Balancing)、包装器(Wrappers)等等, 后续博客将会介绍,我们这里创建一个最简单的客户端

package main

import (
	"context"
	"fmt"

	micro "github.com/micro/go-micro"
	proto "github.com/micro/examples/service/proto"
)


func main() {
	// 定义服务,可以传入其它可选参数
	service := micro.NewService(micro.Name("greeter.client"))
	service.Init()

	// 创建新的客户端
	greeter := proto.NewGreeterService("greeter", service.Client())

	// 调用greeter
	rsp, err := greeter.Hello(context.TODO(), &proto.HelloRequest{Name: "Pengju"})
	if err != nil {
		fmt.Println(err)
	}

	// 打印响应请求
	fmt.Println(rsp.Greeting)
}

复制代码

执行

go run client.go
复制代码

输出

Hello Pengju
复制代码

发布/订阅

发布/订阅是非常常见的设计模式, 在micro中使用发布/订阅也非常简单而且极具拓展性。Go-micro 给事件驱动架构内置了消息代理(broker)接口。发送消息时, 消息就像rpc一样会自动编/解码并通过代理发送, 默认的代理是http, 可以通过go-plugins,拓展自己喜欢的代理方式

  • 更改broker代理

启动时追加参数--broker=nats --broker_address=localhost:4222或配置环境MICRO_BROKER=nats MICRO_BROKER_ADDRESS=localhost:4222

  • 自定义broker代理

可参照 micro/go-plugins 库,目前已完成的有: http(默认)、grpc、kafka、mqtt、nats、rabbitmq、redis等等

  • 发布

编写并编译proto

syntax = "proto3";

// Example message
message Event {
	// unique id
	string id = 1;
	// unix timestamp
	int64 timestamp = 2;
	// message
	string message = 3;
}

复制代码

创建发布器,传入topic主题名,及客户端

p := micro.NewPublisher("events", service.Client())

发布一条protobuf消息

p.Publish(context.TODO(), &proto.Event{
	Id:        uuid.NewUUID().String(),
	Timestamp: time.Now().Unix(),
	Message:   fmt.Sprintf("Messaging you all day on %s", topic),
})
复制代码
  • 订阅

创建消息处理器

func ProcessEvent(ctx context.Context, event *proto.Event) error {
	fmt.Printf("Got event %+v\n", event)
	return nil
}
复制代码

订阅消息

micro.RegisterSubscriber("events", ProcessEvent)
复制代码

函数式编程

Function是指接收一次请求,执行后便退出的服务,编写函数与服务基本没什么差别, 非常简单。

package main

import (
	"context"

	proto "github.com/micro/examples/function/proto"
	"github.com/micro/go-micro"
)

type Greeter struct{}

func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

func main() {
	// 创建新函数
	fnc := micro.NewFunction(
		micro.Name("greeter"),
	)

	// 初始化命令行
	fnc.Init()

	// 注册handler
	fnc.Handle(new(Greeter))

	// 运行服务
	fnc.Run()
}

复制代码

运行

go run main.go
复制代码

输出

2019/02/25 16:01:16 Transport [http] Listening on [::]:53445
2019/02/25 16:01:16 Broker [http] Listening on [::]:53446
2019/02/25 16:01:16 Registering node: greeter-fbc3f506-d302-4df3-bb90-2ae8142ca9d6

复制代码

使用客户端调用

// 创建新的客户端
service := micro.NewService(micro.Name("greeter.client"))
service.Init()

cli := proto.NewGreeterService("greeter", service.Client())

// 调用greeter
rsp, err := cli.Hello(context.TODO(), &proto.HelloRequest{Name: "Pengju"})
if err != nil {
	fmt.Println(err)
}

// 打印响应请求
fmt.Println(rsp.Greeting)
复制代码

或使用micro toolkit调用

micro call greeter Greeter.Hello '{"name": "Pengju"}'
复制代码

都会输出

{
	"greeting": "Hello Pengju"
}
复制代码

同时,Function也会退出

2019/02/25 16:07:41 Deregistering node: greeter-fbc3f506-d302-4df3-bb90-2ae8142ca9d6
复制代码

包装器(Wrappers)

Go-micro中有中间件即包装器的概念。客户端或者处理器可以使用装饰模式包装起来。下面以打印日志需求分别在服务端和客户端实现log wrapper。

  • 服务端(handler)
// 实现server.HandlerWrapper接口
func logWrapper(fn server.HandlerFunc) server.HandlerFunc {
	return func(ctx context.Context, req server.Request, rsp interface{}) error {
		fmt.Printf("[%v] server request: %s", time.Now(), req.Endpoint())
		return fn(ctx, req, rsp)
	}
}
复制代码

可以在创建服务时初始化

service := micro.NewService(
	micro.Name("greeter"),
	// 把handler包起来
	micro.WrapHandler(logWrapper),
)
复制代码
  • 客户端(client)
type logWrapper struct {
	client.Client
}

func (l *logWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
	fmt.Printf("[wrapper] client request to service: %s method: %s\n", req.Service(), req.Endpoint())
	return l.Client.Call(ctx, req, rsp)
}

// 实现client.Wrapper,充当日志包装器
func logWrap(c client.Client) client.Client {
	return &logWrapper{c}
}
复制代码

可以在创建客户端时初始化,以上面的Function调用为例

// 创建新的客户端
service := micro.NewService(micro.Name("greeter.client"), micro.WrapClient(logWrap))
service.Init()

cli := proto.NewGreeterService("greeter", service.Client())

// 调用greeter
rsp, err := cli.Hello(context.TODO(), &proto.HelloRequest{Name: "Pengju"})
if err != nil {
	fmt.Println(err)
}

// 打印响应请求
fmt.Println(rsp.Greeting)
复制代码

再次调用输出

[wrapper] client request to service: greeter method: Greeter.Hello
复制代码

选择器(selector)

假如greeter微服务现在启动了3个, 当有客户端进行rpc调用时, 默认情况下会使用随机处理过的哈希负载均衡机制去访问这三个服务实例, 假如我们想对其中某一个符合自定义条件的服务实例进行访问,就需要使用selector, 下面以firstNodeSelector为例, 实现客户端永远调用从服务发现取出来的第一个服务实例。要自定义selector非常简单,只需实现selector包下的Selector接口即可

type firstNodeSelector struct {
	opts selector.Options
}

// 初始化选择器
func (n *firstNodeSelector) Init(opts ...selector.Option) error {
	for _, o := range opts {
		o(&n.opts)
	}
	return nil
}

// selector 返回options
func (n *firstNodeSelector) Options() selector.Options {
	return n.opts
}

// 对从服务发现取出来的服务实例进行选择
func (n *firstNodeSelector) Select(service string, opts ...selector.SelectOption) (selector.Next, error) {
	services, err := n.opts.Registry.GetService(service)
	if err != nil {
		return nil, err
	}

	if len(services) == 0 {
		return nil, selector.ErrNotFound
	}

	var sopts selector.SelectOptions
	for _, opt := range opts {
		opt(&sopts)
	}

	for _, filter := range sopts.Filters {
		services = filter(services)
	}

	if len(services) == 0 {
		return nil, selector.ErrNotFound
	}

	if len(services[0].Nodes) == 0 {
		return nil, selector.ErrNotFound
	}

	return func() (*registry.Node, error) {
		return services[0].Nodes[0], nil
	}, nil
}

func (n *firstNodeSelector) Mark(service string, node *registry.Node, err error) {
	return
}

func (n *firstNodeSelector) Reset(service string) {
	return
}

func (n *firstNodeSelector) Close() error {
	return nil
}

// 返回selector的命名
func (n *firstNodeSelector) String() string {
	return "first"
}
复制代码

创建客户端时,添加选择器

cli := client.NewClient(
	client.Selector(FirstNodeSelector()),
)
复制代码

过滤器(filter)

与selector类似, 过滤器配置过滤出符合条件的服务实例, 过滤器相当于选择器的简化版本,下面以版本选择过滤器为例,实现过滤某个特定版本的服务实例

func Filter(v string) client.CallOption {
	filter := func(services []*registry.Service) []*registry.Service {
		var filtered []*registry.Service

		for _, service := range services {
			if service.Version == v {
				filtered = append(filtered, service)
			}
		}

		return filtered
	}

	return client.WithSelectOption(selector.WithFilter(filter))
}
复制代码

调用时添加过滤器

rsp, err := greeter.Hello(
	// provide a context
	context.TODO(),
	// provide the request
	&proto.HelloRequest{Name: "Pengju"},
	// set the filter
	version.Filter("latest"),
)
复制代码

本人学习golang、micro、k8s、grpc、protobuf等知识的时间较短,如果有理解错误的地方,欢迎批评指正,可以加我微信一起探讨学习