> 本文作者:Che Dan,授权发布
>
> 原文链接:https://medium.com/@dche423/micro-in-action-part2-cn-9bbc33d356eb
![](https://s1.ax1x.com/2020/03/13/8MpEBn.png)
本文是[Micro](https://micro.mu/)系列文章的第二篇。我们将以实际开发微服务为主线,顺带解析相关功能。从最基本的话题开始,逐步转到高级特性。
---
## 项目结构
在上篇文章中我们创建了一个简单的项目, 并过将它运行起来。本篇将继续这个旅程,先介绍项目结构及其中每个文件的用途。
**注**: 由于本系列文章的主题是 Micro,所以不会讨论无关话题, 例如:项目布局的最佳实践、如何连接数据库、如何依赖注入(如果对此感兴趣,可以看我的 《[Go:一文读懂 Wire](https://studygolang.com/articles/27163#reply0)》)等。 因此我们只是原样解释项目文件, 不对其作无关调整。
项目结构如下:
```bash
.
├── 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 的[约定](https://micro.mu/docs/plugins.html#usage), 建议在这里管理所需 plugin 的导入, 后续会用到。
- **proto/hello/hello.proto**,gRPC [服务定义](https://grpc.io/docs/guides/concepts/)文件, 定义了 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**:
```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 和运行服务。
### 导入依赖
这部分只有一行代码值得单独说明:
```go
hello "hello/proto/hello"
```
导入时定义了别名。 这也是 Micro 的一个习惯约定:对所有接口导入包设置别名。 这样就可以避免依赖导入代码的包名。 实践中, 如果不作特别设置,自动生成代码的包名会比较长, 以 **hello.pb.go** 为例, 它的包名是 `com_foo_srv_hello。`显然设置一个别名是更好的选择
### 创建及初始化服务
```go
// 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 可以指定多次,形成叠加效果(后面会提到)。
但是, 如此重要的选项竟**没有任何一份说明文档**,想要学习只能去查看[源码](https://github.com/micro/go-micro/blob/v1.18.0/options.go)。而很多 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](https://github.com/gin-gonic/gin#using-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](https://github.com/micro/cli)
15. **micro.Flags(flags …cli.Flag) Option**,快捷支持命令行控制标记, 详情请见 [micro/cli](https://github.com/micro/cli)
16. **micro.Cmd(c cmd.Cmd) Option**, 指定命令行处理对象。 默认由 [newCmd](https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263)生成,此对象包含了一系列默认的环境变量、命令行参数支持。 可以看作是多个内置 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,便可以高度定制服务的行为。 例如要想修改注册信息有效期:
```go
...
// 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](https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263) 源码。 本系列后续文章对此话题会作进一步解读。
创建之后就可以初始化服务了:
```go
// Initialize service
service.Init()
```
**service.Init** 方法可以接收与 **micro.NewService** 相同的参数。 所以上述 25 个 Option 也可以用在 **service.Init**方法中。 他们效果相同只是时机有差异。由于此时服务已经创建, 我们可以使用服务实例的某些信息。例如,可自动读取随机端口:
```go
// 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
```go
// 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. 注册消息处理函数。与对象注册相似, 只是第三个参数是对应的消息处理函数
关于消息处理的更多细节, 我们将在后续文章中专门说明。
### 运行服务
```go
if err := service.Run(); err != nil {
log.Fatal(err)
}
```
至此, 服务便真正运行起来了
---
## 查看运行时状态
[上一篇文章](https://studygolang.com/articles/27111)提到, `micro` 这个命令行工具可以用来在运行时查看和操作服务。下面我们来试一下。
在服务启动之后, 运行 `micro web`命令:
```bash
$ 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 将能以网页形式查看服务状态。截图如下:
![](https://s1.ax1x.com/2020/03/13/8Kz6sA.png)
从上图中, 我们可以看到该服务的各种关键信息:
- 服务名称。
- 服务节点列表。 如果此服务有多个节点同时运行, 此处会看到多行
- 每个节点中显示了版本号, 名称,编一 ID,地址,元数据等
- Endpoints。服务的接口定义, 方法名,参数结构与数据类型等等
可见通过 `micro web` 可以很方便的了解各种运行时状态。 你可能会问, 我们的服务与 `micro web` 之间并没有互相调用, 它是怎么知道这些信息的呢? 答案在于前文提到的**服务发现。** Micro 内置支持服务发现, 在未作特别设置的情况下, 默认的服务发现是基于 mDNS 的, 因此只要在同一个局域内, 就可以自动发现彼此。
当然 `micro web` 的功能不只于此,我们只是展现与本篇主题相关的内容。 后续文章会展开介绍。
---
## 总结
本文是 Micro in Action 系列的第二篇文章, 我们作了几件事:
1. 介绍了上篇文章所创建的项目结构, 说明每一个文件的用途。
2. 对照源码逐行分析一个 Micro 服务的启动过程。
3. 考虑到 Micro 文档的缺失, 本文完整介绍了创建 Micro 服务所支持的全部 Option
4. 最后用 `micro web` 查看了服务的运行时状态
有疑问加站长微信联系(非本文作者))