浅析Spring Cloud Consul(源码分析)

浅析Spring Cloud Consul(源码分析)

Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。Spring Cloud Consul 项目是针对Consul的服务治理实现。项目中有运用到,因此做个简要分析。实际上Spring Cloud已经搭建了一套模版,各个注册服务(Cloud、Eureka等)只需要根据自身的差异进行实现即可。

1. 服务注册

1.1 主要类说明

1.1.1 Registration

Registration和ServiceInstance是由spring-cloud-commons提供了接口。

Registration是一个标记接口,继承了ServiceInstance接口,代表一个抽象的服务实例。ServiceInstace接口,提供了serviceId、host、port等get方法定义。Consul对于Registration的具体实现就是ConsulRegistration,Consul自己的服务实例就是NewService对象,ConsulRegistration实现了ServiceInstace接口方法(内部的实现,是通过从NewService对象上取值)。而ConsulAutoRegistration,是Consul自己对服务自动注册的扩展实例,内部有自己特殊的方式。

1.1.2 AutoServiceRegistration

AutoServiceRegistration和AbstractAutoServiceRegistration,这两个类(或接口)都是spring-cloud-commons提供。

AutoServiceRegistration是一个标记接口,它的抽象实现是AbstractAutoServiceRegistration。AbstractAutoServiceRegistration提供了模板功能,实现了start方法提供服务自动注册能力,实现了register、deregister等protected方法。也提供了getRegistration()、isEnable()等子类需要实现的方法。在Consul中,该抽象类的具体实现就是ConsulAutoServiceRegistration,由该类完成自动注册的一些逻辑判断。

1.1.3 ServiceRegistry

ServiceRegistry接口是spring-cloud-commons提供。

ServiceRegistry是一个接口,对于服务实例Registration,提供了服务注册、服务注销等方法。在Consul中,该类的具体实现是ConsulServiceRegistry。

java 复制代码
public interface ServiceRegistry<R extends Registration> {
    void register(R registration);
    void deregister(R registration);
    void close();
    void setStatus(R registration, String status);
    <T> T getStatus(R registration);
}

1.1.4 其他类

1)ConsulAutoServiceRegistrationListener:实现SmartApplicationListener,利用Spring事件机制的扩展点,触发服务自动注册

2)ConsulRegistrationCustomizer:Consul为ConsulRegistration提供的扩展点。

3)ConsulServiceRegistryAutoConfiguration:主要是ConsulServiceRegistry的配置类

4)ConsulAutoServiceRegistrationAutoConfiguration:主要是ConsulAutoRegistration和ConsulServiceAutoRegistration的配置类。

1.2 自动注册

先抛开配置类,结合刚刚分析的各个类,先看下Consul如何实现自动注册。

1)ConsulAutoServiceRegistrationListener类是服务注册的入口,该类实现了SmartApplicationListener接口,实现了具体方法onApplicationEvent。

java 复制代码
//ConsulAutoServiceRegistrationListener#onApplicationEvent
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
   if (applicationEvent instanceof WebServerInitializedEvent) {
      WebServerInitializedEvent event = (WebServerInitializedEvent) applicationEvent;

      ApplicationContext context = event.getApplicationContext();
      if (context instanceof ConfigurableWebServerApplicationContext) {
         if ("management"
               .equals(((ConfigurableWebServerApplicationContext) context)
                     .getServerNamespace())) {
            return;
         }
      }
      //通过WebServerInitializedEvent可以获取到WebServer实例,进而获取到启动端口
      this.autoServiceRegistration.setPortIfNeeded(event.getWebServer().getPort());
      //执行自动注册逻辑
      this.autoServiceRegistration.start();
   }
}

从上述代码,可以看出,关注的Event是WebServerInitializedEvent,该事件是在Servlet容器启动后触发。在onApplicationEvent方法中,会调用ConsulAutoServiceRegistration的start方法。

2)ConsulAutoServiceRegistration的start方法,仅调用抽象父类的start方法

java 复制代码
//ConsulAutoServiceRegistration#start
@Override
@Retryable(interceptor = "consulRetryInterceptor")
public void start() {
   super.start();
}

3)AbstractAutoServiceRegistration的start方法,主要是控制服务注册的流程。把扩展点预留,让子类实现。

java 复制代码
//AbstractAutoServiceRegistration#start
public void start() {
    //抽象方法,子类实现
    if (!this.isEnabled()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Discovery Lifecycle disabled. Not starting");
        }

    } else {
        if (!this.running.get()) {
            //发布InstancePreRegisteredEvent事件
            this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
            //protected方法,子类可以覆盖抽闲类方法。
            this.register();
            if (this.shouldRegisterManagement()) {
                this.registerManagement();
            }
			//发布InstanceRegisteredEvent事件
            this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
            this.running.compareAndSet(false, true);
        }

    }
}

