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

段鵬舉發表於2019-02-21

作者簡介

Asim Aslam, 前谷歌工程師,領英地址。Technology is rapidly evolving. Cloud computing now gives us almost unlimited scale, however leveraging that scale with existing tools is still difficult. Micro is solving this problem with a developer first focus.

Asim在宣傳視訊中說因為在谷歌工作6年的經歷,他對proto、grpc、golang是非常熟悉的,micro這個框架也做的比較完善,許多企業都在用,所以個人比較放心,而且看這個髮量,應該是也是很靠譜的。。。

我從學習到使用的時間也不長,有一些關於作者的客觀評價

  • Asim目前在全職做micro這個專案,註冊了一家公司也叫micro,有商業版本(官網), 認真的在賺錢,開源版和商業版的區別
  • 無論是micro這個專案還是公司,貌似都只是Asim一個人
  • github上的issue解決的非常快,給我的感覺他是24小時線上的,但是他特別喜歡關閉issue,open狀態的issue幾乎看不到
  • 版本變化比較快
  • 文件比較少且不細緻,坑不少(可能也是筆者懂的知識太少了)

helloWorld

先寫proto, 命名為greeter.proto

syntax = "proto3";

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

message HelloRequest {
	string name = 1;
}

message HelloResponse {
	string greeting = 2;
}

複製程式碼

編譯proto
protoc -I . --go_out=. --micro_out=. proto/greeter.proto
會生成兩個檔案,greeter.pb.go 原生proto go檔案,greeter.micro.go 微服務proto go檔案

微服務程式碼

package main

