微服务架构的系统,由多个以业务边界为划分的服务编排或者协同组成。这些服务在独立进程上运行,服务之间通过进程级别的通信机制(rpc、http)进行交互。这就涉及微服务的服务治理问题,首先要解决的是服务注册和服务发现。
需求
最简单的服务注册和发现,需要解决的问题:
- 一个进程可以对外提供服务,怎么让其他进程知道?
- 怎么调用其他进程的服务?
这要求有服务注册中心,存储服务列表和路由。
一个服务通常由多个实例进程提供。于是带来新的需求:
- 提供者:实例上线、下线,要通知注册中心
- 消费者:这次服务调用要使用哪个实例?(负载均衡)
- 注册中心:下发服务变化的通知到消费者
- 注册中心:检查服务提供者是否可用(心跳检查)
当服务提供者和消费者越来越多,对注册中心提出新的需求:
- 性能
- 高可用
组件
从上面需求来看,服务注册和发现,至少有以下组件。
- registry center。存储服务列表和路由信息。
- discovery。服务提供者暴露服务,服务消费者获取服务列表。
- health check。心跳检查,服务中心检查服务是否可用。
- load balance。请求在服务实例之间负载均衡。
服务治理需要哪些信息
在注册、调用之前,先想想服务治理需要哪些信息:
- 协议。比如http,dubbo,thrift
- 机房标识。处理跨机房调用,以及同机房优先的策略。
- 服务地址。可以是ip + host + port描述,也可以是域名。
- 服务名字。
- 版本。一个服务可以有多个版本共存。
- 服务的元数据:方法名,参数列表,返回结果,抛出异常类型,同步/异步响应等
- 与负载均衡相关的信息,比如超时、限流、权重
- 服务鉴权。
一种常见的实现方式是,使用URL来描述服务
dubbo://192.168.1.100:20880/com.example.HelloService
http://demo-service/HelloService?version=1
服务注册、服务发现的流程
以dubbo服务注册和发现为例,看看服务注册、服务发现的流程

(图片来源:https://dubbo.apache.org/img/architecture.png)
- 注册中心启动
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
客户端发现 vs 服务端发现
服务发现有2种模式:客户端发现和服务端发现。上面提到的dubbo框架使用的是客户端发现。Consul、AWS ELB则是服务端发现。
客户端发现,client side discovery
- 服务提供者启动后向注册中心登记,服务下线前向注册中心注销。注册中心定时检查服务实例是否在线,并且踢掉异常实例。
- 服务消费者从注册中心获取服务地址列表,直接向服务实例发起请求。

(图片来源:https://www.nginx.com/wp-content/uploads/2016/04/Richardson-microservices-part4-2_client-side-pattern.png)
服务端发现,server side discovery
- 服务提供者启动后向注册中心登记,服务下线前向注册中心注销。注册中心定时检查服务实例是否在线,并且踢掉异常实例。
- 服务消费者发起的服务调用请求,经过服务路由器、或者负载均衡器处理,由它们去访问注册中心,并且找到服务实例。

(图片来源:https://www.nginx.com/wp-content/uploads/2016/04/Richardson-microservices-part4-3_server-side-pattern.png)
客户端发现对比服务端发现:
- 服务请求路径:client side比server side少了一次网络跳转,效率更高。
- 服务发现逻辑:client side模式在服务消费者端实现。对于不同编程语言的服务,要有不同的客户端框架,否则可能无法通信(rpc)。server side由路由器/负载均衡器完成,简化了客户端依赖,但是增加了服务路由器,对基础建设要求更高。
AP or CP
分布式系统逃不开CAP理论。以下摘自wiki
一致性(Consistency) (等同于所有节点访问同一份最新的数据副本) 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据) 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)

(图片来源:https://www.researchgate.net/figure/Visualization-of-CAP-theorem_fig2_282679529)
因为网络分区无法避免,因此实际上是CP或者AP的选择。 Eureka是AP设计。节点之间的服务注册信息不做强一致性同步,依赖最终一致性实现节点间注册信息同步。 也有基于zookeeper设计的注册中心。由于zookeeper是强一致性的设计,因此对应的服务中心是CP类型。
那么注册中心应该使用AP还是CP?这就见仁见智。个人认为,对于服务注册这种类型,同一机房使用AP更为合适。
服务的优雅上线、优雅下线
服务的优雅上线实现比较简单,当服务提供者初始化完毕后,再向服务注册中心注册。
实现服务治理框架可以提供接口,判断是否已经完成注册。例如Thrift框架的Server.isServing()。
也可以自行对外提供http接口,查询是否已经完成注册。
服务的优雅下线则相对复杂。
如果是正常的主动下线,可以先从注册中心下线,处理完现有请求后,再进行服务自身的下线操作。
考虑非正常的下线情况。
进程被杀掉。如果是kill -15,则会触发应用的关闭钩子,例如java的addShutdownHook()。只要在shutdown hook执行下线操作,也是可以顺利主动下线。
如果是kill -9,则不会触发shutdown hook。此时要依赖注册中心进行health check,或者消费者上报调用失败统计,再对该服务实例下线。
消费者接收服务变更通知
注册中心push方式通知消费者,更新及时。但是如果网络瞬间故障,会丢失更新信息。 消费者pull方式主动向注册中心拉取更新,即使单次访问失败,最终也会保持一致。缺点是效率低,接收更新不及时。 因此实际上是推拉结合方式。
服务的心跳检查
通过心跳检查,剔除不在线的服务,从而维护可用服务列表。 服务提供者和注册中心建立health check机制,服务提供者定时发送心跳信息。注册中心的定时任务检查超时未上报服务实例,再根据心跳策略决定是否对失联服务实例下线。 如果服务实例规模相当大,那么心跳机制也会对注册中心产生流量压力。可以考虑其他的通信机制,例如gossip。
注册中心的性能
以下内容复制自“聊聊微服务的服务注册与发现”
对于那些采用了类 Paxos 协议的强一致性的组件,如ZooKeeper,由于每次写操作需要过半的节点确认。水平扩容不能提升整个集群的写性能,只能提升整个集群的读性能。 而对于采用最终一致性的组件来说,水平扩容可以同时提升整个集群的写性能和读性能。
注册中心故障
注册中心故障后,新的服务不能注册,旧的服务无法及时下线、增加或者减少服务实例。 对于消费者,通常会缓存服务路由信息(内存、本地文件)注册中心故障,不会影响已有消费者。