
在单体应用向微服务架构演进的过程中,原本的巨石型应用会按照业务需求被拆分成多个微服务,每个微服务会提供特定的功能,并可能依赖于其他的微服务。每个微服务实例都可以动态部署,服务实例之间的调用通过轻量级的远程调用方式(HTTP、消息队列等)实现,它们之间通过预先定义好的接口进行访问。
由于服务实例是动态部署的,每个服务实例的地址和服务信息都可能动态变化,这就势必需要一个中心化的组件对各个服务实例的信息进行管理,该组件管理了各个部署好的服务实例元数据,包括服务名、IP地址、端口号、服务描述和服务状态等。
什么是服务注册与发现
服务注册与发现由两部分组成:
**①服务注册,**指服务实例在启动时将自身信息注册到服务注册与发现中心,并在运行时通过心跳等方式向服务注册与发现中心汇报自身服务状态;
**②服务发现,**指服务实例根据服务名向服务注册与发现中心请求其他服务实例信息,用于进行接下来的远程调用。
下面我们就来介绍服务注册与发现中心的职责以及分布式系统中数据同步的基本原理CAP。
1.服务注册与发现中心的职责
随着应用架构向微服务架构迁移,服务数量不断增加,再加上动态部署动态扩展的特性,就使得服务地址和端口在运行时是随时可变的。对此,我们需要一个额外的中心化组件统一管理动态部署的微服务应用的服务实例元数据,我们一般称该中心化组件为服务注册与发现中心。
服务注册与发现中心主要有以下的职责:
- 管理当前注册到服务注册与发现中心的微服务实例元数据信息,包括服务实例的服务名、IP地址、端口号、服务描述和服务状态等;
- 与注册到服务注册与发现中心的微服务实例维持心跳,定期检查注册表中的服务实例是否在线,并剔除无效服务实例信息;
- 提供服务发现能力,为服务调用方提供服务提供方的服务实例元数据。
通过服务注册与发现中心,可以很方便地管理系统中动态变化的服务实例信息。与此同时,它也可能成为系统的瓶颈和故障点。因为服务之间的调用信息来自服务注册与发现中心,当它不可用时,服务之间的调用可能就无法正常进行了。因此,服务注册与发现中心一般会集群化部署,以提供高可用性和高稳定性。
2.分布式中的CAP理论
从本质上来讲,微服务应用属于分布式系统的一种落地实践,而分布式系统最大的难点是处理各个节点之间数据状态的一致性。即使是倡导无状态的HTTPRESTfuIAPI请求,在处理多服务实例情况下的修改数据状态请求,也是需要通过数据库或者分布式缓存等外部系统维护数据的一致性
CAP原理就是描述分布式系统下节点数据同步的基本定理。 该原理由加州大学的EricBrewer教授提出,**分别指Consistency(一致性)、Availability(可用性)和Partition tolerance性(分区容忍)。**其具体含义如下:
- Consistency,指数据一致性,表示一个系统的数据信息(包括备份数据)在同一时刻都是一致的。在分布式系统下,同一份数据可能存在于多个不同的实例中,在数据强一致性的要求下,对其中一份数据的修改必须同步到它的所有备份中。在数据同步的任何时候,都需要保证所有对该份数据的请求将返回同样的状态。
- Availability,指服务可用性,要求服务在接收到客户端请求后,都能够给出响应。服务可用性考量的是系统的可用性,要求系统在高并发情况下和部分节点宕机的情况下,系统整体依然能够响应客户端的请求。
- Partitiontolerance,指分区容忍性。在分布式系统中,不同节点之间是通过网络进行通信。基于网络的不可靠性,位于不同网络分区的服务节点可能会通信失败,如果系统能够容忍这种情况,说明它是满足分区容忍性的。如果系统不能够满足分区容忍性,那么将会限制分布式系统的扩展性,即服务节点的部署数量和地区都会受限,这就违背了分布式系统设计的初衷,所以一般来讲分布式系统都会满足P,也就是分区容忍性。
另外,EricBrewer认为,**这三个指标最多同时满足两个。**基于分布式系统的基本特质,P(分区容忍性)是必须要满足的,所以接下来需要考虑满足C(数据一致性)还是A(服务可用性)。在类似银行之类对金额数据要求强一致性的系统中,要优先考虑满足C(数据一致性);而在类似大众网页之类的系统中,用户对网页版本的新日不会有特别的要求,在这种场景下A(服务可用性)会高于C(数据一致性)。
如何选择服务注册与发现框架
随着近几年微服务框架的高速发展,目前业界已经开源出了大量优秀的服务注册与发现组件,比如Consul、Etcd、ZooKeeper等。它们之间各有干秋,在组件选型时你可以根据自身业务的需要进行选择和改造。
接下来我们就来介绍下Consul、Etcd 和ZooKeeper这三个组件,最后再将三者进行对比,给出你一些选择的依据。
1. Consul
Consul由HashiCorp开源,是支持多个平台的分布式高可用系统。Consul 采用Go开发,主要用于分布式系统的服务发现与配置,它满足CP 特性。Consul是分布式、高可用和可横向扩展的,提供以下主要特性:
- **服务发现。**可以使用HTTP 或者DNS的方式将服务实例的元数据注册到 Consul,通过Consul 发现所依赖服务的元数据列表。
- **健康检查。**Consul提供定时的健康检查机制,定时请求注册到Consul中的服务实例提供的健康检查接口,将异常返回的服务实例标记为"不健康"。
- **Key/Value.**Consul 提供了 Key/Value 存储功能,可以通过简单的 HTTP 接口进行使用。
- **多数据中心。**Consul使用Raft算法来保证数据一致性,提供了开箱即用的多数据中心功能。
服务实例与Consul的交互如下图所示:

