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

段鵬舉發表於2019-02-25

部落格目錄 牌類遊戲使用微服務重構筆記
上期部落格 牌類遊戲使用微服務重構筆記(二): 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等知識的時間較短,如果有理解錯誤的地方,歡迎批評指正,可以加我微信一起探討學習

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

相關文章