点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言
本文作者:Che Dan,授权发布
原文链接:https://medium.com/@dche423/micro-in-action-part2-cn-9bbc33d356eb
本文是Micro[1]系列文章的第二篇。我们将以实际开发微服务为主线,顺带解析相关功能。从最基本的话题开始,逐步转到高级特性。
项目结构
在上篇文章中我们创建了一个简单的项目, 并过将它运行起来。本篇将继续这个旅程,先介绍项目结构及其中每个文件的用途。
注:由于本系列文章的主题是 Micro,所以不会讨论无关话题, 例如:项目布局的最佳实践、如何连接数据库、如何依赖注入(如果对此感兴趣,可以看我的 《Go:一文读懂 Wire》)等。因此我们只是原样解释项目文件, 不对其作无关调整。
项目结构如下:
.
├── main.go
├── generate.go
├── plugin.go
├── proto/hello
│ └── hello.proto
│ └── hello.pb.go
│ └── hello.pb.micro.go
├── handler
│ └── hello.go
├── subscriber
│ └── hello.go
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
└── README.md
每个文件的说明为:
- main.go ,项目主文件,后面会详细说明
- generate.go ,只包含一行
//go:generate make proto
,实现与 go generate 命令的集成。在运行go generate
命令时自动调用make proto
- plugins.go,目前是空文件, 根据 Micro 的约定[2], 建议在这里管理所需 plugin 的导入, 后续会用到。
- proto/hello/hello.proto,gRPC 服务定义[3]文件, 定义了 rpc 服务
Hello
,服务中提供 3 种典型 gRPC 调用:单向 RPC,单向 Stream 和双向 Stream - **proto/hello/hello.pb.go,**根据上述 proto 文件, 由
protoc
生成 gRPC 相关代码 - proto/hello/hello.pb.micro.go,由前文提到的
protoc-gen-micro
生成的, 进一步简化开发者的工作。其中定义了HelloSerivce 接口, 以及 HelloHandler 接口。后者是我们需要去实现、完成业务逻辑的接口 - handler/hello.go ,实现 gRPC 业务逻辑的地方。其中定义了 Hello 对象, 此对象实现了前面提到 HelloHandler 接口。
- subscriber/hello.go,实现异步消息接收并处理的地方。其中展示了用两种不同方式处理消息,一是以对象方法处理, 二是以一个函数来处理。
- Dockerfile,定义如何构建 Docker 镜像
- go.mod / go.sum , Go Module 相关文件
- Makefile,包含了几个常用任务定义, 编译、测试、生在 Docker 镜像等
- README.md,记录了生成项目的基本信息,以及基本运行指南
注:文件夹 proto有特殊含义。虽然在技术上没有限制, 但在 Micro 的约定中,每个项目根目录下的proto文件夹专门用来存放“接口”文件。这既包含本项目需要对外暴露的接口, 也包含本项目所依赖其它接口。举例来说, 假如我们实现业务逻辑时需要依赖另外一个服务 foo。那么我们会建立proto/foo 文件夹,并在其中放置 foo.proto, foo.pb.go, foo.pb.micro.go 三个文件,供业务代码调用。
启动过程解析
接下来看一看启动代码,main.go:
package main
import (
"github.com/micro/go-micro/util/log"
"github.com/micro/go-micro"
"hello/handler"
"hello/subscriber"
hello "hello/proto/hello"
)
func main() {
// New Service
service := micro.NewService(
micro.Name("com.foo.srv.hello"),
micro.Version("latest"),
)
// Initialise service
service.Init()
// Register Handler
hello.RegisterHelloHandler(service.Server(), new(handler.Hello))
// Register Struct as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), new(subscriber.Hello))
// Register Function as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler)
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
代码大体分 4 个部分,分别是导入依赖、创建及初始化服务、注册业务处理 Handler 和运行服务。
导入依赖
这部分只有一行代码值得单独说明:
hello "hello/proto/hello"
导入时定义了别名。这也是 Micro 的一个习惯约定:对所有接口导入包设置别名。这样就可以避免依赖导入代码的包名。实践中, 如果不作特别设置,自动生成代码的包名会比较长, 以 hello.pb.go 为例, 它的包名是 com_foo_srv_hello。
显然设置一个别名是更好的选择
创建及初始化服务
// New Service
service := micro.NewService(
micro.Name("com.foo.srv.hello"),
micro.Version("latest"),
)
创建服务用到了 micro.NewService(opts …Option) Service
方法。此方法可接收多个 micro.Option
为参数, 生成并返回 micro.Service
接口实例。
可见 micro.Option
是控制服务的关键。示例代码用 Option 分别指定了服务的名称和版本号。目前共有 25 个 Option 可供使用, 能够控制服务的方方面面。有些 Option 可以指定多次,形成叠加效果(后面会提到)。
但是, 如此重要的选项竟没有任何一份说明文档,想要学习只能去查看源码[4]。而很多 Option 的源码中连注释也没有,这进一步提高了学习的难度。虽然本文并不打算成为完备的 Micro 参考手册,但这些 Option 对于理解和使用 Micro 非常重要,又没有其它资料可参考, 所以我决定列出 v1.18.0 版本中全部 25 个 Option。逐一加以说明:
- micro.Name(n string) Option , 指定服务名称。命名规则一般是“type.$name”。其中 namespace 代表项目的名称空间, type 代表服务类型(例如 gRPC 和 web),一般会把 gRPC service 类型缩写成 srv。服务实例运行后, 此名称将自动注册到 Registry, 成为服务发现的依据。默认为“go.micro.server”。注:因此此项必须要指定, 否则所有节点使用相同的默认名称,会导致调用混乱
- micro.Version(v string) Option,指定服务版本。默认为启动时间格式化的字符串。恰当地选择版本号再配合相应的 Selector, 可以实现优雅的轮转升级、灰度发布、A/B 测试等功能。
- micro.Address(addr string) Option,指定 gRPC 服务地址。默认为随机端口。由于客户端是通过注册中心来定位服务, 所以随机端口并不影响使用。但实践中经常是指定固定端口号的, 这会有利于运维管理和安全控制
- micro.RegisterTTL(t time.Duration) Option,指定服务注册信息在注册中心的有效期。默认为一分种
- micro.RegisterInterval(t time.Duration) Option,指定服务主动向注册中心报告健康状态的时间间隔, 默认为 30 秒。这两个注册中心相关的 Option 结合起来用,可以避免因服务意外宕机而未通知注册中心,产生“无效注册信息”
- micro.WrapHandler(w …server.HandlerWrapper) Option,包装服务 Handler, 概念上类似于 Gin Middleware[5], 集中控制 Handler 行为。可包装多层,执行顺序由外到内(后续会有实例)
- micro.WrapSubscriber(w …server.SubscriberWrapper) Option,与 WrapHandler 相似,不同之处在于它用来包装异步消费处理中的“订阅者”。
- micro.WrapCall(w …client.CallWrapper) Option,包装客户端发起的每一次方法调用。
- micro.WrapClient(w …client.Wrapper) Option,包装客户端,可包装多层, 执行顺序由内到外。
- micro.BeforeStart(fn func() error) Option,设置服务启动前回调函数,可设置多个。
- micro.BeforeStop(fn func() error) Option,设置服务关闭前回调函数,可设置多个。
- micro.AfterStart(fn func() error) Option,设置服务启动后回调函数,可设置多个。
- micro.AfterStop(fn func() error) Option,设置服务关闭后回调函数,可设置多个。
- micro.Action(a func(*cli.Context)) Option,处理命令行参数。支持子命令及控制标记。详情请见 micro/cli[6]
- micro.Flags(flags …cli.Flag) Option,快捷支持命令行控制标记, 详情请见 micro/cli[7]
- micro.Cmd(c cmd.Cmd) Option, 指定命令行处理对象。默认由 newCmd[8]生成,此对象包含了一系列默认的环境变量、命令行参数支持。可以看作是多个内置 cli.Flag 的集合。注:go-micro 框架对命令行处理的设计方案有利有弊。利是提供大量默认选项,可以节省开发者时间。弊是此设计对用户程序的有强烈的侵入性:框架要求开发者必须以 micro/cli 统一要求的方式来处理命令行参数。如若不然, 程序会报错无法运行。例如,我们运行
./hello-srv --foo=bar
就会报出“Incorrect Usage. flag provided but not defined: -foo=bar”的错误。好在有这个 Option,可以弥补这种强侵入性带来的弊端。假如一个现存项目想引入 Micro ,而它已经有自己的参数处理机制, 那么就需要使用此 Option 覆盖默认行为(同时丢掉一些默认的参数处理能力)。关于命令行参数, 本文后面部分有进一步解释。 - micro.Metadata(md map[string]string) Option,指定服务元数据。元数据时常被用来为服务标记与分组, 实现特定的负载策略等
- micro.Transport(t transport.Transport) Option,指定传输协议, 默认为 http 协议
- micro.Selector(s selector.Selector) Option ,指定节点选择器, 实现不同负载策略。默认为随机 Selector
- micro.Registry(r registry.Registry) Option,指定用于服务发现的注册机制, 默认为基于 mDNS 的注册机制
- micro.Server(s server.Server) Option, 指定自定义 Server, 用于默认 Server 不满足业务要求的情况。默认为 rpcServer
- micro.HandleSignal(b bool) Option, 是否允许服务自动响应 TERM, INT, QUIT 等信号。默认为 true
- micro.Context(ctx context.Context) Option,指定服务初始 Context,默认为 context.BackGround(),可用于控制服务生存期及其它
- micro.Client(c client.Client) Option,指定对外调用的客户端。默认为 rpcClient
- micro.Broker(b broker.Broker) Option, 指定用于 发布/订阅 消息通讯的 Broker。默认为 http broker
因此,通过在创建时指定恰当的 Option,便可以高度定制服务的行为。例如要想修改注册信息有效期:
...
// New Service
service := micro.NewService(
micro.Name("foo.bar"),
micro.Version("v1.0"),
// change default TTL value
micro.RegisterTTL(5 * time.Minute),
...
)
...
注:上述大部分 Option 可以通过多种方式指定。在源码中硬编码只是几种其中之一。事实上, Micro 建议用户优先通过环境变量来指定某些 Option, 因为这样可以提供更大的灵活性。以micro.RegisterTTL 为例 , 我们可以在运行时通过环境变量 **$**MICRO_REGISTER_TTL
或者命令行参数 --register_ttl value
来指定(单位是秒)。运行 ./hello-srv -h
可以看到这些内置参数的简要说明。如果想了解全部细节,目前没有完整文档,需要自行查看 newCmd[9] 源码。本系列后续文章对此话题会作进一步解读。
创建之后就可以初始化服务了:
// Initialize service
service.Init()
service.Init 方法可以接收与 micro.NewService 相同的参数。所以上述 25 个 Option 也可以用在 service.Init方法中。他们效果相同只是时机有差异。由于此时服务已经创建, 我们可以使用服务实例的某些信息。例如,可自动读取随机端口:
// Initialize service
service.Init(
// print log after start
micro.AfterStart(func() error {
log.Infof("service listening on %s!",
service.Options().Server.Options().Address,
)
return nil
}),
)
注册业务处理 Handler
// Register Handler
hello.RegisterHelloHandler(service.Server(), new(handler.Hello))
// Register Struct as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), new(subscriber.Hello))
// Register Function as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler)
只有在完成 Handler 注册后, 我们的业务代码才能真正对外提供服务。这里展示了 3 个典型的注册操作:
- 注册 gRPC handler。创建handler.Hello对象, 并注册到 Server 上。由于handler.Hello实现了HelloHandler 接口, 所以它才可以作为 hello.RegisterHelloHandler 的方法参数被传入,否则会报错。一个服务中可以注册多个 Handler 以完成不同业务功能。
- 注册消息处理对象。第一个参数为消息 Topic, 第二个参数是 Server, 第三个参数是消息处理对象。
- 注册消息处理函数。与对象注册相似, 只是第三个参数是对应的消息处理函数
关于消息处理的更多细节, 我们将在后续文章中专门说明。
运行服务
if err := service.Run(); err != nil {
log.Fatal(err)
}
至此, 服务便真正运行起来了
查看运行时状态
上一篇文章提到, micro
这个命令行工具可以用来在运行时查看和操作服务。下面我们来试一下。
在服务启动之后, 运行 micro web
命令:
$ micro web
2020/01/15 18:13:25 : [web] HTTP API Listening on [::]:8082
2020/01/15 18:13:25 : [web] Transport [http] Listening on [::]:59005
2020/01/15 18:13:25 : [web] Broker [http] Connected to [::]:59006
2020/01/15 18:13:25 : [web] Registry [mdns] Registering node: go.micro.web-950a8b2b-003d-47c1-a512-53aedebc9d12
可见此命令已在本机 8082 端口上服务。注:8082 端口是默认值,可以通过环境变量或命令行参数修改。具体可以运行 micro web -h
查看说明
从浏览器访问 http://127.0.0.1:8082/registry?service=com.foo.srv.hello 将能以网页形式查看服务状态。截图如下:
从上图中, 我们可以看到该服务的各种关键信息:
- 服务名称。
- 服务节点列表。如果此服务有多个节点同时运行, 此处会看到多行
- 每个节点中显示了版本号, 名称,编一 ID,地址,元数据等
- Endpoints。服务的接口定义, 方法名,参数结构与数据类型等等
可见通过 micro web
可以很方便的了解各种运行时状态。你可能会问, 我们的服务与 micro web
之间并没有互相调用, 它是怎么知道这些信息的呢?答案在于前文提到的服务发现。 Micro 内置支持服务发现, 在未作特别设置的情况下, 默认的服务发现是基于 mDNS 的, 因此只要在同一个局域内, 就可以自动发现彼此。
当然 micro web
的功能不只于此,我们只是展现与本篇主题相关的内容。后续文章会展开介绍。
总结
本文是 Micro in Action 系列的第二篇文章, 我们作了几件事:
- 介绍了上篇文章所创建的项目结构, 说明每一个文件的用途。
- 对照源码逐行分析一个 Micro 服务的启动过程。
- 考虑到 Micro 文档的缺失, 本文完整介绍了创建 Micro 服务所支持的全部 Option
- 最后用
micro web
查看了服务的运行时状态
参考资料
[1]Micro: https://micro.mu/
[2]约定: https://micro.mu/docs/plugins.html#usage
[3]服务定义: https://grpc.io/docs/guides/concepts/
[4]源码: https://github.com/micro/go-micro/blob/v1.18.0/options.go
[5]Gin Middleware: https://github.com/gin-gonic/gin#using-middleware
[6]micro/cli: https://github.com/micro/cli
[7]micro/cli: https://github.com/micro/cli
[8]newCmd: https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263
[9]newCmd: https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263
推荐阅读
Micro In Action(一):入门
喜欢本文的朋友,欢迎关注“Go语言中文网”:
Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