Consul 的交互图
通过该图,我们可以看到Consul实现服务注册与发现中心的调用过程如下:
- Producer在启动之初会通过/register接口将自己的服务实例元数据注册到Consul 中;
- Consul 通过Producer 提供的健康检查接口/health 定时检查 Producer 的服务实例状态;
- Consumer请求Consul的接口获取Producer服务的元数据;
- Consumer 从Consul 中返回的Producer服务实例元数据列表中选择合适的服务实例,并使用其配置的IP 和端口信息发起服务调用,如上图中 Consumer 调用 Producer的/service 接口。
Consul是一个高可用的分布式系统,支持多数据中心部署。 一个Consul集群由多个部署和运行了Consul Agent 的节点组成。Consul 集群中主要存在两种角色:**Server和 client。**Consul 使用 Gossip协议来管理成员和广播消息到集群。每个Consul Agent负责对本地的服务进行监控检查,并将查询请求转发到Server中进行处理。Consul的架构图如下所示:

Consul 的架构图
简言之,作为一个开箱即用、高可用分布式服务发现和配置系统,Consul可以很方便地为微服务的服务治理提供强有力的支持。在后面的课时中,我会带你实现一个Consul的客户端,将我们自身的Web服务注册到Consul中,以供其他服务或者网关调用。
2. Etcd
Etcd 是基于HTTP 协议的分布式key/Value 存储系统,由CoreOS 开源,采用Go语言编写,主要用于服务发现和配置共享。Ectd 的经典应用场景有:
- **Key/Value 存储。**Etcd 支持HTTP RESTful API,提供强一致性、高可用的数据存储能力。
- **服务发现。**通过在Etcd中注册某个服务的目录,服务实例连接Etcd并在目录下发布对应IP和Port以供调用方使用,可以有效实现服务注册与发现的功能。
- **消息发布与订阅。**通过Etcd的Watcher机制,可以使订阅者订阅他们关心的目录。当消息发布者修改被监控的目录内容时,可以将变化实时通知给订阅者。