4)重点看下ConsulAutoServiceRegistration如何重写方法。isEnabled()取决于配置中的lifecycle(配置spring.cloud.consul.discovery.lifecycle.enable),该对象默认为true。register()方法中增加了一层配置判断,spring.cloud.consul.discovery.register,该配置默认为true,然后调用父类的register方法。AbstractAutoServiceRegistration中的register方法,则是调用ServiceRegistry接口中的register方法,进行实际注册。在Consul中就是ConsulServiceRegistry

java 复制代码
//ConsulAutoServiceRegistration#start 
@Override
protected boolean isEnabled() {
   return this.properties.getLifecycle().isEnabled();
}
java 复制代码
//ConsulAutoServiceRegistration#register
@Override
protected void register() {
   if (!this.properties.isRegister()) {
      log.debug("Registration disabled.");
      return;
   }

   super.register();
}
java 复制代码
//AbstractAutoServiceRegistration#register
protected void register() {
    this.serviceRegistry.register(this.getRegistration());
}

5)getRegistration()具体在ConsulAutoServiceRegistration方法里实现,返回一个ConsulAutoRegistration对象。

java 复制代码
//ConsulAutoServiceRegistration#getRegistration
@Override
protected ConsulAutoRegistration getRegistration() {
   if (this.registration.getService().getPort() == null
         && this.getPort().get() > 0) {
      this.registration.initializePort(this.getPort().get());
   }
   Assert.notNull(this.registration.getService().getPort(),
         "service.port has not been set");
   return this.registration;
}

6)ConsulServiceRegistry调用register方法,进行实际的注册逻辑。主要是想consul注册服务实例。

java 复制代码
//ConsulServiceRegistry#register
@Override
public void register(ConsulRegistration reg) {
   log.info("Registering service with consul: " + reg.getService());
   try {
       //向consul注册服务实例
      this.client.agentServiceRegister(reg.getService(),
            this.properties.getAclToken());
      NewService service = reg.getService();
      //如果健康检查的方式是心跳,则提交一个定时任务,持续向consul agent发送。
      if (this.heartbeatProperties.isEnabled() && this.ttlScheduler != null
            && service.getCheck() != null
            && service.getCheck().getTtl() != null) {
         this.ttlScheduler.add(reg.getInstanceId());
      }
   }
   catch (ConsulException e) {
       //如果快速失败,直接把异常抛出。
      if (this.properties.isFailFast()) {
         log.error("Error registering service with consul: " + reg.getService(),
               e);
         ReflectionUtils.rethrowRuntimeException(e);
      }
      log.warn("Failfast is false. Error registering service with consul: "
            + reg.getService(), e);
   }
}

1.3 自动注销

自动注销,则是依赖于@PreDestroy注解,在bean结束生命周期的时候,通过ServiceRegistry取消注册。具体代码如下

java 复制代码
//AbstractAutoServiceRegistration#destroy
@PreDestroy
public void destroy() {
   stop();
}
java 复制代码
//AbstractAutoServiceRegistration#stop
public void stop() {
   if (this.getRunning().compareAndSet(true, false) && isEnabled()) {
      deregister();
      if (shouldRegisterManagement()) {
         deregisterManagement();
      }
      this.serviceRegistry.close();
   }
}
java 复制代码
//AbstractAutoServiceRegistration#deregister
protected void deregister() {
   this.serviceRegistry.deregister(getRegistration());
}

1.4 自动配置

服务注册之所以能这么方便,是因为用了Spring-boot的自动配置机制。详细如下

spring.factories中配置了,注入的Bean

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationAutoConfiguration,\
org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration,\
......

1.4.1 ConsulServiceRegistryAutoConfiguration

  • @ConditionalOnConsulEnabled是spring-cloud-consul-core提供的注解,要求两点:spring.cloud.consul.enabled为true(默认为true);ConsulClient.class类存在

  • @Conditional后面的的要求有两点:spring.cloud.service-registry.enabled为true(默认为true);spring.cloud.consul.service-registry.enabled为true(默认为true)。

  • @AutoConfigureBefore的意思是:ConsulServiceRegistryAutoConfiguration在ServiceRegistryAutoConfiguration之前加载。

java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnConsulEnabled
@Conditional(ConsulServiceRegistryAutoConfiguration.OnConsulRegistrationEnabledCondition.class)
@AutoConfigureBefore(ServiceRegistryAutoConfiguration.class)
public class ConsulServiceRegistryAutoConfiguration

另外ConsulServiceRegistryAutoConfiguration注册了三个Bean(在没有该Bean的情况下),

  1. ConsulServiceRegistry:直接操作ConsulClient,提供服务注册功能。实现ServiceRegistry接口。

  2. HeartbeatProperties:健康检查的心跳检测方式的配置类

  3. ConsulDiscoveryProperties:服务注册与发现的相关配置。

