Spring Cloud 统一服务注册/发现编程模型

Spring Cloud 统一服务注册和发现编程模型,其中代码在spring-cloud-commons模块

接口 作用
org.springframework.cloud.client.discovery.DiscoveryClient 代表服务发现常见的读取操作
org.springframework.cloud.client.discovery.EnableDiscoveryClient 使用该注解表示开启服务发现功能
org.springframework.cloud.client.discovery.ReactiveDiscoveryClient 基于响应式的代表服务发现常见的读取操作
org.springframework.cloud.client.serviceregistry.ServiceRegistry 注册与销毁服务的操作封装
org.springframework.cloud.client.ServiceInstance 代表服务的一个实例

统一编程模型优点

  • 无须关注底层服务的实现注册和发现的细节,只需要了解上层统一的抽象
  • 更换注册中心非常简单,只需要改maven依赖和对应的注册中心配置信息

DiscoveryClient 和ReactiveDiscoveryClient

两个都是从注册中心发现provider的服务操作,ReactiveDiscoveryClient是在Hoxton M3版本加入的响应式的注册发现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface DiscoveryClient extends Ordered {
// 默认优先级,多个DiscoveryClient 存在的情况下以优先级排序
int DEFAULT_ORDER = 0;
/**
* 具体服务发现组建的描述信息,在health Indicator中会被用到
* @return 描述信息
*/
String description();
// 根据服务名称查询所有的服务实例
List<ServiceInstance> getInstances(String serviceId);
// 返回注册中心所有的服务名称
List<String> getServices();

default void probe() {
this.getServices();
}

default int getOrder() {
return 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public interface ReactiveDiscoveryClient extends Ordered {

/**
* Default order of the discovery client.
*/
int DEFAULT_ORDER = 0;

/**
* A human-readable description of the implementation, used in HealthIndicator.
* @return The description.
*/
String description();

/**
* Gets all ServiceInstances associated with a particular serviceId.
* @param serviceId The serviceId to query.
* @return A List of ServiceInstance.
*/
Flux<ServiceInstance> getInstances(String serviceId);

/**
* @return All known service IDs.
*/
Flux<String> getServices();

/**
* Can be used to verify the client is still valid and able to make calls.
* <p>
* A successful invocation with no exception thrown implies the client is able to make
* calls.
* <p>
* The default implementation simply calls {@link #getServices()} - client
* implementations can override with a lighter weight operation if they choose to.
*/
default void probe() {
getServices();
}

/**
* Default implementation for getting order of discovery clients.
* @return order
*/
@Override
default int getOrder() {
return DEFAULT_ORDER;
}

}

ServiceInstance和Registration

Spring Cloud 提供的ServiceInstance和Registration的作用就是抽象实例在各种注册中心的数据模型,无论Zookeeper的Service Instance、Eureka的InstanceInfo,还是Nacos的Instance。在SpringCloud都会被统一抽象成Service Instance和Registration。其中ServiceInstance表示客户端从注册中心获取实例数据结构,Registration表示客户端注册到注册中心的实例数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface ServiceInstance { // 代表服务的一个实例

/**
* @return 实例ID,可以不现实默认返回null
*/
default String getInstanceId() {
return null;
}

/**
* @return 注册服务id
*/
String getServiceId();

/**
* @return The hostname of the registered service instance. 注册实例的hostname
*/
String getHost();

/**
* @return The port of the registered service instance. 服务实例的端口
*/
int getPort();

/**
* @return Whether the port of the registered service instance uses HTTPS. 是否使用的https
*/
boolean isSecure();

/**
* @return The service URI address. 服务实例的URI地址
*/
URI getUri();

/**
* @return The key / value pair metadata associated with the service instance. 服务实例的键值对的形式metadata信息
*/
Map<String, String> getMetadata();

/**
* @return The scheme of the service instance.
*/
default String getScheme() {
return null;
}

}

使用org.springframework.cloud.client.discovery.DiscoveryClient#getInstances可以基于服务名获取到这个服务下的所有ServiceInstance集合(List<ServiceInstance> )。比如,provider 服务在注册中心可能会存在两个Service Instance,即Service- instance1:x.x.x.x:8080和Service- instance2:x.x.x.x:8081.

1
2
public interface Registration extends ServiceInstance {
}

Registration接口继承了ServiceInstance,并且没有额外的方法定义。因为在注册中心获取实例信息和把实例信息注册到注册中心这个两个过程实例信息的储存结构完全可以相通。未来可能会在Registration接口中添加一些方法。

ServiceRegistration

ServiceRegistration使用了Registration接口,用于服务信息的注册(register)和注销(deregister)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public interface ServiceRegistry<R extends Registration> {

/**
* Registers the registration. A registration typically has information about an
* instance, such as its hostname and port. 基于实例信息将其注册到注册中心
* @param registration registration meta data 实例信息
*/
void register(R registration);

/**
* Deregisters the registration. 基于实例信息将其从注册中心注销
* @param registration registration meta data 实例信息
*/
void deregister(R registration);

/**
* Closes the ServiceRegistry. This is a lifecycle method. 关闭ServiceRegistry,这是一个生命周期方法
*/
void close();

/**
* Sets the status of the registration. The status values are determined by the
* individual implementations. 设置实例状态,状态值由各个实现确定。
* @param registration The registration to update. 服务实例
* @param status The status to set. 状态
* @see org.springframework.cloud.client.serviceregistry.endpoint.ServiceRegistryEndpoint
*/
void setStatus(R registration, String status);

/**
* Gets the status of a particular registration. 获取服务实例状态
* @param registration The registration to query.
* @param <T> The type of the status.
* @return The status of the registration.
* @see org.springframework.cloud.client.serviceregistry.endpoint.ServiceRegistryEndpoint
*/
<T> T getStatus(R registration);

}

服务注册和服务销毁的说明

org.springframework.cloud.client.serviceregistry.AutoServiceRegistration是一个空接口实现,表示自动完成服务注册过程。接口默认实现是org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration抽象类。

我工程引入eureka注册中心,可以看到有org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration实现类,然而会在org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration自动化装配类中被自动构造。

AbstractAutoServiceRegistration抽象实现了AutoServiceRegistration接口,同时也实现了Application Listener接口并且监听WebServerInitializedEvent。收到事件后,使用org.springframework.cloud.client.serviceregistry.ServiceRegistry#register完成服务的注册。应用程序关闭时出发@PreDestroy注解使用org.springframework.cloud.client.serviceregistry.ServiceRegistry完成服务注销。

Spring Cloud 2.2.0 之后通过WebServer InitializedEvent事件监听完成服务注册已经被声明为过期方法。SpringCloud 把注册时机的决定权交给了各个注册中心去现实。

ServiceRegistryEndpoint

Service RegistryEndpoint是Spring Cloud服务注册/发现功能对外报漏的Endpoint,其中ID是service-registry,用于获取和设置服务实例的状态。获取和设置的动作是由ServiceRegister的getStatus和setStatus方法完成。

在服务添加actuator依赖。配置management.endpoints.web.exposure.include=*后启动,然后执行下线操作。http://localhost:8080/actuator/service-registry?status=down

从注册中心就会看到服务下线状态。

ServiceRegistryEndPoint非常实用,可以完成应用无损下线。

无损下线:比如你有五台机器,某天向滚动更新发布新版的服务,你要下线一部分旧版本的服务,部署新版本服务。旧版本下线的时候,服务本地的client存旧服务下线服务地址,会导致调用失败。

因为SpringCloud 默认的服务实例更新机制是30s去注册中心获取服务对应的实例列表信息覆盖内存的实例信息。如果这个旧服务在30s就完成下线操作,但是调用的客户端也没有达到30s刷新时间,内存存储的服务已经被下线,这样就会调用接口就会发生超时异常。

使用ServiceRegistryEndpoint让应用无损下线思路:

  • 调用ServiceRegistryEndpoint,将需要下线的实例服务下线
  • 服务下线之后,等待客户端达到30s刷新时间,通过刷新实例列表信息删除已经下线实例(只是注册中心服务下线,实例服务并为下线)
  • 实例服务下线(服务发现已经不再添加该实例,可以放心下线实例)

双注册双订阅模式

双注册双订阅表示一个Provider应用可以将自身的实例信息注册到多个注册中心上,一个Consumer应用可以订阅到多个注册中心上的服务实例信息。

如图所示,Provider可以把自身的服务实例信息注册到Nacos和Eureka集群上,Consumer发起服务订阅的时候可以从Nacos和Eureka上订阅服务。

双注册双订阅模式分析

Spring Cloud自身的编程模型是支持双注册双订阅模式的。在服务注册侧,Spring Cloud各个注册中心都有AutoServiceRegistration的实现类,比如,NacosAutoServiceRegistration和EurekaAutoServiceRegistration实现在类内部完成服务的注册。这些AutoServiceRegistration的实现类都实现了 Lifecycle接口,在start过程中完成服务注册操作。

在服务订阅侧,DiscoveryClient 统一了Spring Cloud服务发现的操作。其中,CompositeDiscoveryClient是一个特殊的DiscoveryClient实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.springframework.cloud.client.discovery.composite;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;

public class CompositeDiscoveryClient implements DiscoveryClient {
private final List<DiscoveryClient> discoveryClients;

public CompositeDiscoveryClient(List<DiscoveryClient> discoveryClients) {
AnnotationAwareOrderComparator.sort(discoveryClients);
this.discoveryClients = discoveryClients;
}

public String description() {
return "Composite Discovery Client";
}

public List<ServiceInstance> getInstances(String serviceId) {
if (this.discoveryClients != null) {
Iterator var2 = this.discoveryClients.iterator();

while(var2.hasNext()) {
DiscoveryClient discoveryClient = (DiscoveryClient)var2.next();
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (instances != null && !instances.isEmpty()) {
return instances;
}
}
}

return Collections.emptyList();
}

public List<String> getServices() {
LinkedHashSet<String> services = new LinkedHashSet();
if (this.discoveryClients != null) {
Iterator var2 = this.discoveryClients.iterator();

while(var2.hasNext()) {
DiscoveryClient discoveryClient = (DiscoveryClient)var2.next();
List<String> serviceForClient = discoveryClient.getServices();
if (serviceForClient != null) {
services.addAll(serviceForClient);
}
}
}

return new ArrayList(services);
}

public List<DiscoveryClient> getDiscoveryClients() {
return this.discoveryClients;
}
}

在 getInstances 方法中,会聚合所有的 DiscoveryClient 实现类找到的服务名,也会遍历每个DiscoveryClient查询服务名对应的实例信息。

下面在一个应用里分别加上 Nacos (com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery)和 Eureka(org.springframework.cloud:spring-cloud-starter-netfix-eureka-client)依赖,用来完成双注册双订阅。

应用启动后,会出现以下报错信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
***************************
APPLICATION FAILED TO START
***************************

Description:

Field autoServiceRegistration in org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration required a single bean, but 2 were found:
- nacosAutoServiceRegistration: defined by method 'nacosAutoServiceRegistration' in class path resource [com/alibaba/cloud/nacos/registry/NacosServiceRegistryAutoConfiguration.class]
- eurekaAutoServiceRegistration: defined by method 'eurekaAutoServiceRegistration' in class path resource [org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.class]


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

从这个报错信息可以很明显地看出,ServiceRegistryAutoConfiguration 自动化配置类的内部类ServiceRegistryEndpointConfiguration内部依赖一个RegistrationBean,但是在Nacos和Eureka依赖内部分别会构造 NacosRegistration 和EurekaRegistration,这样会出现 ServiceRegistry-EndpointConfiguration 并不知道要注入哪个 Registration Bean 的问题。同理,AutoService-RegistrationAutoConfiguration内部的AutoServiceRegistration Bean也会引起一样的问题。

为了解决这个问题,可以在配置文件里过滤这两个自动化配置类:

1
spring.autoconfigure.exclude=org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration,org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration

加上该配置之后,还需要通过@EnableConfigurationProperties 注解让AutoServiceRegistration-Properties 配置类生效。这是因为所有的AutoServiceRegistration 实现类在构造过程中都需要这个配置类Bean。

有了这两个条件之后,即可享受双注册双订阅模式。

案例:使用双注册双订阅模式将Eureka注册中心迁移到Nacos注册中心

假设某公司原先使用 Eureka 作为注册中心,Nacos 开源之后,该公司想把Eureka替换成Nacos注册中心,要求在这个过程中对客户没有任何影响,也不能造成业务损失。

对于这个场景,可以使用双注册双订阅方案来完成任务。如图所示,这是一个3个阶段的过程图。

第1阶段:Eureka作为注册中心,Provider完成服务注册,Consumer完成服务发现。

第2阶段:双注册双订阅的核心阶段,该阶段内部包括以下4个操作。

上线新的Provider(拥有双注册能力),这时Eureka 注册中心的Provider有两个实例。
下线旧的 Provider,下线之后由于新 Provider 也会注册到 Eureka 上,这时旧的Consumer可以找到新Provider的实例。
上线新的 Consumer(拥有双订阅能力),新 Consumer 可以订阅 Nacos 和Eureka 集群的服务实例,这时可以订阅到Nacos上的服务实例。
下线旧的Consumer。
第3阶段:Eureka下线,使用Nacos替换Eureka作为新的注册中心,Provider和Consumer的服务注册和服务发现操作只与Nacos交互。