Etcd工作原理图
Etcd 集群中的节点提供两种模式,分别为:
- **Proxy 模式。**该模式下的 Etcd 节点会作为一个反向代理,把客户端的请求转发给可用的Etcd Peer集群。Proxy没有加入Etcd 的一致性集群中,不会降低集群的写入性能。
- **Peer 模式。**该模式下的节点提供数据存储和同步的能力。Peer 之间通过Raft协议进行Leader 选举和保持数据强一致性,通常建议部署奇数个节点提供高可用的集群能力。
相对于其他的组件来讲,Etcd 更为轻量级,部署简单,支持HTTP 接口。它为服务发现提供一个稳定且高可用的消息注册仓库,可以有力支撑微服务的协同工作。
3. ZooKeeper
ZooKeeper是一个开源的分布式系统协调服务,目前由Apache 基金会维护,采用Java语言开发。ZooKeeper将分布式系统中那些复杂且易出错的服务封装为简单高效的接口,意在帮助开发人员高效地解决分布式系统中的一致性问题
ZooKeeper底层只提供了两个功能:**管理客户端提交的数据和为客户端程序提供数据节点的监听服务。**它是一个典型的分布式数据一致性解决方案,基于ZooKeeper 可以实现服务发现与注册、消息发布与订阅、分布式协调与通知、分布式锁、Leader 选举、集群管理和分布式队列等诸多功能。
ZooKeeper 集群中 Server 主要存在三种角色,分别为 Leader、Follower 和 Observer,ZooKeeper 的架构如下所示:

ZooKeeper 架构图
ZooKeeper 使用独特的ZAB 协议来保证集群内数据的一致性。ZAB 协议基于Paxos算法设计,是一种崩溃可恢复的原子消息广播协议,主要包含以下两种形式:
- **崩溃恢复模式。**在服务启动或者Leader服务器离线时,为了选举出新的Leader,集群会进入到崩溃恢复模式。当通过投票选举了新的Leader后,集群中 Follower 会与新的Leader 进行状态同步。当集群中有半数以上的服务器完成同步,集群将进入到消息广播模式。
- **消息广播模式。**ZAB协议使用一个类似于二阶段提交的原子广播协议进行消息广播,它不要求Follower节点都返回ACK才完成一致性事务,而是只需要半数以上即可提交并完成一个事务广播。
ZooKeeper为分布式系统提供协调服务,能够有效地支持微服务架构的服务注册和发现机制。同时ZooKeeper 中提供的其他数据一致性解决方案,能够有力支撑微服务中分布式业务的开发。
4.组件对比
以上介绍的三种服务注册与发现组件在业界都已经有了广泛的应用,在很多大公司的项目中都能看到它们的身影,比如 ZooKeeper 在Hadoop 体系中发挥了极其重要的分布式协调作用。下面我们就从特性方面比较它们的异同:

从软件的生态 来看,Consul是以服务发现和配置作为主要功能目标,附带提供了KeyValue存储,相对于 Etcd和ZooKeeper来讲业务范围较小,更适合于服务注册与发现
Etcd和ZooKeeper都是通用的分布式一致性存储系统,被应用于分布式系统的协调工作中,使用范围抽象,具体的业务场景需要开发人员自主实现,如服务发现、分布式锁等。另外,ZooKeeper具备广大的周边生态,在分布式系统中得到了广泛的使用;而Etcd以简单易用的特性吸引了大量开发人员,在目前火热的Kubernetes中也有应用。
而仅从服务注册与发现组件的需求来看,选择Consul作为服务注册与发现中心能够取得更好的效果;如果系统存在其他分布式一致性协作需求,比如分布式事务、分布式Leader选举、分布式锁等,选择Etcd 和 ZooKeeper 反而能够提供更多的服务支持。
小结
服务注册与发现在微服务架构中是各个微服务之间的协调者,因此掌握服务注册与发现的基本原理,正确使用服务注册与发现组件对于我们开发微服务非常重要。
如何基于Consul给微服务添加服务注册与发现的案例。
微服务架构按业务划分微服务的特点,使得原本聚合了大量业务模块的单体应用被划分为众多的微服务。而大量微服务的出现,势必会带来运维管理上的巨大挑战,于是服务注册与发现这类自动化策略应运而生 。但是引入服务注册与发现就可能引入额外技术栈,增加系统总体的复杂性,比如会引入中心化的服务注册与发现中心这类基础组件。
本文,我们将基于Consul给微服务添加服务注册与发现的能力。首先,我们会基于Kubernetes搭建一个接近生产环境的Consul集群;接着再基于搭建好的Consul集群,为我们的微服务添加服务注册与发现的基本能力。
Consul集群
在上文,我们分别介绍了Consul、ZooKeeper和 Etcd 这 3种服务注册与发现组件,考虑到Consul开箱即用、专注于服务注册与发现 的特点,我们就选取Consul作为后续实践使用的服务注册与发现中心组件。
Consul集群中存在Server和client 两种角色节点,Server 中保存了整个集群的数据,而Client负责对本地的服务进行健康检查和转发请求到Server中,并且也保存有注册到本节点的服务实例数据。
关于Server节点,一般建议你部署3个或者5个节点,但并不是越多越好,因为这会增加数据同步的成本。Server节点之间存在一个Leader和多个Follower,通过Raft协议维护Server之间数据的强一致性。一个典型的Consul 集群和微服务的部署方式如下:

