【探索SpringCloud】服务发现

前言

今天,我们来聊聊SpringCloud服务发现。主要有如下几个议题:
一、服务发现的概念与方案;二、SpringCloud是如何与各个服务注册厂商进行集成的。

服务发现

在微服务架构中,我们不可避免的需要通过服务间的调用来完成系统功能。于是我们面临的第一个问题就是:怎么知道目标服务的IP?如果只有一个IP,问题不大,直接写死在URL里访问就是了。但是遗憾的是,为了保证服务的高可用,通常都是多台实例部署。除此之外,在某些时候,系统搞活动,存在突发流量,那么我们还会存在动态扩容。这意味着,我们的服务实例个数可能都是不固定的。我们总不能因为服务实例变更就发布版本改地址吧?怎么办呢?

我们需要一个能够自动地动态地感知服务实例的组件:服务注册中心。

怎么做到的

首先,需要提醒一下,服务发现的实现是一个协作的过程,并不是引入某个组件就自动实现了。协作的步骤:

  1. 服务提供者将自己的信息(包括,服务名、IP、端口等)注册到服务注册中心。
  2. 服务消费者从服务注册中心拉取目标服务实例信息。
  3. 服务消费者根据目标服务实例信息改写请求地址,请求目标服务。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjOJQM6V-1684068242693)(https://www.baeldung.com/wp-content/uploads/sites/4/2022/01/Service-Discovery-1-1.png)]

使用模式

当我们有了服务注册中心之后,我们便有了服务发现的基础。不过,RPC有两端(服务端、客户端),所以完成服务发现也就存在两种模式。其本质区别就是:谁来发现服务。

服务端-服务发现

由服务端负责服务发现,客户端只需要调用某个固定的地址就行。为了实现这个目标,服务端需要引入一个新的组件:路由,又叫网关。路由会从服务注册中心拉取服务实例,并选择实例,以及转发请求。

服务端服务发现

客户端-服务发现

由客户端负责服务发现,自主选择服务实例进行调用。从服务发现的角度看,只要完成从服务注册中心拉取到服务实例列表,就算完成了。但是我们知道服务实例列表只是为了完成服务调用,仅仅只是个开始。客户端还要完成服务选择(负载均衡)、服务调用。

  • 典型应用:内部系统互相调用。

客户端服务发现

服务发现的实现

名称CAP一致性协议描述
NacosCP/APCP:蚂蚁金服-JRaft; AP:阿里-DistroNacos的服务提供者可自行选择CP/AP,默认AP
EurekaAP尽最大努力复制(非真正意义上的协议)来自Netflix. Spring Cloud Netflix虽然依然在更新,但其底层依赖的Eureka 2.0.0也已经不维护了
ZookeeperCPZABDubbo默认使用Zookeeper
ConsulCPRaft客户端使用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,需要:

  1. 实现ServiceRegistry,以便通过他来与服务注册中心进行交互,完成注册。
  2. 实现DiscoveryClient,便于与SpringCloud的其他组件进行整合,为后续的负载均衡、远程调用打下基础。
  3. 实现Registration接口,统一服务实例信息的获取。这个接口没有单独拎出来,因为重要是一些getter方法。
  4. 实现AbstractAutoServiceRegistration,以便通过Spring的事件监听机制,触发向服务注册中心注册当前实例的这个动作。

下面是比较常见的几个注册中心的相关组件实现。

ServiceRegistryDiscoveryClientRegistrationAbstractAutoServiceRegistration
ZookeeperServiceRegistryZookeeperDiscoveryClientZookeeperRegistrationZookeeperAutoServiceRegistration
CousulServiceRegistryCousulDiscoveryClientCousulRegistrationCousulAutoServiceRegistration
NacosServiceRegistryNacosDiscoveryClientNacosRegistrationNacosAutoServiceRegistration
EurekaServiceRegistryEurekaDiscoveryClientEurekaRegistration-

如上面聊到并不是所有的服务中心客户端都选择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,但实现了几个关键接口:

  1. 自动注册的标记接口:AutoServiceRegistration
  2. SmartLifeCycle,通过start方法来完成注册。
  3. 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为例,试着更深入的理解服务注册中心的:
服务续约、服务剔除、服务下线、服务发现。

这篇文章拖太久了,最近虽然有些忙,但自己确实懒惰了一些。加油吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值