讲到这里,我们已经对一个客户端请求进入业务HTTP服务的过程有了较为详细的了解。业务HTTP服务在处理请求的过程中免不了与其他下游服务通信——可能会调用其他业务服务,可能需要访问数据库,可能会向消息中间件投递消息等,所以业务HTTP服务必须知道下游服务部署的可用地址。这就是本节要介绍的服务发现问题。
这里不是特指HTTP服务,在当前流行的微服务架构下,任何服务都涉及与其他服务通信的问题。要求每个服务的研发人员主动维护下游服务地址是不现实的,我们需要为每个服务提供可以自动发现下游服务地址列表的能力,这就是服务发现。服务发现组件是微服务架构下最重要的基础组件之一。
1.5.1 注册与发现
服务发现的技术原理并不复杂,一般通过提供一个服务注册中心来实现。这个服务注册中心主要负责两件事情:
- 管理每个服务的地址列表(注册)
- 将某服务的地址告知调用者(发现)。
让我们通过一个例子来阐明服务注册中心的工作流程。调用者A需要调用服务B来完成某请求,服务发现过程如图1-18所示。
服务发现过程描述如下:
- 服务B的各个服务实例启动后,将自己的地址信息注册到服务注册中心,由服务注册中心将其存储起来。
- 调用者A向服务注册中心查询服务B的地址。
- 服务注册中心将其存储的服务B已注册的实例地址列表返回给调用者A。
- 调用者A得到地址列表,可向服务B的任意一个实例地址发起远程调用。
调用者A除了可以主动查询服务地址,还可以采取订阅推送的方式与服务注册中心通信:
目前业界有很多可选择的服务注册中心组件,比如Spring Cloud的Eureka、CNCF旗下的CoreDNS等。
1.5.2 可用地址管理
服务注册中心可以使用MySQL等数据库来保存已注册的服务地址列表,存储选型相对自由。这不是这一节的重点,本节要讨论的重点是每个地址是否可用。调用者的调用目的地既然由服务注册中心决定,那么服务注册中心提供的地址应该是访问可达的,否则调用者将无法成功完成服务调用。那么,服务注册中心如何保证已注册的服务地址列表总是可用的呢?
由于服务的创建、销毁、升级、扩容/缩容都会造成服务地址列表的变更,所以服务的每个实例在启动时都需要向服务注册中心注册自己的地址,并在退出时注销地址。只有这样,才能使得服务注册中心一直在维护最新的服务地址列表。
不过,这还不够,我们还没有考虑异常情况。比如某服务实例突然挂掉,它还没来得及向服务注册中心注销地址,这就使得服务注册中心向调用者下发了不可用的服务地址,调用者调用请求被拒绝。所以,服务注册中心应该有对已注册的服务地址的探活能力。
地址探活一般有两种思路:
- 第一种思路是主动探活。服务注册中心周期性地向每个已注册的服务地址发起探测请求,如果某地址探测成功,则认为这个地址是可用的;否则,认为这个地址已经失效,服务注册中心主动摘除这个地址。这种思路只适用于服务较少、实例较少的小型互联网产品后台,对于用户量级较大的产品(动辄有上千个服务,个别访问量大的核心服务可能会部署上万个实例,所有服务的 实例总数有几百万个)来说,服务注册中心主动探活实例地址的效率非常低。
- 第二种思路是心跳探活。服务注册中心为每个已注册的服务地址记录一个最近心跳时间,服务实例启动后,每隔一段时间(如30s)就向服务注册中心发送一个心跳包,服务注册中心收到心跳包后更新对应服务地址的最近心跳时间。服务注册中心会启动定时器来检查每个服务地址的最近心跳时间相比于当前时间已经过了多久,如果其超过了某个阈值 (如150s),那么就可以认为这个服务实例已经不可用了,服务注册中心将其地址摘除,如图1-20所示。
服务注册中心基于服务实例心跳探活来摘除不可用的地址,可以在很大程度上保证地址列表可用。不过,在某些场景下摘除地址也存在潜在风险,这里举两个例子。
- 一个例子是服务注册中心的地址摘除逻辑有Bug,导致某服务的地址被意外全部摘除。从整个产品后台来看,这个服务等于凭空消失了,最终与其关联的全部业务场景均产生故障。
- 另一个例子是假设点赞服务有5个实例,每个实例可承受的最大QPS为1000,当前后台点赞服务的总QPS为3000,即每个实例承受的QPS为600。在某一时刻,S1、S2、S3这三个实例与服务注册中心发送心跳包的网络链路发生了长时间中断,服务注册中心认为这三个实例发生故障,于是将其对应的地址摘除。点赞服务的调用者收到的最新地址列表是[S4, S5],于是所有调用请求都转向这两个实例,导致每个实例实际承受了1500的QPS,最终这两个实例被压垮,整个点赞服务不可用了,如图1-21所示。
这两个例子都是因为服务注册中心过度摘除地址而带来了风险,所以这里建议为服务注册中心摘除地址增加简单的保护策略:在某服务的地址列表中,如果已摘除的地址数超过某个阈值(如30%),那么服务注册中心就停止摘除地址,并向服务负责人报警以寻求人工处理,防止出现不可控的故障。
1.5.3 地址变更推送
当服务的地址列表发生变更时,为了让调用者尽快感知到这件事情,服务注册中心需要主动推送最新地址列表给调用者。推送数据可能遇到的一个问题是“推送风暴”——如果某服务有100个调用者,每个调用者又有1000个实例,那么每当这个服务的地址列表发生变更时,就要给100000个节点推送数据;如果有多个服务的地址列表发生变更,则推送的节点会更多,这会严重占用网络带宽。那么,如何解决这个问题呢?
- 首先,建议推送的数据是“新增了哪些地址” “哪些地址已废弃”的增量数据形式,而不是全量地址列表,这能在一定程度上节约推送数据的网络带宽。
- 其次,服务注册中心本身也是一个服务,如果公司的服务注册中心有条件部署大量的实例,那么推送风暴问题也就被轻易解决了,毕竟分摊到每个服务注册中心实例的推送的节点不会很多。
- 最后,可以采用推拉结合的方式:服务注册中心最多给N个调用者节点推送地址变更信息,其他节点周期性地(如1min)从服务注册中心拉取最新地址列表。
由于服务发现组件的存在,调用者只需要关心要调用谁,而不需要关心被调用者的地址——调用者可以随时升级、扩容,不用担心调用者找不到。作为微服务架构下最核心的组件之一,服务发现真正为微服务带来了弹性。
总结
服务注册中心的主要职责?
- 管理每个服务的地址列表(注册)
- 将某服务的地址告知调用者(发现)
调用者如何从服务注册中心获取服务实例地址?
- 主动查询服务实例地址
- 采用订阅-推送方式
服务注册中心如何保证已注册的服务地址列表总是可用的呢?
地址探活的方法?
- 主动探活。服务注册中心周期性地向每个已注册的服务地址发起探测请求,如果某地址探测成功,则认为这个地址是可用的;否则,认为这个地址已经失效,服务注册中心主动摘除这个地址。
- 心跳探活。服务注册中心为每个已注册的服务地址记录一个最近心跳时间,服务实例启动后,每隔一段时间(如30s)就向服务注册中心发送一个心跳包,服务注册中心收到心跳包后更新对应服务地址的最近心跳时间。服务注册中心会启动定时器来检查每个服务地址的最近心跳时间相比于当前时间已经过了多久,如果其超过了某个阈值 (如150s),那么就可以认为这个服务实例已经不可用了,服务注册中心将其地址摘除。
当服务的地址列表发生变更时,服务注册中心需要主动推送最新地址列表给调用者。推送数据可能遇到什么问题?
- 推送风暴
如何解决推送风暴问题?