Consul集群和微服务部署图
在上述图片的Consul 集群中,存在3个Server节点,它们分别部署在不同的服务节点上。我们假设Node2节点中的Server被选举为Leader,那么其他节点上的Server则需要与其同步数据。
在Node4、Node5和Node6节点中分别部署了ConsulClient,各节点中的各个微服务通过它进行服务注册。Consul Client会将注册信息通过RPC调用转发给ConsulServer,这些服务实例元数据会保存到Server中的各个节点,并通过Raft协议保证数据的强一致性;同时Consul Client会对注册到自身的微服务进行健康检查,并将检查到的服务状态同步到 Server 中。
其实服务实例也可以直接注册到Consul Server,但是每个节点的注册服务实例数量存在上限,因为节点还负责定时对服务实例进行健康检查,因此在服务实例数量较多的时候,建议使用ConsulClient分担 Consul Server 的处理工作。
当微服务间要发起远程调用时,比如位于Node5节点的ServiceC想要调用ServiceB服务,它将首先向节点上的ConsulClient根据服务名请求ServiceB的服务实例信息列表,ConsulClient会把请求转发到Consul Server 中,查询ServiceB的服务实例信息列表返回。接着ServiceC根据一定的负载均衡策略,从中选择合适的 Service B 的实例IP 和端口发起远程调用。
接下来我们就基于Kubernetes搭建一个接近生产环境的Consul集群,包含3个Server节点和1个Client节点。考虑到Pod 的意外重启会导致Consul Server IP的变化,我们首先为Consul Server 声明一个 Service, consul-server-service.yaml 定义如下:
bash
apiversion: v1
kind: Service
metadata:
name: consul-server
labels:
name: consul-server
spec:
selector:
name: consul-server
ports:
- name: http
port: 8500
targetPort: 8500
- name: https
port: 8443
targetPort: 8443
- name: rpc
port: 8400
targetPort: 8400
.... //其他暴露的端口
ConsulServer对外暴露了诸多接口,包括响应HTTP和HTTPS请求的8500和8433端口等,Service方式使得这些端口在集群内可通过ClusterlP:Port 的方式访问。
当Consul Server所在的Pod 重启后,新启动的Consul Server需要重新加入集群中,这就需要知道Leader 节点IP。对此,我们可以使用 Kubernetes 提供的 DNS 功能访问不同 Pod 的 Consul Server,并通过StatefulSetsController管理Consul Server,使得每个Consul Server Pod有固定的标识用于DNS 解析。通过这样的方式能够使得Consul Server 集群可以自动处理Leader 选举和新节点加入的问题,充分利用 Kubernetes 的自动伸缩和调度能力。consul-server.yaml 配置如下:
bash
apiversion: apps/v1
kind: Statefulset
metadata:
name: consul-server
labels:
name: consul-server
spec:
serviceName: consul-server
selector:
matchLabels:
name: consul-server
replicas: 3
template:
metadata:
labels:
name: consul-server
spec:
terminationGracePeriodseconds: 10
containers:
- name: consul
image: consul:latest
imagePullpolicy: IfNotPresent
args:
- "agent""-server"
- "-bootstrap-expect=3"
- "-ui"
- "-data-dir=/consul/data"
- "-bind=0.0.0.0"
- "-client=0.0.0.0"
- "-advertise=$(POD_IP)"
- "-retry-join=consul-server-0.consul-server.$(NAMESPACE) .svc.cluster.local"
- "-retry-join=consul-server-1.consul-server.$(NAMESPACE) . svc.cluster. local"
- "-retry-join=consul-server-2.consul-server.$(NAMESPACE) .svc.cluster.local"
- "-domain=cluster.local"
- "-disable-host-node-id"
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 8500
name: http
- containerPort: 8400
name: rpc
- containerPort: 8443
name: https-port
// ...其他端口
上述配置中指定了Controller为StatefulSet,这使得被管理的Pod具备固定的命名规则,可用于DNS解析,它们的 Pod 名称分别为 consul-server-0、consul-server-1 和 consul-server-2。配置还通过 -retry-join 选项让新加入的节点逐一尝试加入每一个Consul Server,直到发现真正的Leader 节点并加入 Consul Server 集群中。
为了方便在Kubernetes 集群外访问 Consul Ul,可L以通过 NodePort暴露Consul Server 的 8500端口, 如下 consul-server-http.yaml 配置所示:
bash
apiversion: v1
kind: Service
metadata:
name: consul-server-http
spec:
selector:
name: consul-server
type: NodePort
ports:
- protocol: TCP
port: 8500
targetPort: 8500
nodePort: 30098
name: consul-server-tcp
依次通过kubectl apply-f {yaml}启动上述 3个 yaml配置后,即可在集群外通过 30098 端口访问Consul Ul,结果图如下:

