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

polaris · · 2136 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

> 本文作者: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` 查看了服务的运行时状态

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2136 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传