牌類遊戲使用微服務重構筆記(三): micro框架簡介 go-micro

博客目錄 牌類遊戲使用微服務重構筆記
上期博客 牌類遊戲使用微服務重構筆記(二): micro框架簡介:micro toolkitnode

micro與go-micro

上文講過,micro是個toolkit工具包,主要用於開發、調試、部署、運維、api網關等,而go-micro纔是咱們代碼中常常使用到的項目git

以前的helloworld example裏咱們已經使用過go-micro了github

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不依賴其餘組件,能夠當作本地開發的服務發現方式golang

  • 更改服務發現

    啓動微服務時追加參數--registry=consul --registry_address=localhost:8500或配置環境MICRO_REGISTRY=consul MICRO_REGISTRY_ADDRESS=localhost:8500便可, 若是更改了服務發現方式,須要重啓micro api網關,參數一致,不然沒法讀取服務列表redis

  • 自定義服務發現

    micro中服務發現是很好拓展的,可使用插件實現本身的服務發現方式,例如:etcd, kubernetes, zookeeper, nats, redis等,可參照 micro/go-plugins 庫編程

Protobuf

微服務中有個關鍵需求點,就是接口的強定義。Micro使用protobuf來完成這個需求。下面定義一個微服務Greeter,有一個Hello方法。它有HelloRequest入參對象及HelloResponse出參對象,兩個對象都有一個字符串類型的參數。設計模式

安裝protoc micro插件
api

go get github.com/micro/protoc-gen-micro
複製代碼

編寫proto
bash

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等知識的時間較短,若是有理解錯誤的地方,歡迎批評指正,能夠加我微信一塊兒探討學習

相關文章
相關標籤/搜索