Consul Server 部署效果图
从上图可以看到目前Consul集群中有3个ConsulServer,带小星星的consul-server-O为Leader节点。在前面介绍Consul集群的部署图时,为方便Consul Client对服务节点上的微服务进行管理,建议在每一个服务节点上部署ConsulClient,对此我们可以通过DaemonSetController的方式部署Consul Client.
DaemonSetController能够确保在集群所有的Node中或者指定的Node中都运行一个副本Pod。consul-client.yaml 的配置如下 :
bash
apiversion: apps/v1
kind: DaemonSet
metadata:
name: consul-client
labels:
name: consul-client
spec:
selector:
matchLabels:
name: consul-client
template:
metadata:
labels:
name: consul-client
spec:
volumes :
- name: consul-data-dir
hostPath:
path: /data/consul/data
type: DirectoryorCreate
containers:
- name: consul
image: consul:latest
imagePullPolicy: IfNotPresent
args:
- "agent"
- "-data-dir=/consul/data"
- "-bind=0.0.0.0"
- "-c1ient=0.0.0.0"
- "-advertise=$(POD_IP)"
- "-retry-join=consul-server-0.consul-server.$(NAMESPACE) .svc.cluster. local"
- "-retry-join=consul-server-1.consul-server.$(NAMESPACE) . svc.cluster. local"
- "-retry-join=consul-server-2.consul-server.$(NAMESPACE) .svc.cluster. local"
- "-domain=cluster.local""-disable-host-node-id"
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
lifecycle:
poststart:
exec:
command:
- /bin/sh
- -c
- consul reload
prestop:
exec:
command:
- /bin/sh
- -c
- consul leave
volumeMounts:
- name: consul-data-dir
mountPath: /consul/data
ports:
- containerPort: 8500
hostPort: 8500
name: http
- containerPort: 8400
name: rpc
- containerPort: 8443
name: https
// ···其他端口
在上述配置中,我们指定Controller 为 DaemonSet,并修改Consul的启动命令,去除-server等选项,使得Consul 以 Client 的角色启动并加入集群中。除此之外,还通过volumes 配置将ConsulClient的数据目录/consul/data 挂载到 Node节点上,使得意外宕机的Consul Client重启时能够复用相同的node-id等元数据,避免导致Consul 中出现同一个IP对应不同主机名的服务注册错误的情况。
运行上述的yaml 配置文件后,就能在 Consul Ul 中发现 Consul 集群中出现了 Consul Client。(由于这里我们搭建的 Kubernetes 集群只有一个 Node 节点,因此 Consul 集群中仅有一个 ConsulClient. )

