2.4 Spring Cloud统一服务注册/发现编程模型
Spring Cloud统一了服务注册和服务发现编程模型,其代码在spring-cloud-commons模块里,相关接口的说明如表2-2所示。
表2-2
统一编程模型有以下两个优点。
·无须关注底层服务注册/发现的实现细节,只需了解上层统一的抽象。
·更换注册中心非常简单,只需修改 maven 依赖和对应的注册中心配置信息(比如注册中心地址、namespace、group等)。
2.4.1 DiscoveryClient和ReactiveDiscoveryClient
DiscoveryClient和ReactiveDiscoveryClient代表Consumer从注册中心发现Provider的服务发现操作,接口定义如下:
2.2节和2.3节介绍的Alibaba Nacos和Netflix Eureka服务注册/发现实例中都是直接注入DiscoveryClient完成服务实例的获取。它们对应的DiscoveryClient 是依赖内部自动化配置类产生的NacosDiscoveryClient和EurekaDiscoveryClient。
ReactiveDiscoveryClient是在Spring Cloud Hoxton M3中首次加入的基于响应式的服务发现接口。
ReactiveDiscoveryClient 接口中的方法与 DiscoveryClient 几乎相同,只是把 List 类型转换成了Reactor里的Flux类型。
在Spring WebFlux场景下,ReactiveDiscoveryClient会生效,这个NacosReactiveConsumer启动类内部的逻辑就是使用ReactiveDiscoveryClient的一个场景。
上述代码中:
① 注入 ReactiveDiscoveryClient,在 WebFlux场景下,NacosReactiveDiscoveryClient-Configuration会自动构造NacosReactiveDiscoveryClient。
② 使用 ReactiveDiscoveryClient#getServices 方法得到 Flux<String>。注意,这里是一个Reactor的Flux类型。
③得到Flux<ServiceInstance>,然后使用map方法进行映射。
④使用WebClient这种reactive客户端去调用服务。
2.4.2 Servicelnstance和Registration
服务信息在每个注册中心的内部数据存储结构都有各自的模型。如图2-12 所示,Zookeeper注册中心内部的数据模型是一种树状结构,这棵树由不同的节点组成。顶层是“/”节点,“/”节点有两个子节点,分别是/app1 和/app2,所有注册的实例信息在Zookeeper里都是一个节点,这个节点在Zookeeper内部被称为ZNode。
图2-12
ZNode在 Zookeeper Curator客户端中的对应类是org.apache.curator.x.discovery.ServiceInstance,该类内部包含name、id、address、port、sslPort、payload等属性。
如图2-13 所示,这是Eureka注册中心内部的数据模型。所有注册的实例信息在Eureka内都是一个 InstanceInfo 对象实例,该类内部包含 instanceId、appName、status、port、asgName、dataCenterInfo、metadata等属性。
图2-13
如图2-14所示,这是Nacos注册中心内部的数据模型。Nacos服务信息由3元组namespace (命名空间用于数据隔离)、group(分组)和 serviceName(服务名)组成。所有注册的实例信息在 Nacos 内都是一个 com.alibaba.nacos.api.naming.pojo.Instance 对象实例,该类内部拥有instanceId、ip、port、weight、healthy、enabled、serviceName、cluseterName等属性。
图2-14
Spring Cloud提供的ServiceInstance和 Registration的作用是抽象实例在各种注册中心的数据模型。无论是 Zookeeper的 ServiceInstance、Eureka的 InstanceInfo,还是 Nacos的 Instance,在 Spring Cloud 体系内会被统一抽象为 ServiceInstance 和 Registration。其中,ServiceInstance表示客户端从注册中心获取到的实例数据结构,Registration表示客户端注册到注册中心的实例数据结构,其表现如图2-15所示。
图2-15
ServiceInstance接口定义如下:
使用 DiscoveryClient可以基于服务名获取到这个服务下的所有 ServiceInstance集合。比如,my-provider服务在注册中心可能会存在两个ServiceInstance,即service-instance1:192.168.1.1:8080和service-instance2:192.168.1.2:8080。
Registration接口定义如下:
可以看到,这个接口继承了 ServiceInstance,并且没有额外的方法定义。因为在从注册中心获取实例信息和把实例信息注册到注册中心这两个过程中,实例信息的存储结构完全可以相同。笔者预测将来这个接口可能会新增一些方法。那么,哪个接口使用 Registration 注册服务呢?那就是下面要介绍的ServiceRegistry。
2.4.3 ServiceRegistry
ServiceRegistry用于服务信息的注册(register)和注销(deregister),其接口定义如下:
如图2-16所示,这是服务注册和注销过程的一个简单说明。
图2-16
AutoServiceRegistration 是一个空接口,表示自动完成服务注册过程。接口具体的实现类(如 NacosAutoServiceRegistration)会在 NacosServiceRegistryAutoConfiguration 自动化配置类中被自动构造。
AbstractAutoServiceRegistration抽象类实现了AutoServiceRegistration接口,同时也实现了ApplicationListener 接口并监听 WebServerInitializedEvent。收到事件后,使用 ServiceRegistry完成服务注册。应用程序关闭时触发@PreDestroy注解使用ServiceRegistry完成服务注销,这就是Spring Cloud服务信息的注册和注销时机。
提示:Spring Cloud 2.2.0.RELEASE之后的版本通过WebServerInitializedEvent事件的监听完成服务注册已被声明为过期方法。Spring Cloud 把注册时机的决定权交给了各个注册中心实现。
2.4.4 ServiceRegistryEndpoint
ServiceRegistryEndpoint是Spring Cloud服务注册/发现功能对外暴露的Endpoint,其ID是service-registry,用于获取和设置服务实例的状态。获取和设置的动作由 ServiceRegistry 的getStatus和setStatus方法完成。
在NacosProvider 项目中加上org.springframework.boot:spring-boot-starter-actuator 依赖,配置management.endpoints.web.exposure.include=*后进行启动,启动后执行如下脚本:
如图2-17 所示,执行后可看到在Nacos控制台的“操作”列下的“下线”按钮变成了“上线”按钮。
图2-17
ServiceRegistryEndpoint非常有用,可以完成应用的无损下线。应用的无损下线是什么意思呢?比如某个应用有10个实例,某天需要发布新版本,发布新版本意味着需要先下线部分实例(下线旧版本),然后使用新的部署包发布新版本,以此类推,直到所有的旧版本都被替换成新版本。无损下线的意思是在旧版本下线的过程中,服务调用不发生任何错误。
Spring Cloud默认的服务实例更新机制会每隔30s去注册中心获取服务对应的实例列表信息来覆盖内存里的实例信息。如果一些实例在某个时间点完成下线操作,但是调用这个服务的客户端因为还没达到30s刷新时间,其内存中的服务实例列表中存在已经下线的实例,调用这些已经下线的实例的服务接口会发生连接超时异常(这个实例服务已经下线,对应的端口未开启,客户端访问会超时)。这就需要无损下线来避免异常的发生。
使用ServiceRegistryEndpoint让应用无损下线的思路如下:
① 调用ServiceRegistryEndpoint,将需要下线的实例的服务下线。
② 服务下线之后,等待客户端达到30s刷新时间,通过刷新实例列表信息来删除已下线的实例(这时候只是服务下线,实例并未下线)。
③实例下线(服务发现已经不再添加该实例,可以放心下线实例)。
调用ServiceRegistryEndpoint的过程如下:
①在依赖文件中加上org.springframework.boot:spring-boot-actuator组件。
②在配置文件中加上management.endpoints.web.exposure.include=service-registry。
③ 使用 curl--header "Content-Type:application/json"-XPOST http://host:port/actuator/service-registry-d'{"status":"DOWN"}'完成服务下线(如果是误操作,需要重新上线,只需将DOWN改成UP即可)。