Micro In Action(二):项目结构与启动过程

本文作者:Che Dan,授权发布

原文链接:https://medium.com/@dche423/micro-in-action-part2-cn-9bbc33d356eb

本文是 Micro 系列文章的第二篇。我们将以实际开发微服务为主线,顺带解析相关功能。从最基本的话题开始,逐步转到高级特性。

项目结构

在上篇文章中我们创建了一个简单的项目, 并过将它运行起来。本篇将继续这个旅程,先介绍项目结构及其中每个文件的用途。

注: 由于本系列文章的主题是 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 的 约定 , 建议在这里管理所需 plugin 的导入, 后续会用到。
  • proto/hello/hello.proto ,gRPC 服务定义 文件, 定义了 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 可以指定多次,形成叠加效果(后面会提到)。

但是, 如此重要的选项竟 没有任何一份说明文档 ,想要学习只能去查看 源码 。而很多 Option 的源码中连注释也没有,这进一步提高了学习的难度。虽然本文并不打算成为完备的 Micro 参考手册,但这些 Option 对于理解和使用 Micro 非常重要,又没有其它资料可参考, 所以我决定列出 v1.18.0 版本中全部 25 个 Option。逐一加以说明:

  1. micro.Name(n string) Option , 指定服务名称。命名规则一般是“$namespace.$type.$name”。其中 namespace 代表项目的名称空间, type 代表服务类型(例如 gRPC 和 web),一般会把 gRPC service 类型缩写成 srv。服务实例运行后, 此名称将自动注册到 Registry, 成为服务发现的依据。默认为“go.micro.server”。 :因此此项必须要指定, 否则所有节点使用相同的默认名称,会导致调用混乱
  2. micro.Version(v string) Option ,指定服务版本。默认为启动时间格式化的字符串。恰当地选择版本号再配合相应的 Selector, 可以实现优雅的轮转升级、灰度发布、A/B 测试等功能。
  3. micro.Address(addr string) Option ,指定 gRPC 服务地址。 默认为随机端口。由于客户端是通过注册中心来定位服务, 所以随机端口并不影响使用。 但实践中经常是指定固定端口号的, 这会有利于运维管理和安全控制
  4. micro.RegisterTTL(t time.Duration) Option ,指定服务注册信息在注册中心的有效期。 默认为一分种
  5. micro.RegisterInterval(t time.Duration) Option ,指定服务主动向注册中心报告健康状态的时间间隔, 默认为 30 秒。 这两个注册中心相关的 Option 结合起来用,可以避免因服务意外宕机而未通知注册中心,产生“无效注册信息”
  6. micro.WrapHandler(w …server.HandlerWrapper) Option ,包装服务 Handler, 概念上类似于 Gin Middleware , 集中控制 Handler 行为。可包装多层,执行顺序由外到内(后续会有实例)
  7. micro.WrapSubscriber(w …server.SubscriberWrapper) Option ,与 WrapHandler 相似,不同之处在于它用来包装异步消费处理中的“订阅者”。
  8. micro.WrapCall(w …client.CallWrapper) Option ,包装客户端发起的每一次方法调用。
  9. micro.WrapClient(w …client.Wrapper) Option ,包装客户端,可包装多层, 执行顺序由内到外。
  10. micro.BeforeStart(fn func() error) Option ,设置服务启动前回调函数,可设置多个。
  11. micro.BeforeStop(fn func() error) Option ,设置服务关闭前回调函数,可设置多个。
  12. micro.AfterStart(fn func() error) Option ,设置服务启动后回调函数,可设置多个。
  13. micro.AfterStop(fn func() error) Option ,设置服务关闭后回调函数,可设置多个。
  14. micro.Action(a func(*cli.Context)) Option ,处理命令行参数。 支持子命令及控制标记。 详情请见 micro/cli
  15. micro.Flags(flags …cli.Flag) Option ,快捷支持命令行控制标记, 详情请见 micro/cli
  16. micro.Cmd(c cmd.Cmd) Option , 指定命令行处理对象。 默认由 newCmd 生成,此对象包含了一系列默认的环境变量、命令行参数支持。 可以看作是多个内置 cli.Flag 的集合。 : go-micro 框架对命令行处理的设计方案有利有弊。 利是提供大量默认选项,可以节省开发者时间。 弊是此设计对用户程序的 有强烈的侵入性 : 框架要求开发者必须以 micro/cli 统一要求的方式来处理命令行参数。如若不然, 程序会报错无法运行。 例如,我们运行 ./hello-srv --foo=bar 就会报出“ Incorrect Usage. flag provided but not defined: -foo=bar ”的错误。 好在有这个 Option,可以弥补这种强侵入性带来的弊端。假如一个现存项目想引入 Micro ,而它已经有自己的参数处理机制, 那么就需要使用此 Option 覆盖默认行为(同时丢掉一些默认的参数处理能力)。 关于命令行参数, 本文后面部分有进一步解释。
  17. micro.Metadata(md map[string]string) Option ,指定服务元数据。 元数据时常被用来为服务标记与分组, 实现特定的负载策略等
  18. micro.Transport(t transport.Transport) Option ,指定传输协议, 默认为 http 协议
  19. micro.Selector(s selector.Selector) Option ,指定节点选择器, 实现不同负载策略。默认为随机 Selector
  20. micro.Registry(r registry.Registry) Option ,指定用于服务发现的注册机制, 默认为基于 mDNS 的注册机制
  21. micro.Server(s server.Server) Option , 指定自定义 Server, 用于默认 Server 不满足业务要求的情况。默认为 rpcServer
  22. micro.HandleSignal(b bool) Option , 是否允许服务自动响应 TERM, INT, QUIT 等信号。默认为 true
  23. micro.Context(ctx context.Context) Option ,指定服务初始 Context,默认为 context.BackGround(),可用于控制服务生存期及其它
  24. micro.Client(c client.Client) Option ,指定对外调用的客户端。 默认为 rpcClient
  25. 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 源码。 本系列后续文章对此话题会作进一步解读。

创建之后就可以初始化服务了:

// 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 个典型的注册操作:

  1. 注册 gRPC handler。 创建 handler.Hello 对象, 并注册到 Server 上。由于 handler.Hello 实现了 HelloHandler 接口, 所以它才可以作为 hello.RegisterHelloHandler 的方法参数被传入,否则会报错。一个服务中可以注册多个 Handler 以完成不同业务功能。
  2. 注册消息处理对象。 第一个参数为消息 Topic, 第二个参数是 Server, 第三个参数是消息处理对象。
  3. 注册消息处理函数。与对象注册相似, 只是第三个参数是对应的消息处理函数

关于消息处理的更多细节, 我们将在后续文章中专门说明。

运行服务

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 web
我来评几句
登录后评论

已发表评论数()

相关站点

热门文章