Consul Client 出现在集群
注册服务到Consul
ConSuI提供HTTP和DNS两种方式访问服务注册与发现接口,我们接下来的实践主要是基于HTTPAPI进行的。
由于我们是通过Consul Client进行服务注册与发现,所以接下来我们会首先介绍Consul Client中提供的用于服务注册、服务注销和服务发现的 HTTP API,如下所示:
bash
/vl/agent/service/register // 服务注册接口
/vl/agent/service/deregister/${instanceId} // 服务注销接口
/v1/health/service/${serviceName} // 服务发现接口
服务注册接口 用于服务启动成功后,服务实例将自身所属的服务名和服务元数据,包括服务实例1D、服务IP、服务端口等提交到Consul Client中完成服务注册。当服务关闭时,为了避免无效的请求,服务实例会调用服务注销接口 主动将自身服务实例数据从Consul中移除。服务发现接口用于在发起远程调用时根据服务名获取该服务可用的服务实例信息列表,然后调用方就可以使用一定的负载均衡策略选择某个服务实例发起远程调用,该接口会把查询请求转发到ConsulServer处理。另外,还存在/v1/agent/health/service/接口用于获取注册到本地ConsulClient的可用服务实例信息列表。
需要提交到Consul 的服务实例信息主要有以下这些:

