前言
今天,我们来聊聊SpringCloud服务发现。主要有如下几个议题:
一、服务发现的概念与方案;二、SpringCloud是如何与各个服务注册厂商进行集成的。
服务发现
在微服务架构中,我们不可避免的需要通过服务间的调用来完成系统功能。于是我们面临的第一个问题就是:怎么知道目标服务的IP?如果只有一个IP,问题不大,直接写死在URL里访问就是了。但是遗憾的是,为了保证服务的高可用,通常都是多台实例部署。除此之外,在某些时候,系统搞活动,存在突发流量,那么我们还会存在动态扩容。这意味着,我们的服务实例个数可能都是不固定的。我们总不能因为服务实例变更就发布版本改地址吧?怎么办呢?
我们需要一个能够自动地动态地感知服务实例的组件:服务注册中心。
怎么做到的
首先,需要提醒一下,服务发现的实现是一个协作的过程,并不是引入某个组件就自动实现了。协作的步骤:
- 服务提供者将自己的信息(包括,服务名、IP、端口等)注册到服务注册中心。
- 服务消费者从服务注册中心拉取目标服务实例信息。
- 服务消费者根据目标服务实例信息改写请求地址,请求目标服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjOJQM6V-1684068242693)(https://www.baeldung.com/wp-content/uploads/sites/4/2022/01/Service-Discovery-1-1.png)]
使用模式
当我们有了服务注册中心之后,我们便有了服务发现的基础。不过,RPC有两端(服务端、客户端),所以完成服务发现也就存在两种模式。其本质区别就是:谁来发现服务。
服务端-服务发现
由服务端负责服务发现,客户端只需要调用某个固定的地址就行。为了实现这个目标,服务端需要引入一个新的组件:路由,又叫网关。路由会从服务注册中心拉取服务实例,并选择实例,以及转发请求。
- 典型应用:外部系统调用内部系统。
- Spring Cloud提供的路由:Spring Cloud Gateway
客户端-服务发现
由客户端负责服务发现,自主选择服务实例进行调用。从服务发现的角度看,只要完成从服务注册中心拉取到服务实例列表,就算完成了。但是我们知道服务实例列表只是为了完成服务调用,仅仅只是个开始。客户端还要完成服务选择(负载均衡)、服务调用。
- 典型应用:内部系统互相调用。
服务发现的实现
名称 | CAP | 一致性协议 | 描述 |
---|---|---|---|
Nacos | CP/AP | CP:蚂蚁金服-JRaft; AP:阿里-Distro | Nacos的服务提供者可自行选择CP/AP,默认AP |
Eureka | AP | 尽最大努力复制(非真正意义上的协议) | 来自Netflix. Spring Cloud Netflix虽然依然在更新,但其底层依赖的Eureka 2.0.0也已经不维护了 |
Zookeeper | CP | ZAB | Dubbo默认使用Zookeeper |
Consul | CP | Raft | 客户端使用gossip协议。大多在ServiceMesh使用 |
注:Nacos是个神奇又新鲜的玩意儿。他把一致性直接做到数据层面,意味着他可以同时存在基于AP协议实现一致性的数据,以及基于CP协议实现一致性的数据。对Nacos的设计和实现感兴趣的同学,可以看看官方的《Nacos架构&原理》
Spring Cloud的服务发现
到这里,我们知道服务发现有两个重大步骤:一是服务提供者注册服务,一是服务消费者拉取服务实例列表。为此,Spring Cloud也是有两个对应的抽象。
服务注册:ServiceRegistry
package org.springframework.cloud.client.serviceregistry;
/**
* Contract to register and deregister instances with a Service Registry.
* 按照服务注册中心的约定,注册/注销实例。
*
* @param <R> registration meta data
* @author Spencer Gibb
* @since 1.2.0
*/
public interface ServiceRegistry<R extends Registration> {
/**
* 注册实例。一个典型的注册信息包括:主机名和端口
* @param registration 注册信息,实例元数据。
*/
void register(R registration);
/**
* 注销实例。
* @param registration 注册信息,实例元数据。
*/
void deregister(R registration);
/**
* 关闭服务注册,这是个生命周期方法.
* 关闭服务注册,将会销毁注册信息,同时不再保持心跳等实例保活机制。但不影响服务注册中心的运行。
*/
void close();
/**
* 设置注册信息的状态。状态值由独立的实现决定.
* @param registration 需要更新的注册信息.
* @param status 要设置的状态
* @see org.springframework.cloud.client.serviceregistry.endpoint.ServiceRegistryEndpoint
*/
void setStatus(R registration, String status);
/**
* 获取特定的注册信息。
* @param registration 需要查询的注册信息.
* @param <T> 状态的类型信息.
* @return 注册信息的状态.
* @see org.springframework.cloud.client.serviceregistry.endpoint.ServiceRegistryEndpoint
*/
<T> T getStatus(R registration);
}
有了这个抽象,不管我们选择什么实现,都能方便地切换了。
服务发现客户端:DiscoveryClient
public interface DiscoveryClient extends Ordered {
// 获取某个服务的所有实例
List<ServiceInstance> getInstances(String serviceId);
// 获取所有服务名
List<String> getServices();
}
DiscoveryClient作为SpringCloud的抽象层,便于SpringCloud统一调用接口,而无需关系底层实现。其核心方法就两个,见上面的源码。
自动注册原理
ServiceRegistry相当于提供了个工具,但怎么使用这个工具进行注册,自动注册,Spring Cloud提供的默认实现是:事件监听机制。
AutoServiceRegistration
这个接口目前算是个标记接口,但他也算提供了未来扩展的可能。
为了便于提供商接入,提供了AbstractAutoServiceRegistration抽象类,而他就是自动注册的关键。
/**
* 为了便于ServiceRegistry的实现,提供的有用且通用的生命周期方法。
*/
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {
@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
// 会调用到start方法
}
public void start() {
// 1. 检查是否开启自动注册
// 2. 如果尚未注册过,则进行注册。
if (!this.running.get()) {
// 2.1 发布预注册事件:InstancePreRegisteredEvent
...
// 2.2 注册实例,并检查是否需要注册本地管理服务(JMX)
register();
if (shouldRegisterManagement()) {
registerManagement();
}
// 2.3 发布注册事件:InstanceRegisteredEvent
...
// 2.4 将状态改为已注册
this.running.compareAndSet(false, true);
}
}
protected void register() {
this.serviceRegistry.register(getRegistration());
}
/**
* Registration就是当前实例的基本信息
*/
protected abstract R getRegistration();
@PreDestroy
public void destroy() {
// 服务关机下线,要取消注册
stop();
}
public void stop() {
if (this.getRunning().compareAndSet(true, false) && isEnabled()) {
deregister();
if (shouldRegisterManagement()) {
deregisterManagement();
}
this.serviceRegistry.close();
}
}
}
可以看到这个抽象类,通过监听WebServerInitializedEvent事件来做的自动注册。正是这点,也使得通过该接口实现自动注册的服务中心客户端,绑定了Spring内置的Tomcat。因为只有内置的tomcat,才会通过spring的上下文发布该事件。
对于客户端,额,对的。这些接口/组件都是客户端的,是集成我们的应用上的。这里提醒一下大家哈。对于我们的应用而言,只要启动完成后,将应用注册到服务中心就行了。因此,并不是所有的服务中心客户端都那样实现。只要我们能知道服务器的IP/机器名和端口,我们自己就能实现。举个简单的例子:自定义配置项把IP和端口配置上。实现上下文监听器监听ContextRefreshedEvent即可。
而关于如何在非内置tomcat的应用实现自动注册,在Nacos的Issue也有讨论,感兴趣的可以自己看看:tomcat部署没有自动注册服务#341
小结
从上面的抽象接口,可以发现一个服务注册中心的客户端,要对接SpringCloud,需要:
- 实现ServiceRegistry,以便通过他来与服务注册中心进行交互,完成注册。
- 实现DiscoveryClient,便于与SpringCloud的其他组件进行整合,为后续的负载均衡、远程调用打下基础。
- 实现Registration接口,统一服务实例信息的获取。这个接口没有单独拎出来,因为重要是一些getter方法。
- 实现AbstractAutoServiceRegistration,以便通过Spring的事件监听机制,触发向服务注册中心注册当前实例的这个动作。
下面是比较常见的几个注册中心的相关组件实现。
ServiceRegistry | DiscoveryClient | Registration | AbstractAutoServiceRegistration |
---|---|---|---|
ZookeeperServiceRegistry | ZookeeperDiscoveryClient | ZookeeperRegistration | ZookeeperAutoServiceRegistration |
CousulServiceRegistry | CousulDiscoveryClient | CousulRegistration | CousulAutoServiceRegistration |
NacosServiceRegistry | NacosDiscoveryClient | NacosRegistration | NacosAutoServiceRegistration |
EurekaServiceRegistry | EurekaDiscoveryClient | EurekaRegistration | - |
如上面聊到并不是所有的服务中心客户端都选择AbstractAutoServiceRegistration来实现自动注册那样,上面表格中的Eureka就是一个例子。Eureka留了一手,我们一起来看看:
public class EurekaAutoServiceRegistration implements AutoServiceRegistration,
SmartLifecycle, Ordered, SmartApplicationListener {
@Override
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}
if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
this.registration.setSecurePort(this.port.get());
}
}
// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
this.serviceRegistry.register(this.registration);
this.context.publishEvent(new InstanceRegisteredEvent<>(this,
this.registration.getInstanceConfig()));
this.running.set(true);
}
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof WebServerInitializedEvent) {
onApplicationEvent((WebServerInitializedEvent) event);
}
else if (event instanceof ContextClosedEvent) {
onApplicationEvent((ContextClosedEvent) event);
}
}
}
虽然没有实现AbstractAutoServiceRegistration,但实现了几个关键接口:
- 自动注册的标记接口:AutoServiceRegistration
- SmartLifeCycle,通过start方法来完成注册。
- SmartApplicationListener,通过监听WebServerInitializedEvent事件来完成注册。
这样当应用运行在外置的tomcat中,则SmartLifeCycle发挥作用,完成注册。而当运行在内置的tomcat中,则与SpringCloud原生的接口一样,通过监听WebServerInitializedEvent完成注册。
参考
Pattern: Server-side service discovery
Pattern: Client-side service discovery
Service Discovery in Microservices
服务发现技术选型那点事儿
5种微服务注册中心如何选型?这几个维度告诉你
java源码详解系列(十二)–Eureka的使用和源码
Springboot War包部署下nacos无法注册问题
Eureka核心源码解析系列(一)- 服务注册、续约篇
后记
这次咱们探讨了在SpringCloud中是如何完成服务自动注册的。下次,咱们就以Nacos为例,试着更深入的理解服务注册中心的:
服务续约、服务剔除、服务下线、服务发现。
这篇文章拖太久了,最近虽然有些忙,但自己确实懒惰了一些。加油吧。