同学们好!前面我们曾经提到过,gRPC是一个非常轻量级的微服务框架。轻就轻在它只着意解决两个问题,一个是对远程过程调用的面向对象化封装,再一个就是数据的序列化和反序列化。但作为一个真正意义上的微服务框架,仅解决这两个问题,其实是远远不够的。比如我们的RPC客户机要想调用服务器上的远程方法,首先必须知道服务器的IP地址和通信端口,显式地建立TCP连接。这对于工作在分布式场景下的复杂应用而言,是不现实的。在这样的场景下,一个微服务只负责处理非常少量的业务,而一个复杂的业务可能需要多个这样的微服务协作完成。同时为了满足高可用和负载均衡的要求,处理每种业务的微服务通常也不会只部署在一台服务器上,而是采用集群式的冗余型部署方案。众多针对相同或不同业务的微服务,IP和端口各异,有的在线,有的离线,时而繁忙,时而空闲,生生灭灭,此消彼长。要让系统中的每台客户机了解所有这些信息,并时刻保持更新,这几乎是一件不可能完成的任务。由此看来,我们确实需要一个更加完善的,专门针对微服务场景的应用框架。今天我们要学习的go-micro框架就是其中之一。
首先我们先简单了解一些关于go-micro的基本常识,然后我们要学习微服务架构中一个非常重要的组成部分,服务注册与服务发现,并以Consul为例,了解服务注册与服务发现的基本用法,最后我们将安装并使用go-micro框架,实现一个简单版本的微服务应用。
我们要回答的第一个问题是,go-micro究竟是什么?
go-micro脱胎于micro。micro是一个面向微服务的技术生态,它不仅可以创建微服务,它还能跟包括Consul、ETCD、K8s、NATS、RabbitMQ等在内的许多插件集成,以完成更复杂的任务,而这其中当然也包括gRPC。micro项目已在GitHub上开源。go-micro作为micro生态的一部分,专注于Go语言的微服务开发。相较于Go语言自带的RPC和轻量级的gRPC,go-micro是一个功能更全面,在微服务开发中更具实用性的应用框架。和micro一样,go-micro项目也在GitHub上开源。
要想使用服务,首先必须找到服务,这就是服务发现需要解决的问题。
在我们之前编写的客户机代码中,经常出现对Dial方法的调用。它们可能是net包的,也可能是rpc或者jsonrpc包的。就在上节课,我们还用到了grpc包的Dial方法。无论是哪个包的Dial方法,都无一例外地将服务器的IP地址和端口号作为其参数之一,其目的只有一个,那就是建立与服务器的TCP连接。因此我们说,传统方式的客户机想要连接服务器,就必须先知道服务器的IP地址和通信端口。当服务比较少时,这样处理并没有什么问题。但当服务越来越多,特别是在基于分布式的微服务场景中,单个服务的小型化必然导致总体服务数量的激增,有些在线,有些离线,有些开始在线而后离线然后又在线。它们的运行状态、IP地址和端口号随时有可能发生变化。在这种情况下,请问你打算传什么参数给Dial方法?
假设在我们的应用中包括A和B两种业务,分别用两个独立的微服务提供支持。将执行A业务的微服务部署在三台主机上,分别用微服务A1、微服务A2和微服务A3表示。将执行B业务的微服务部署在另外两台主机上,分别用微服务B1和微服务B2表示。使用该应用的三台客户机中的每一台,都必须自己维护所有五台主机的IP地址和端口号,还要实时关注每台主机的运行状态,只访问那些能够正常提供服务的主机,且要兼顾负载均衡。这项工作无疑是繁冗而困难的。传统方式微服务架构的弊端显而易见。
为此,我们可以考虑引入一台专门负责管理这些微服务的服务发现服务器。每个微服务进程一启动即将自己的信息注册到这台服务发现服务器上。服务发现服务器维护一套数据结构,以服务名为键,IP地址和端口号为值,为每个微服务进程提供一个键值对形式的记录条目。同时,服务发现服务器还要实时跟踪每个微服务进程的运行状态,将每个微服务进程的在线、离线、活动、休眠、繁忙、空闲等状态信息关联于该条目。当客户机需要访问某个微服务时,先将该微服务的服务名提交给服务发现服务器。后者根据自己掌握的信息,选择最适合的微服务进程,将其IP地址和端口号提供给客户机。客户机据此与特定的微服务进程建立业务交互。客户机也可以直接将业务请求发送给服务发现服务器,由后者转发给特定的微服务进程,并将处理结果返回给客户机。
这里我们看到,引入服务发现服务器以后的微服务架构,变得更加清晰而简洁。所有关于微服务的管理、审计和状态跟踪工作,全部由服务发现服务器承担。每个客户机无需再自己维护各个微服务的IP地址、端口号和运行状态。既消除了不必要的信息冗余及因冗余而带来的信息不一致,又简化了客户机的工作,使其更专注于业务逻辑本身,提高了运行效能。
当然,我们必须清醒地认识到,服务发现本身也是一种服务。被服务发现管理的微服务就是服务发现的客户端。为了不使服务发现成为新的性能瓶颈,可通过搭建服务发现集群,提高其负载均衡能力,满足低延迟、高可用的性能要求。时至今日,服务发现已然成为一切微服务系统中不可或缺的重要组成部分。常见的服务发现框架包括但不限于MDNS、Consul、ETCD、Eureka、ZooKeeper等。其中MDNS是go-micro微服务框架中默认的服务发现组件,而Consul则是基于go-micro构建的微服务系统中最常用到的服务发现框架。
下面我们就以Consul为例,一起走进服务发现的世界。
Consul之所以成为基于go-micro构建的微服务系统中最常用到的服务发现框架,与它的特性密不可分。
Consul支持通过DNS或HTTP协议注册和发现服务。它可以借助心跳机制,实时跟踪每个微服务进程的运行状态,避免将请求转发给发生故障的微服务进程。在Consul内部,以键值对的形式动态存储每个微服务的配置信息。我们甚至可以通过搭建基于Consul集群的多数据中心,实现负载均衡。
Consul作为一款独立的服务器软件,需要单独安装。
Consul是HashiCorp公司推出的诸多软件产品之一。我们可以从它的官方网站直接下载对应平台的压缩包,解压到特定目录下,并将该目录添加到PATH环境变量中。
Consul提供了强大的命令行工具。熟练掌握这些命令,将使我们在今后的微服务开发中事半功倍。
操作Consul服务器只有一个命令,即consul。它带有多个子命令用于实现不同的功能,其中最常用的莫过于agent子命令,用于启动Consul服务器。在使用agent子命令时,可以携带不同的参数,以完成更精细化的控制。dev参数表示开发模式,一切皆取默认值,这是我们日常开发时最常使用的参数。bind参数用于指定Consul服务器的侦听地址,默认地址为0.0.0.0,表示侦听任意可用地址。http-port参数用于指定Consul服务器Web端的侦听端口,默认端口为8500。借助Consul服务器的Web端,我们可以在浏览器中管理、监视和配置Consul服务器。client参数用于指定哪些IP可以访问此Consul服务器,默认IP为127.0.0.1,即本机,0.0.0.0表示任意主机皆可访问。node参数用于为此服务发现节点命名。
config-dir参数用于指定配置文件所在的目录。我们可以在配置文件中,描述注册到此Consul服务器的微服务的信息。data-dir参数用于指定数据文件的目录。Consul服务器会将注册其上的每个微服务的运行状态,记录在数据文件中。rejoin参数令Consul服务器重新加入集群。server参数表示以服务器模式运行Consul,无此选项则表示以客户机模式运行。ui参数表示允许通过Web页面查看服务发现的详细信息。
除了agent子命令以外,我们还可以使用member子命令,查看集群中的节点,info子命令,查看Consul服务器本身的信息,leave子命令,退出集群。
下面我们将通过一系列实验,了解Consul的基本用法。首先是启动服务发现。
通过Consul启动服务发现的方法非常简单,直接在命令行执行consul命令,使用agent子命令,携带dev参数即可。为了验证Consul服务器已被启动,我们可以打开浏览器,访问本机的8500端口。这时我们看到的便是Consul服务器的管理页面。
服务发现的前提是服务注册,即由微服务主动将自身信息,注册到服务发现服务器上。
但是目前我们还没有能够完成此注册过程的微服务,因此我们采用另一种注册服务的方法,即通过配置文件注册。我们创建一个名为Consul的工程,在工程目录下编辑arithmetic.json配置文件:
1{
2 "service": {
3 "name": "arithmetic",
4 "id": "arithmeticService",
5 "tags": ["math"],
6 "address": "127.0.0.1",
7 "port": 9000,
8 }
9}
该配置文件定义了一个名为arithmetic的服务,其ID为arithmeticService,标签为math,侦听本机的9000端口。为了让该配置文件生效,我们需要在启动Consul服务器时,通过config-dir参数指定配置文件所在的目录。
再次打开浏览器,访问本机的8500端口。我们看到名为arithmetic的服务已赫然出现在服务列表之中。除了使用浏览器以外,我们也可以在命令行上,通过curl命令,查看特定服务的信息。
前面我们曾经讲过,Consul可以借助心跳机制,实时跟踪每个微服务进程的运行状态,避免将请求转发给发生故障的微服务进程。这就是所谓的健康检查。
为了验证这一点,我们可以在arithmetic.json文件中,添加有关健康检查的配置:
xxxxxxxxxx
151{
2 "service": {
3 "name": "arithmetic",
4 "id": "arithmeticService",
5 "tags": ["math"],
6 "address": "127.0.0.1",
7 "port": 9000,
8 "check": {
9 "id": "arithmeticHealth",
10 "http": "http://127.0.0.1:9000",
11 "interval": "5s",
12 "timeout": "1s"
13 }
14 }
15}
配置文件中的check即表示健康检查。这段配置表明,Consul服务器每隔5秒,向本机的9000端口发送一个HTTP形式的心跳包,超过1秒未收到响应,即判定该服务离线。此项检查的ID为arithmeticHealth。Consul健康检查必须是script、http、tcp或ttl中的一种。
因为我们修改了配置文件,如果Consul服务器已在运行,consul命令的reload子命令将令其重新加载配置。打开浏览器,访问本机的8500端口。这时我们可以看到页面中显示,ID为arithmeticService的服务不健康。这是必然的,毕竟该服务并不真的存在,Consul服务器的心跳监测会发现这个事实。
回到我们在上节课基于gRPC框架构建的RPC应用。在这个应用里,用于计算两个整数的和与差的RPC服务器,也可以被看作是一个微服务,而我们的客户机,则先根据IP地址和端口号建立与服务器的TCP连接,而后再执行远程过程调用。这里有一个前提,就是客户机必须先知道服务器的IP地址和端口号。由于之前我们讲过的种种原因,这在实际的微服务架构中,其实是很难做到的。如果在此基础上,引入Consul作为服务发现服务器,就可以将对微服务的管理工作交给Consul服务器,而客户机只需向Consul服务器询问所要访问微服务的IP地址和端口号,这样就避免了在客户机侧维护服务器信息的复杂性。
为了做到这一点,无论是RPC服务器还是RPC客户机,都无可避免地要和Consul服务器打交道,前者将自身信息注册到Consul服务器,而后者则从Consul服务器获得前者的IP地址和端口号。在Go语言程序中访问Consul服务器,需要调用Consul提供的编程接口。为此我们需要安装一系列软件包。
在开始后面的实验之前,请先用开发模式重新启动Consul服务器。服务端向Consul注册自身,需要四个步骤。首先要拿到Consul的默认配置,并利用此配置创建一个Consul客户机对象。这也印证了我们前面讲过的,服务发现本身也是一种服务,被服务发现管理的微服务就是服务发现的客户端。之后我们需要构造一个注册信息对象,其中包含有关微服务的描述,如服务名、服务ID、IP地址、端口号等等。有点儿类似于我们前面写过的arithmetic.json配置文件中的内容。最后通过Consul客户机,将这些注册信息注册到Consul服务器中。
为了复用之前的代码,我们将gRPC工程目录下的pb、MathServer和MathClient三个子目录,一起复制到Consul工程目录下。然后对MathServer目录下的main.go文件做一些修改:
x1package main
2
3import (
4 "Consul/pb"
5 "context"
6 "fmt"
7 "github.com/hashicorp/consul/api"
8 "google.golang.org/grpc"
9 "net"
10)
11
12type Arithmetic struct {
13 pb.UnimplementedArithmeticServer
14}
15
16func (arithmetic *Arithmetic) Add(
17 ctx context.Context, ops *pb.Operands) (*pb.Result, error) {
18 var res pb.Result
19 res.C = ops.A + ops.B
20 return &res, nil
21}
22
23func (arithmetic *Arithmetic) Sub(
24 ctx context.Context, ops *pb.Operands) (*pb.Result, error) {
25 var res pb.Result
26 res.C = ops.A - ops.B
27 return &res, nil
28}
29
30func main() {
31 fmt.Println("注册到Consul")
32
33 // 获取Consul默认配置
34 config := api.DefaultConfig()
35 // 创建Consul客户机
36 client, err := api.NewClient(config)
37 if err != nil {
38 fmt.Println("api.NewClient错误:", err)
39 return
40 }
41 // 构造注册信息
42 registration := api.AgentServiceRegistration{
43 Name: "arithmetic",
44 ID: "arithmeticService",
45 Tags: []string{"math"},
46 Address: "127.0.0.1",
47 Port: 9000,
48 Check: &api.AgentServiceCheck{
49 CheckID: "arithmeticHealth",
50 TCP: "127.0.0.1:9000",
51 Interval: "5s",
52 Timeout: "1s",
53 },
54 }
55 // 注册到Consul
56 if err = client.Agent().ServiceRegister(®istration); err != nil {
57 fmt.Println("Agent.ServiceRegister错误:", err)
58 return
59 }
60
61 server := grpc.NewServer()
62
63 fmt.Println("注册服务")
64 pb.RegisterArithmeticServer(server, new(Arithmetic))
65
66 fmt.Println("启动监听")
67
68 listener, err := net.Listen("tcp", "127.0.0.1:9000")
69 if err != nil {
70 fmt.Println("net.Listen错误:", err)
71 return
72 }
73
74 defer func() {
75 fmt.Println("结束监听")
76 listener.Close()
77 }()
78
79 fmt.Println("启动服务")
80 server.Serve(listener)
81}
我们看到这里所做的修改,主要集中在main函数的开始部分。Consul提供的api包中,有一个名为DefaultConfig的方法,调用它可以获得Consul的默认配置。再以该配置作为参数,调用api包的NewClient方法,创建一个Consul客户机对象。api包里还定义了一个名为AgentServiceRegistration的注册信息结构体类型,其中包含多个成员,用于描述微服务的各个属性,比如Name表示服务名、ID表示服务ID、Tags表示服务标签、Address表示服务的IP地址、Port表示服务的侦听端口、Check是一个AgentServiceCheck类型的结构体指针,表示有关健康检查的信息。到这里其实我们已经看出,所谓注册信息,和我们之前写在arithmetic.json配置文件中的内容大体相当。创建并初始化注册信息后,将其交给由Consul客户机对象获得的代理对象的ServiceRegister方法,即完成了微服务向Consul服务发现服务器注册自身的过程。
启动微服务。打开浏览器,访问本机的8500端口。我们可以看到名为arithmetic的服务,已被成功注册到Consul服务器中,其健康状态良好。
完成了服务端向Consul服务器的注册,我们再来看客户端如何从Consul服务器,获取服务端的IP地址和端口号。完成这项工作只需要三个步骤,其中前两个步骤与服务端一样,也是先获取Consul的默认配置,再创建Consul客户机。毕竟我们的客户端相对于Consul服务器而言,也是它的客户机。最后通过Consul客户机,获取记录在Consul服务器上的,有关健康服务的信息,其中就包含了,所有处于健康状态的微服务的IP地址和端口号。
打开MathClient目录下的main.go文件,我们对其做一些修改:
xxxxxxxxxx
701package main
2
3import (
4 "Consul/pb"
5 "context"
6 "fmt"
7 "github.com/hashicorp/consul/api"
8 "google.golang.org/grpc"
9 "google.golang.org/grpc/credentials/insecure"
10 "strconv"
11)
12
13func main() {
14 fmt.Println("从Consul中获取健康服务")
15
16 // 获取Consul默认配置
17 config := api.DefaultConfig()
18 // 创建Consul客户机
19 client, err := api.NewClient(config)
20 if err != nil {
21 fmt.Println("api.NewClient错误:", err)
22 return
23 }
24 // 从Consul中获取健康服务
25 services, _, err := client.Health().Service(
26 "arithmetic", "math", true, nil)
27 if err != nil {
28 fmt.Println("Health.Servic错误:", err)
29 return
30 }
31
32 fmt.Println("请求连接")
33
34 service := services[0].Service
35 conn, err := grpc.Dial(
36 service.Address+":"+strconv.Itoa(service.Port),
37 grpc.WithTransportCredentials(insecure.NewCredentials()))
38 if err != nil {
39 fmt.Println("grpc.Dial错误:", err)
40 return
41 }
42
43 defer func() {
44 fmt.Println("关闭连接")
45 conn.Close()
46 }()
47
48 arithmetic := pb.NewArithmeticClient(conn)
49
50 var ops pb.Operands
51 ops.A, ops.B = 123, 456
52
53 fmt.Println("远程调用")
54
55 if res, err := arithmetic.Add(context.TODO(), &ops); err != nil {
56 fmt.Println("ArithmeticClient.Add错误:", err)
57 return
58 } else {
59 fmt.Printf("调用返回: %d+%d = %d\n", ops.A, ops.B, res.C)
60 }
61
62 fmt.Println("远程调用")
63
64 if res, err := arithmetic.Sub(context.TODO(), &ops); err != nil {
65 fmt.Println("ArithmeticClient.Add错误:", err)
66 return
67 } else {
68 fmt.Printf("调用返回: %d-%d = %d\n", ops.A, ops.B, res.C)
69 }
70}
和服务端的情况类似,我们的修改主要集中在main函数的开始部分。在获取到Consul的默认配置并创建了Consul客户机以后,我们就可以利用Consul客户机,获取Consul服务器中记录的,健康服务的集合。其中的每个元素都是一个包含IP地址和端口号等信息在内的服务描述。而获取的依据,就是该服务的服务名和服务标签。它们体现在传递给由Consul客户机对象获得的健康对象的Service方法的参数中。当然,运行的结果,与不使用Consul服务器时应该是一样的。
理论上讲,当一个已被注册到Consul服务器中的微服务进程终止时,其也会被自动注销。但有时,我们可能希望在微服务进程尚在运行时,提前完成注销。为此,可以在获取到Consul的默认配置并创建Consul客户机后,调用由Consul客户机对象获得的代理对象的ServiceDeregister方法,其参数为被注销微服务的服务ID。
在Consul工程的工程目录下,创建一个Deregister子目录,在该目录下编辑main.go文件:
xxxxxxxxxx
291package main
2
3import (
4 "fmt"
5 "github.com/hashicorp/consul/api"
6)
7
8func main() {
9 fmt.Println("从Consul中注销服务")
10
11 // 获取Consul默认配置
12 config := api.DefaultConfig()
13
14 // 创建Consul客户机
15 client, err := api.NewClient(config)
16 if err != nil {
17 fmt.Println("api.NewClient错误:", err)
18 return
19 }
20
21 // 从Consul中注销服务
22 if err := client.Agent().ServiceDeregister(
23 "arithmeticService"); err != nil {
24 fmt.Println("Agent.ServiceDeregister错误:", err)
25 return
26 }
27
28 fmt.Println("服务注销成功")
29}
程序一开始依然是获取Consul的默认配置,并以该配置为参数创建Consul客户机。之后,我们调用了由Consul客户机对象获得的代理对象的ServiceDeregister方法,完成了对指定微服务的注销。注意传递给ServiceDeregister方法的参数,是所要注销微服务的服务ID,而非其服务名或服务标签。
注销以后。打开浏览器,访问本机的8500端口。名为arithmetic的服务已从服务列表中消失。
再次强调,获取微服务时,传递给Service方法的参数,是微服务的服务名和服务标签,而注销微服务时,传递给ServiceDeregister方法的参数,却是微服务的服务ID。这一点千万不要弄错。另外,对于微服务,这里有两个“注册”,一个是向Consul服务器注册,一个是向RPC系统注册,前者注册的是微服务进程的描述信息,而后者注册的则是封装远程方法的服务对象。
截止目前,我们所编写的客户机和服务器,都是自己动手,基于gRPC框架实现了远程过程调用,基于Consul编程接口实现了服务注册与发现。go-micro框架对这两个方面,做了更高层次的封装与抽象。基于go-micro框架构建微服务应用,我们已经不再需要直接和gRPC或者Consul打交道了,但我们要知道,它的底层依然是做了和我们之前所做的类似的工作。
go-micro框架不仅仅是一套运行时间库,它还包括一个命令行工具,可以帮助我们自动生成,基于微服务架构的服务器、客户机工程框架。该工具需要手动安装。
go-micro命令行工具已在GitHub上开源,可以通过go install命令直接下载安装。在Windows系统上,它就是GOPATH目录下bin子目录里的go-micro.exe可执行文件。
下面我们将利用go-micro命令行工具,创建一个基于微服务架构的服务器工程。
直接在命令行上执行go-micro命令,子命令为new表示创建,第一个参数为service表示所要创建的是微服务,第二个参数为微服务的服务名,同时也是该服务器工程的工程名。进入该工程的工程目录,可以看到名为Makefile的构建脚本文件。执行make命令,目标参数为init、proto、update和tidy。该命令将安装可被ProtoBuf编译器调用的Go语言代码生成器、编译ProtoBuf脚本、更新并下载所有依赖的包。在最终生成的工程目录下,可以看到入口函数文件main.go、远程方法实现目录handler、ProtoBuf脚本及其被编译后生成的Go语言程序目录proto,等等。如果使用的是Windows系统,默认并没有make命令,可以先行安装MinGW。
事实上,go-micro命令行工具,已经为我们生成了一个最简单的微服务工程。虽然我们一行代码都没有写,但它已经可以被编译和运行,并对外提供服务。服务的内容,就是在从客户机传入的任何名字前面冠以“Hello”,并返回给客户机。但在编译和运行之前,我们不妨先浏览一下工程中的代码。
打开工程目录下,proto子目录中的helloworld.pb.micro.go文件。该文件分别为客户机和服务器定义了两套API。在面向客户机的API中,有一个名为HelloworldService的接口,其中有一个Call方法,表示对服务器方法的远程调用。该接口的实现类也在这个文件中,名为helloworldService。在实现类中给出了Call方法的具体定义。而实现类的实例化对象,则由NewHelloworldService函数创建并返回。在面向服务器的API中,有一个名为HelloworldHandler的接口,其中也有一个Call方法,该方法由HelloworldHandler接口的实现类提供定义,将被客户机从远程调用。该文件中还有一个名为RegisterHelloworldHandler的函数,用于将HelloworldHandler接口的实现类对象注册到系统中。这里所说的系统,既包括RPC系统,也包括服务发现服务器,如MDNS、Consul等。
从对helloworld.pb.micro.go文件的解读不难理解,开发微服务,其实就是实现HelloworldHandler接口,即将业务逻辑体现在该接口Call方法的实现中。事实上,go-micro命令行工具已经为我们自动生成了这样的实现类。打开工程目录下,handler子目录中的helloworld.go文件。其中包含一个名为Helloworld的类,该类即为HelloworldHandler接口的实现类。其对Call方法的实现,表达了我们的业务逻辑,即将“Hello”字符串和请求中的名字拼接在一起,放到响应中。服务器进程的入口函数main,位于工程目录下的main.go文件中。它主要完成三个步骤,第一步通过micro包的NewService方法创建一个服务对象,并通过该服务对象的Init方法完成初始化。在初始化的过程中,提供了服务的名字和版本。第二步调用RegisterHelloworldHandler函数,将HelloworldHandler接口的实现类对象注册到系统中。第三步通过服务对象的Run方法启动并运行该服务。
前面我们讲过,调用RegisterHelloworldHandler函数,会将HelloworldHandler接口的实现类对象注册到系统中。而所谓系统,不单单指RPC系统,也包括服务发现服务器。那么我们如何指定服务发现服务器呢?
micro包NewService方法所创建并返回的服务对象,是对当前微服务进程的抽象。在对其初始化的过程中,我们只提供了服务的名字和版本,却并没有告诉它需要注册到哪里。事实上,在go-micro中,默认的服务发现是MDNS。这是一种基于组播的服务发现,只支持运行在同一个局域网内的本地微服务。
如果我们想把我们的微服务注册到基于Consul的服务发现中,只需在现有main函数的基础上,稍加修改即可:
xxxxxxxxxx
111...
2func main() {
3 // Create service
4 srv := micro.NewService()
5 srv.Init(
6 micro.Name(service),
7 micro.Version(version),
8 micro.Registry(consul.NewRegistry()),
9 )
10 ...
11}
Consul是基于集群的服务发现,注册到Consul的微服务,既可以在同一个局域网内,也可以在不同的局域网内。
在了解了这些以后。现在我们可以按照以前的方法,启动Consul服务器和名为helloworld的微服务。
打开浏览器,访问本机的8500端口。我们可以看到名为helloworld的服务,已被成功注册到Consul服务器中,其健康状态良好。
go-micro命令行工具,不但可以为我们生成服务器的工程框架,还可以为我们生成客户机的工程框架,去访问指定的微服务。
直接在命令行上执行go-micro命令,子命令为new表示创建,第一个参数为client表示所要创建的是客户机,第二个参数为所要访问微服务的服务名。该客户机工程的工程名,将在此服务名的基础上,添加“-client”后缀。客户机与服务器所使用的ProtoBuf脚本及其被编译后生成的Go语言程序是完全一样的。因此可以直接将服务器工程目录下的proto子目录,完整地复制到客户机的工程目录下,并将客户机工程目录下main.go文件中import部分的pb "helloworld/proto"
改为pb "helloworld-client/proto"
。
进入客户机工程的工程目录,可以看到名为Makefile的构建脚本文件。执行make命令,目标参数为init、update和tidy。该命令将安装可被ProtoBuf编译器调用的Go语言代码生成器、更新并下载所有依赖的包。同样,为了从基于Consul而非MDNS的服务发现中获取所要访问的微服务信息,需要对客户机工程目录下的main.go文件稍做修改:
xxxxxxxxxx
61...
2func main() {
3 // Create service
4 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
5 ...
6}
客户机的main函数同样是三个步骤。第一步通过micro包的NewService方法创建一个服务对象,并通过该服务对象的Init方法完成初始化。第二步调用NewHelloworldService函数,获得HelloworldService接口的实现类helloworldService的实例化对象。第三步以请求对象为参数,通过helloworldService类所实现的Call方法,调用服务器上的同名方法,并接收其返回的响应对象和错误对象。我们可以观察客户机打印的日志,验证来自微服务的响应是否符合预期。
谢谢大家,我们下节课再见!