本文作者: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
每个文件的说明为:
//go:generate make proto
,实现与 go generate 命令的集成。在运行 go generate
命令时自动调用 make proto
Hello
,服务中提供 3 种典型 gRPC 调用: 单向 RPC,单向 Stream 和双向 Stream protoc
生成 gRPC 相关代码 protoc-gen-micro
生成的, 进一步简化开发者的工作。其中定义了 HelloSerivce
接口, 以及 HelloHandler
接口。后者是我们需要去实现、完成业务逻辑的接口 注: 文件夹 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。逐一加以说明:
./hello-srv --foo=bar
就会报出“ Incorrect Usage. flag provided but not defined: -foo=bar
”的错误。 好在有这个 Option,可以弥补这种强侵入性带来的弊端。假如一个现存项目想引入 Micro ,而它已经有自己的参数处理机制, 那么就需要使用此 Option 覆盖默认行为(同时丢掉一些默认的参数处理能力)。 关于命令行参数, 本文后面部分有进一步解释。 因此,通过在创建时指定恰当的 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 }), )
// 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 个典型的注册操作:
关于消息处理的更多细节, 我们将在后续文章中专门说明。
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 将能以网页形式查看服务状态。截图如下:
从上图中, 我们可以看到该服务的各种关键信息:
可见通过 micro web
可以很方便的了解各种运行时状态。 你可能会问, 我们的服务与 micro web
之间并没有互相调用, 它是怎么知道这些信息的呢? 答案在于前文提到的 服务发现。
Micro 内置支持服务发现, 在未作特别设置的情况下, 默认的服务发现是基于 mDNS 的, 因此只要在同一个局域内, 就可以自动发现彼此。
当然 micro web
的功能不只于此,我们只是展现与本篇主题相关的内容。 后续文章会展开介绍。
本文是 Micro in Action 系列的第二篇文章, 我们作了几件事:
micro web