1.4.2 ConsulAutoServiceRegistrationAutoConfiguration

  • @ConditionalOnConsulEnable前面已经提到过了。

  • @Conditional后面的要求有4点,要求同时满足:

    1. spring.cloud.service-registry.auto-registration.enabled配置为true。(默认为true)
    2. spring.cloud.consul.service-registry.auto-registration.enabled配置为true。(默认为true)
    3. spring.cloud.service-registry.enabled配置为true。(默认为true)
    4. spring.cloud.consul.service-registry.enabled配置为true。(默认为true)
  • @AutoConfigureAfter表示,ConsulAutoServiceRegistrationAutoConfiguration在AutoServiceRegistrationConfiguration和ConsulServiceRegistryAutoConfiguration之后加载。

java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnMissingBean(
      type = "org.springframework.cloud.consul.discovery.ConsulLifecycle")
@ConditionalOnConsulEnabled
@Conditional(ConsulAutoServiceRegistrationAutoConfiguration.OnConsulRegistrationEnabledCondition.class)
@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class,
      ConsulServiceRegistryAutoConfiguration.class })
public class ConsulAutoServiceRegistrationAutoConfiguration

另外ConsulAutoServiceRegistrationAutoConfiguration注册了三个Bean

  1. ConsulAutoRegistration:代表当前服务实例。

  2. ConsulAutoServiceRegistration:提供服务注册功能。实现了AbstractAutoServiceRegistration抽象类。

  3. ConsulAutoServiceRegistrationListener:服务自动注册的入口。实现了SmartApplicationListener接口。

2. 服务发现

2.1 DiscoveryClient实现

DiscoveryClient是spring-cloud-common提供了的一个接口。核心就两个方法

java 复制代码
//DiscoveryClient
public interface DiscoveryClient extends Ordered {
	//根据serviceId查询所有服务实例
    List<ServiceInstance> getInstances(String serviceId);
	//查询所有serviceId
    List<String> getServices();
}

Consul对于该接口的实现,就是ConsulDiscoveryClient

java 复制代码
//ConsulDiscoveryClient#getInstances
@Override
public List<ServiceInstance> getInstances(final String serviceId) {
   return getInstances(serviceId,
         new QueryParams(this.properties.getConsistencyMode()));
}
java 复制代码
//ConsulDiscoveryClient#getServices
@Override
public List<String> getServices() {
   String aclToken = this.properties.getAclToken();

   CatalogServicesRequest request = CatalogServicesRequest.newBuilder()
         .setQueryParams(QueryParams.DEFAULT)
         .setToken(this.properties.getAclToken()).build();
   return new ArrayList<>(
         this.client.getCatalogServices(request).getValue().keySet());
}

2.2 结合Ribbon实现

1)ConsulServer。继承com.netflix.loadbalancer的Server类。

2)ConsulServerList。继承com.netflix.loadbalancer的抽象类AbstractServerList类,实现ServerList接口。

3)HealthServiceServerListFilter:实现com.netflix.loadbalancer的ServerListFilter接口。

4)ConsulPing:实现com.netflix.loadbalancer的IPing接口。

5)ConsulServerIntrospector。继承org.springframework.cloud.netflix.ribbon的DefaultServerIntrospector类

2.3 其他

ConsulCatalogWatch:启动一个定时任务,每秒更新consulIndex,并且发送HeartbeatEvent事件。暂不清楚,这个类的意义。

3. 健康监测

3.1 Consul健康检查方式

ConsulAgent负责健康检查工作,主要有以下几种方式:

1)Script + Interval:定时执行脚本,根据脚本返回码确定应用是否存活,0表示存活,1表示警告,其他表示死亡。

2)HTTP + Interval:定时向应用某个端点发送http请求,根据http状态码确认应用是否存活,2xx表示存活。

3)TCP + Interval:定时发起tcp连接,超时时间内连接上表示存活。

4)Time To Live(TTL):应用在过期时间内,通过http的方式主动发送心跳给ConsulAgent,超过过期时间没有收到心跳表示死亡。

5)Docker + Interval:定时执行Docket exec命令,通过shell脚本的方式根据返回码确认应用是否存活。

6)gRPC +Interval : 定时通过gRPC协议请求配置的端点,检测是否存活。

对于Spring Cloud Consul 来说,支持两种方式

  • HTTP+ Interval
  • Time To Live(TTL)

无论哪种方式,都是在首次调用Consul执行服务注册的报文中确定的。

3.2 Check方式确认

在自动注册的ConsulRegistration的NewService中,包含了Check信息,在生成ConsulAutoRegistration时确认。