这其中,ID用来唯一标识服务实例,Name代表服务实例归属的服务集群,Address、Port表示服务的IP地址和监听端口,Check用于指定健康检查的配置,Consul中提供主动上报和被动回调两种方式维持心跳。
接下来我们以服务注册为例,演示服务实例如何注册自身数据到Consul,discovery_client.go 中服务注册的代码如下所示:
Go
func (consulclient *Discoveryclient) Register(ctx context.Context, serviceName,instanceId, healthcheckUrl string, instanceHost string, instancePort int, metamap[string]string, weights *weights) error {
instanceInfo := &InstanceInfo{
ID: instanceId,
Name: serviceName,
Address : instanceHost,
Port: instancePort,
Meta: meta,
EnableTagoverride: false,
Check: Check{
DeregisterCriticalserviceAfter: "30s",
HTTP: "http://" + instanceHost + ":" strconv.Itoa(instancePort) + healthcheckUrl,
Interval: "15s",
},
}
if weights != nil{
instanceInfo.Weights = *weights
}else {
instanceInfo.Weights = Weights{
Passing: 10,
warning: 1,
}
}
byteData, err := json.Marshal(instanceInfo)
if err != nil{
log.Printf("json format err: %s", err)
return err
}
req, err := http.NewRequest("pUT",
"http: //"+consulclient.host+":"+strconv.Itoa(consulclient.port)+"/v1/agent/service/r
egister",
bytes.NewReader(byteData))
if err != nil{
return err
}
req.Header.Set("content-Type", "application/json;charset=UTF-8")
client := http.Client{}
client.Timeout = time.Second * 2
resp, err := client.Do(req)
// 检查 HTTP 请求是否发送成功,判断是否注册成功
....
}
上述代码的关键在于将服务实例信息封装为Instancelnfo,并通过HTTP请求访问ConsulClient的/v1/agent/service/register 接口,将服务实例信息提交Consul 中。我们在Instancelnfo 中设定了服务实例ID、服务名、服务地址、服务端口等关键数据,并指定了健康检查的地址用于与Consul维持心跳。类似的,服务发现和服务注销的实现也是通过调用上面介绍的HTTPAPI完成,在此就不演示代码了。
微服务需要在准备就绪后,向服务注册与发现中心发起服务注册,并在服务关闭前及时从服务注册与发现中心注销自身服务实例信息,这些我们都可以在main函数中添加对应的行为,代码如下所示:
Go
// ... 通过命令行参数或者环境变量获取服务配置信息
flag.Parse()
client := discovery.NewDiscoveryclient(*consulAddr, *consulPort)
//..· 省略 注册 endpoint,构建 transport
<Span class="hljs-keyword">go</span> <span class="hljs-function"><span
class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
errchan <- http.ListenAndserve(<span class="hljs-string">":"</span> +
strconv.Itoa(*servicePort), handler)
}()
<span class="hljs-keyword">go</span> <span class="hljs-function"><span
class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
<Span class="hljs-comment">//监控系统信号,等待 ctr] + c 系统信号通知服务关闭
</span>
c := <span class="hljs-built_in">make</span>(<span class="hljs-
keyword">chan</span> 0s.Signal, <span class="hljs-number">1</span>)
signal.Notify(c, SyScall.SIGINT, SyScall.SIGTERM)
errchan <- fmt.Errorf(<span class="hljs-string">"%s"</span>, <-c)
}()
instanceId := *serviceName + <span class="hljs-string">"-"</span> +uuid.New() .String()
err := client.Register(context.Background(), *serviceName, instanceId, <span
class="hljs-string">"/health"</span>, *serviceAddr, *servicePort, <span
class="hljs-literal">nil</span>, <span class="hljs-literal">nil</span>)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span>
{
log.Printf(<span class="hljs-string">"register service err : %s"</span>, err)
os.Exit(<span class="hljs-number">-l</span>)
}
err = <-errchan
log.Printf(<span class="hljs-string">"listen err : %s"</span>, err)
client.Deregister(context.Background(), instanceId)
在 main 函数中启动http.ListenAndServe 监听对应的服务端口时,我们同时调用了DiscoveryClient.Register方法向Consul发起服务注册,并采用监控系统信号的方式,在服务关闭时调用DiscoveryClient.Deregister进行服务注销,以避免无效的请求发送到已关闭的服务实例。
为了保证Consul Client主动进行健康检查成功,我们还需要在transport层中定义/health 接口用于响应 Consul Client的调用,代码如下所示:
Go
r.Methods("GET").Path("/health") .Handler(kithttp.NewServer(
endpoints.HealthcheckEndpoint,
decodeHealthCheckRequest,
encodeJSONResponse,
options...,
))
在 Kubernetes 部署微服务实例时,可以通过获取服务实例所在的 Pod IP 作为服务实例IP 提交到Consul,使用Kubernetes的valueFrom 即可获取服务实例所在Pod 的相关信息。部署register 微服务的Kubernetes简单配置如下所示:
bash
apiversion: vl
kind: Pod
metadata:
name: register
labels:
name: register
spec:
containers:
- name: register
image: register
ports:
- containerPort: 12312
imagePullPolicy: IfNotPresent
env:
- name: consulAddr
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: serviceAddr
valueFrom:
fieldRef:
fieldPath: status.podIP
由于Consul Client部署在每一个Node节点中,我们可以直接获取spec.nodeName(即Pod 所在Node节点的主机名)作为ConsulClient的地址传递给Go微服务,而Go微服务的IP地址即其所在Pod 的IP。在Kubernetes 中启动该配置后即可在Consul Ul 中查看到该服务实例注册到 Consul 中,如图所示:

register服务注册到Consul
进入到 register服务所在的 Pod,通过curl 访问/discovery/name?serviceName={serviceName}即可根据服务名获取注册到Consul 中的服务实例信息列表。
小结
在微服务架构中,服务注册与发现能够管理集群中大量动态变化的服务实例,有效提高服务治理的效率。
本文我们主要介绍了如何结合Consul给Go微服务整合服务注册与发现能力。首先。我们借助
Kubernetes搭建了具备3个Consul Server 的Consul 集群;接着,我们又基于Consul Client 提供的HTTP API 完成了 Go 微服务与Consul 的服务注册与发现。
希望通过本文能够加深你对Consul的理解,并掌握如何为Go微服务添加服务注册与发现能力。