import (
	"context"
	"log"

	"github.com/micro/go-micro"
	// 引用上面生成的proto檔案
	proto "micro-blog/helloworld/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() {
	// new一個微服務出來
	service := micro.NewService(
		micro.Name("greeter"),
		micro.Version("latest"),
	)

	// 可選 解析命令列
	service.Init()

	// 註冊 handler
	proto.RegisterGreeterHandler(service.Server(), new(Greeter))

	// 啟動服務
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

複製程式碼

啟動微服務 go run main.go
這樣一個最簡單的微服務就ok了,是不是非常簡單,我們再嘗試一下呼叫

package main

import (
	"context"
	"fmt"

	// 引用上面生成的proto檔案
	"github.com/micro/go-micro"
	proto "micro-blog/helloworld/proto"
)

func main() {
	// new一個服務
	service := micro.NewService()

	// 解析命令列flag
	service.Init()

	// 使用proto建立一個客戶端
	cl := proto.NewGreeterService("greeter", service.Client())

	// 發出請求
	rsp, err := cl.Hello(context.Background(), &proto.HelloRequest{
		Name: "John",
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(rsp.Greeting)
}

複製程式碼

輸出 Hello John
可以看出,呼叫微服務也是非常簡單的,這是因為micro把許多東西都幫我們做好了,後面我們逐漸去了解

安裝micro

micro是一個工具包toolkit合集,能幫助我們快速開發、除錯微服務 本文使用的micro版本是v0.22.0,其他版本可能會有不同之處
執行go get -u github.com/micro/micro即可安裝,micro依賴的東西比較多,建議"fanqiang"
測試安裝 micro --version

micro工具包介紹

  • micro cli 互動模式

micro cli
進入互動模式,互動模式下的命令與下文中的命令類似但可能不同,具體檢視help
如果要連線遠端環境,可以使用代理,在遠端機器上執行 micro proxy
在本機上執行MICRO_PROXY_ADDRESS=staging.micro.mu:8081 micro cli

例如筆者在公司開發時會把micro執行在桌上型電腦上,然後在自己的筆記本上敲程式碼

  • 列出服務

micro list services
如果你還執行著上文的helloworld服務,會輸出greeter

  • 獲取服務

micro get service [servicename]獲取某個服務的詳細資訊

例如micro get service greeter,會輸出:

service  greeter # 服務名上文填寫的

version latest # 服務的版本號

# 詳細資訊 服務id、服務所在機器的ip、埠號、協議、註冊方式、transport、broker等等,如果你再啟動一個相同的服務程式,下面會出現兩條記錄
ID	Address	Port	Metadata
greeter-8f9ea45b-d8ce-43f6-97b5-a41d8514cd79	192.168.1.107	65217	protocol=mucp,registry=mdns,server=rpc,transport=http,broker=http

# 可以理解為一個rpc介面
Endpoint: Greeter.Hello
Metadata: stream=false

# Greeter.Hello請求結構
Request: {
	name string
	- 
	- []uint8 {
		uint8 uint8
	}
	- int32
}

# Greeter.Hello響應結構
Response: {
	greeting string
	- 
	- []uint8 {
		uint8 uint8
	}
	- int32
}
複製程式碼
  • 呼叫服務

micro call [servicename] [endpoint] [data]立即呼叫某個服務的某個方法

例如micro call greeter Greeter.Hello '{"name": "John"}',會輸出

{
	"greeting": "Hello John"
}
複製程式碼

與程式碼裡呼叫的結果一致,其實無論是micro cli裡呼叫還是程式碼裡面呼叫,過程是一樣的,後面會有詳細解釋

  • 服務健康檢測

micro health [servicename]獲取某個服務的健康情況
例如micro health greeter, 會輸出

service  greeter

version latest

node		address:port		status
greeter-8f9ea45b-d8ce-43f6-97b5-a41d8514cd79		192.168.1.107:65217		ok
複製程式碼
  • 快速生成服務模板

micro new [servicename] [arguments...]新寫一個服務時可以使用這個命令快速生成模板,預設會生成在$GOPATH的相對目錄

例如micro new test --gopath=false,會輸出

Creating service go.micro.srv.test in test

.
├── main.go
├── plugin.go
├── handler
│   └── example.go
├── subscriber
│   └── example.go
├── proto/example
│   └── example.proto
├── Dockerfile
├── Makefile
└── README.md


download protobuf for micro:

brew install protobuf
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
go get -u github.com/micro/protoc-gen-micro

compile the proto file example.proto:

cd test
protoc --proto_path=. --go_out=. --micro_out=. proto/example/example.proto

複製程式碼
  • Web管理控制檯

micro web [arguments...] 啟動web管理介面,在瀏覽器中輸入localhost:8082即可開啟
管理介面提供了許多類似於cli的功能,更加方便、直觀, micro web本身也是個微服務
檢視web資源

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

呼叫服務

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

檢視服務詳細資訊

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

互動式命令列

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

  • API閘道器

micro api [arguments...] 詳見下文, micro api本身也是個微服務

API閘道器

  • 一切皆服務

在micro的系統中,有許多資源型別,筆者理解的是服務的一種抽象歸類,比如常用的有:api、fnc(函式)、srv、web。這些資源在被定義時都是以服務的方式進行的,上文helloworld中的服務"greeter" 只是最簡單的服務,如何進行資源歸類呢?答案就是增加字首,比如"greeter"屬於一個後端服務,我們把它定義為"srv.greeter",這樣micro就知道了這個服務是srv分類,修改程式碼

// new一個微服務出來
	service := micro.NewService(
		micro.Name("srv.greeter"),
		micro.Version("latest"),
	)
複製程式碼
  • 名稱空間

在服務的型別歸類好之後,有時候可能需要根據專案、服務特點等再次進行歸類,這時候就需要名稱空間了,例如修改"srv.greeter"為"proj1.srv.greeter",表示這個資源屬於"proj1"這個名稱空間下,預設的名稱空間是"go.micro"

  • 三層架構

在micro中,推薦以三層架構方式組織眾多的微服務

micro api: http訪問入口

some api: 對外暴露的API服務

some srv: 內網的後臺服務
複製程式碼

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

  • 啟動api閘道器

micro api 即可啟動api一個閘道器,預設的埠是8080
可以通過--address=0.0.0.0:8080flag或者設定環境MICRO_API_ADDRESS=0.0.0.0:8080來修改

  • 使用ACME協議

ACME( Automatic Certificate Management Environment)是由Let’s Encrypt制定的安全協議 通過--enable_acme=true或者設定環境MICRO_ENABLE_ACME=true

可以選擇是否配置白名單
--acme_hosts=example.comMICRO_ACME_HOSTS=example.com,api.example.com

  • 設定TLS證書

micro --enable_tls=true --tls_cert_file=/path/to/cert --tls_key_file=/path/to/key apiMICRO_ENABLE_TLS=true MICRO_TLS_CERT_FILE=/path/to/cert MICRO_TLS_KEY_FILE=/path/to/key micro api

  • 設定名稱空間

micro --api_namespace=namespace apiMICRO_API_NAMESPACE=namespace micro api
注意啟動api時設定的namespace必須與要訪問的資源的namespace一致不然無法訪問,Web管理控制檯類似

  • 直接訪問服務

通過/rpc這個固定url可以繞過rpc處理器直接對服務進行訪問,例如

curl -d 'service=go.micro.srv.greeter' \
     -d 'endpoint=Greeter.Hello' \
     -d 'request={"name": "Bob"}' \
     http://localhost:8080/rpc
複製程式碼

會輸出

{"greeting":"Hello Bob"}
複製程式碼

但是不推薦這麼使用,因為使用micro一般都是三層架構,可以在開發除錯階段這麼使用。如果要禁用rpc呼叫方式,需要使用go-plugin外掛,後文會有介紹

  • 使用處理器訪問服務

所謂處理器,筆者理解的就是通過api閘道器訪問service時處理http請求的方式。micro提供了幾種處理器,上文通過/rpc這個固定路由就是繞過處理器直接使用傳送的json序列化請求進而訪問服務。使用處理器一般用於上文提過的三層架構,處理器提供一層api服務,進而再訪問其他後端微服務

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

    - /[service]/[method]	# HTTP paths are dynamically mapped to services
    - /rpc			# Explicitly call a backend service by name and method
複製程式碼
  • api處理器

API是預設的處理器,接收http請求,把http請求/響應資訊序列化成api.Request/api.Response格式

Content-Type: Any
Body: Any
Forward Format: api.Request/api.Response
Path: /[service]/[method]
Resolver: 請求解析器,路徑會被解析成服務與方法
Configure: 配置,在啟動時指定--handler=api或在啟動命令前指定環境變數MICRO_API_HANDLER=api

例如,將上文中的helloworld服務,補一個api結構

package main

import (
	"encoding/json"
	"log"
	"strings"

	// 引用上面生成的proto檔案
	"github.com/micro/go-micro"
	"github.com/micro/go-micro/errors"
	api "github.com/micro/go-api/proto"
	proto "micro-blog/helloworld/proto"

	"context"
)

type Say struct {
	Client proto.GreeterService
}

func (s *Say) Hello(ctx context.Context, req *api.Request, rsp *api.Response) error {
	log.Print("Received Say.Hello API request")

	name, ok := req.Get["name"]
	if !ok || len(name.Values) == 0 {
		return errors.BadRequest("go.micro.api.greeter", "Name cannot be blank")
	}

	response, err := s.Client.Hello(ctx, &proto.HelloRequest{
		Name: strings.Join(name.Values, " "),
	})
	if err != nil {
		return err
	}

	rsp.StatusCode = 200
	b, _ := json.Marshal(map[string]string{
		"message": response.Greeting,
	})
	rsp.Body = string(b)

	return nil
}

func main() {
	// new一個微服務出來 資源型別設定為api
	service := micro.NewService(
		micro.Name("go.micro.api.greeter"),
	)

	// 可選 解析命令列
	service.Init()

	// 註冊handler
	service.Server().Handle(
		service.Server().NewHandler(
			&Say{Client: proto.NewGreeterService("go.micro.srv.greeter", service.Client())},
		),
	)

	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

複製程式碼

通過api閘道器進行呼叫

curl -H 'Content-Type: application/json' \
    -s "http://localhost:8080/greeter/say/hello?name=Pengju"
複製程式碼

會輸出:

{
    "message": "Hello Pengju"
}
複製程式碼

greeter/say/hello被解析成傳送到service=go.micro.srv.greeter endpoint=Greeter.Hello的請求, 請求結構被解析成api.Request, 使用proto提供的方法就可以獲取本次http請求的資訊,例如name, ok := req.Get["name"]獲取查詢引數"name"

  • rpc 處理器

與api處理器類似, 不同的是序列化資料的結構可以自定義指定 例如使用下面的proto

Content-Type: application/json or application/protobuf
Body: JSON 或者 Protobuf
Forward Format: json-rpc或者proto-rpc,與Content-Type有關
Path: /[service]/[method]
Resolver: 請求解析器,路徑會被解析成服務與方法
Configure: 配置,在啟動時指定--handler=rpc或在啟動命令前指定環境變數MICRO_API_HANDLER=rpc

syntax = "proto3";

service Example {
	rpc Call(CallRequest) returns(CallResponse) {};
}

service Foo {
	rpc Bar(EmptyRequest) returns(EmptyResponse) {};
}

message CallRequest {
	string name = 1;
}

message CallResponse {
	string message = 2;
}

message EmptyRequest {
}

message EmptyResponse {
}

複製程式碼

更改helloworld api的程式碼

package main

import (
	"log"

	"github.com/micro/go-micro"
	proto "micro-blog/helloworld/api/rpc/proto"
	greeter "micro-blog/helloworld/proto"

	"context"
)

type Greeter struct {
	Client greeter.GreeterService
}

func (g *Greeter) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
	log.Print("Received Greeter.Hello API request")

	// make the request
	response, err := g.Client.Hello(ctx, &greeter.HelloRequest{Name: req.Name})
	if err != nil {
		return err
	}

	// set api response
	rsp.Msg = response.Greeting
	return nil
}

func main() {
	// Create service
	service := micro.NewService(
		micro.Name("go.micro.api.greeter"),
	)

	// Init to parse flags
	service.Init()

	// Register Handlers
	proto.RegisterGreeterHandler(service.Server(), &Greeter{
		// Create Service Client
		Client: greeter.NewGreeterService("go.micro.srv.greeter", service.Client()),
	})

	// for handler use

	// Run server
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

複製程式碼

發起呼叫

curl -H 'Content-Type: application/json' \
         -X POST \
         -d "{\"name\": \"PengJu\"}" \
         http://localhost:8080/greeter/greeter/hello

複製程式碼

輸出

{"msg":"Hello PengJu"}
複製程式碼

可以看到,請求已按照自定義的proto進行序列化。有個文件沒提的細節是,api型別的服務使用handler時,服務的endpoint是類名首字母小寫 + "." + 方法名首字母小寫, 如上文中的 Greeter的Hello方法,那麼整個路由就是http://localhost:8080/greeter/greeter/hello,第一個greeter是服務名,micro.Name("go.micro.api.greeter") 裡的

  • web處理器

http反向代理,支援web socket,只會匹配Path: /[service]這一層,剩下的就都交給開發者自己了,你可以使用自己喜歡的web框架,自定義中介軟體等等好處, 比較自由,也是筆者最終選擇的處理器

Content-Type: 支援任何型別
Body: 支援任何格式
Forward Format: HTTP反向代理,包括web socket
Path: /[service]
Resolver: 請求解析器,路徑會被解析成服務名
Configure: 配置,在啟動時指定--handler=web或在啟動命令前指定環境變數MICRO_API_HANDLER=web

比如使用gin

package main

import (
	"log"

	"github.com/gin-gonic/gin"

	"context"
	"github.com/micro/go-micro/client"
	"github.com/micro/go-web"
	proto "micro-blog/helloworld/proto"
)

type Say struct{}

var (
	cl proto.GreeterService
)

func (s *Say) Anything(c *gin.Context) {
	log.Print("Received Say.Anything API request")
	c.JSON(200, map[string]string{
		"message": "Hi, this is the Greeter API",
	})
}

func (s *Say) Hello(c *gin.Context) {
	log.Print("Received Say.Hello API request")

	name := c.Param("name")

	response, err := cl.Hello(context.TODO(), &proto.HelloRequest{
		Name: name,
	})

	if err != nil {
		c.JSON(500, err)
	}

	c.JSON(200, response)
}

func main() {
	// Create service 這裡需要注意使用的web.NewService 而不是micro.NewService 後文會有解釋
	service := web.NewService(
		web.Name("go.micro.api.greeter"),
	)

	service.Init()

	// setup Greeter Server Client
	cl = proto.NewGreeterService("go.micro.srv.greeter", client.DefaultClient)

	// Create RESTful handler (using Gin)
	say := new(Say)
	router := gin.Default()
	router.GET("/greeter", say.Anything)
	router.GET("/greeter/:name", say.Hello)

	// Register Handler
	service.Handle("/", router)

	// Run server
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

複製程式碼

發起呼叫

curl -H 'Content-Type: application/json' \
    -s "http://localhost:8080/greeter/Pengju"
複製程式碼

輸出

{"greeting":"Hello Pengju"}
複製程式碼

本人學習golang、micro、k8s、grpc、protobuf等知識的時間較短,如果有理解錯誤的地方,歡迎批評指正,可以加我微信一起探討學習

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

相關文章