java 复制代码
//ConsulAutoRegistration#setCheck
public static void setCheck(NewService service,
      AutoServiceRegistrationProperties autoServiceRegistrationProperties,
      ConsulDiscoveryProperties properties, ApplicationContext context,
      HeartbeatProperties heartbeatProperties) {
   if (properties.isRegisterHealthCheck() && service.getCheck() == null) {
      Integer checkPort;
      if (shouldRegisterManagement(autoServiceRegistrationProperties, properties,
            context)) {
         checkPort = getManagementPort(properties, context);
      }
      else {
         checkPort = service.getPort();
      }
      Assert.notNull(checkPort, "checkPort may not be null");
      service.setCheck(createCheck(checkPort, heartbeatProperties, properties));
   }
}

主要逻辑在createCheck中。

java 复制代码
//ConsulAutoRegistration#createCheck
public static NewService.Check createCheck(Integer port,
      HeartbeatProperties ttlConfig, ConsulDiscoveryProperties properties) {
   NewService.Check check = new NewService.Check();
   if (StringUtils.hasText(properties.getHealthCheckCriticalTimeout())) {
      check.setDeregisterCriticalServiceAfter(
            properties.getHealthCheckCriticalTimeout());
   }
    //spring.cloud.consul.discovery.heartbeat.enable为true(默认为false),采用TTL方式进行健康检查
   if (ttlConfig.isEnabled()) {
      //默认ttl为30s
      check.setTtl(ttlConfig.getTtl());
      return check;
   }

   Assert.notNull(port, "createCheck port must not be null");
   Assert.isTrue(port > 0, "createCheck port must be greater than 0");
   //否则采用http + interval 的健康检查方式,consulAgent定时请求应用http端点,返回200表示健康
   //如果自定义了healthCechkUrl,使用自定义的健康检查接口 
   if (properties.getHealthCheckUrl() != null) {
      check.setHttp(properties.getHealthCheckUrl());
   }//否则使用springboot-actuator自带的/actuator/health
   else {
      check.setHttp(String.format("%s://%s:%s%s", properties.getScheme(),
            properties.getHostname(), port, properties.getHealthCheckPath()));
   }
   check.setHeader(properties.getHealthCheckHeaders());
    //检查间隔时间,默认10s
   check.setInterval(properties.getHealthCheckInterval());
   //检查超时时间
   check.setTimeout(properties.getHealthCheckTimeout());
   check.setTlsSkipVerify(properties.getHealthCheckTlsSkipVerify());
   return check;
}

3.3 TTL实现

TTL的实现方式是在注册结束后,把服务id封装为ConsulHeartbeatTask放到线程池,持续请求consul发送心跳。

在ConsulServiceRegistry注册是,如果健康检查方式是ttl的话,则提交一个定时任务,持续向consul agent发送心跳。

java 复制代码
//ConsulServiceRegistry#register
if (this.heartbeatProperties.isEnabled() && this.ttlScheduler != null
         && service.getCheck() != null
         && service.getCheck().getTtl() != null) {
      this.ttlScheduler.add(reg.getInstanceId());
   }
}

TtlScheduler把instanceId封装为ConsulHeartbeatTask(实现Runnable接口),通过scheduler以一定的时间间隔发送心跳。

java 复制代码
//TtlScheduler#add
public void add(String instanceId) {
   ScheduledFuture task = this.scheduler.scheduleAtFixedRate(
         new ConsulHeartbeatTask(instanceId), this.configuration
               .computeHeartbeatInterval().toStandardDuration().getMillis());
   ScheduledFuture previousTask = this.serviceHeartbeats.put(instanceId, task);
   if (previousTask != null) {
      previousTask.cancel(true);
   }
}

在ConsulHeartbeatTask的任务里,想ConsulAgent发送心跳。

java 复制代码
//TtlScheduler$ConsulHeartbeatTask#run
@Override
public void run() {
   TtlScheduler.this.client.agentCheckPass(this.checkId);
   if (log.isDebugEnabled()) {
      log.debug("Sending consul heartbeat for: " + this.checkId);
   }
}

4. 参考资料

1)Spring-cloud-consul代码分支:2.2.x

2)SpringCloudConsul源码分析(二)服务注册 juejin.cn/post/687189...

3)SpringCloudConsul源码分析(三)服务发现 juejin.cn/post/687223...

4)SpringCloudConsul源码分析(四)健康检查 juejin.cn/post/687844...

相关推荐
LuckyLay8 分钟前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
Stringzhua14 分钟前
【SpringCloud】Kafka消息中间件
spring·spring cloud·kafka
向阳121821 分钟前
Dubbo负载均衡
java·运维·负载均衡·dubbo
Gu Gu Study31 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
WaaTong1 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048441 小时前
初识Java EE和Spring Boot
java·java-ee
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
小灰灰__1 小时前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot