阅读 1788

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

作者简介

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资源

调用服务

查看服务详细信息

交互式命令行

  • 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: 内网的后台服务
复制代码

  • 启动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服务,进而再访问其他后端微服务

    - /[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等知识的时间较短,如果有理解错误的地方,欢迎批评指正,可以加我微信一起探讨学习

关注下面的标签,发现更多相似文章
评论