微服务治理1 - Eureka服务治理架构及客户端装配和启动流程分析

服务治理是微服务架构中的核心模块,主要用来实现各个微服务的自动化注册和发现。随着业务的增长和微服务实例的增长,服务治理可以大大减少手动配置的工作和手动配置错误,并且结合其他中间件实现服务的负载均衡。一个微服务治理框架一般包含三个核心要素:

  • 服务注册中心:提供服务注册和发现的功能
  • 服务提供者:服务提供者向服务注册中心注册自己的信息,如服务名、IP地址、端口号等信息
  • 服务消费者:服务消费者从服务注册中心获取服务列表,从而消费者可以知道去何处调用其所需要的服务

在实际场景中,一般服务注册中心是单独的微服务(在高可用环境下会是集群),而服务提供者可能也是服务消费者,服务消费者也可能是服务提供者。

Spring Cloud Eurek是Spring Cloud社区提供的微服务中间件,使用Netflix Eureka来实现服务的注册和发现。其包含两部分:

  • Eureka服务端(Eureka Server),即服务注册中心,支持集群式部署
  • Eureka客户端(Eureka Client),主要处理服务的注册和发现,周期性的向Eureka服务端发送心跳信息来更新它的服务租约,当服务下线时通知Eureka服务端及时下线服务

1. Eureka服务治理架构

在实际使用中,Eureka的服务治理架构一般如下图所示:

Eureka的高级架构图

从图可以看出在这个架构中,可以看到:

  • 有2个角色,即Eureka Server和Eureka Client
  • 每个区域有一个Eureka集群,并且每个区域至少有一个eureka服务器可以处理区域故障,以防服务器瘫痪
  • Eureka Server间相互同步注册信息
  • Eureka Client分为Applicaton Service和Application Client,即服务提供者何服务消费者
  • Applicaton Service向Eureka Server注册、续约和下线和获得注册表信息
  • Application Client获得注册信息并调用服务

在分布式环境下需要考虑单点故障问题,因此需要在生产环境下为各个服务部署多个服务结点,以提高服务的可用性, 对Eureka服务注册中心同样如此。

接下来介绍一下多个Eureka服务注册中心的的关键配置,基于Spring Boot搭建Eureka服务的步骤请参考Eureka帮助文档。当工程初始化好后,新建两个配置文件applicaiton-peer1.yml和application-peer2.yml,内容分别是:

application-peer1.yml

spring:
  application:
    name: eureka-server
server:
  port: 9000
eureka:
  instance:
    hostname: peer1
  client:
    service-url:
      defaultZone: http://peer2:9001/eureka

aplication-peer2.yml

spring:
  application:
    name: eureka-server
server:
  port: 9001
eureka:
  instance:
    hostname: peer2
  client:
    service-url:
      defaultZone: http://peer1:9000/eureka

并在/etc/host中添加如下配置:

127.0.0.1 peer1
127.0.0.1 peer2

然后执行如下Maven命令启动两个Eureka注册服务中心:

mvn spring-boot:run -Dspring-boot.run.profiles=peer1&
mvn spring-boot:run -Dspring-boot.run.profiles=peer2&

在浏览器访问http://peer1:9000,可以看到如下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x7HL0OX4-1582968212541)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200225131016057.png)]

服务消费者和服务提供者需要将eureka.client.service-url.defaultZone修改为

eureka:
  client:
    service-url:
      defaultZone: http://peer1:9000/eureka, http://peer2:9001/eureka

重启服务提供者和服务消费者,此时就可以在peer1和peer2中都能看到服务消费者和服务提供者的信息。

通过配置多个Eureka注册中心可以看到只要有一个Eureka服务注册中心可用,服务消费者和服务提供者就可以通过Eureka服务注册中心找到其要消费的服务,从而达到了Eureka服务高可用。

接下来将进入Eureka源码的分析,由于Eureka的源码包含服务端和客户端模块,文章将分开分析并先从Eureka客户端代码开始分析。

2. Eureka客户端装配

在应用中一般通过@EnableDiscoveryClient注解开启Eureka客户端功能,从注释可以看到该注解的作用是开启DiscoveryClient实现,但是深入到@EnableDiscoveryClient源码中去看,该注解并没有做什么初始化操作。

尝试在应用中去掉@EnableDiscoveryClient注解,重启应用后发现服务依然注册到了Eureka注册中心,由此说明Eureka客户端的装配并不是由@EnableDiscoveryClient注解触发的,那么Eureka客户端是在哪里装配的呢?

在Spring中有一种类似于Java SPI的加载机制,它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。因此,看下spring-cloud-netflix-eureka-client下的spring.factories文件,发现其中有如下配置:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

可以判断Eureka客户端是在EurekaDiscoveryClientConfigServiceBootstrapConfiguration类中装配的。看下该类的源码:

@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
		matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
@Import({ 
   (1)EurekaDiscoveryClientConfiguration.class, // this emulates
		// @EnableDiscoveryClient, the import
		// selector doesn't run before the
		// bootstrap phase
		(2)EurekaClientAutoConfiguration.class,
		EurekaReactiveDiscoveryClientConfiguration.class,
		ReactiveCommonsClientAutoConfiguration.class })
public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {

}

(1) EurekaDiscoveryClientConfiguration注入了三个bean

  • discoveryClient:该Bean是最Eureka的重点,实现类为EurekaDiscoveryClient
  • eurekaHealthCheckHandler:Eureka健康检查的处理类

(2) EurekaClientAutoConfiguration是最复杂的装配类,其依赖的装配非常多,需要用到时再讲解

EurekaReactiveDiscoveryClientConfigurationReactiveCommonsClientAutoConfiguration用于Spring响应式编程,这里不做介绍。

3. Eureka客户端启动流程

上一节中说了步骤(1)中注册的discoveryClient是整个Eureka的重点,因此接下来将重点分析这个Bean,首先看一下该Bean的注册代码,位于EurekaDiscoveryClientConfiguration类中:

@Bean
@ConditionalOnMissingBean
(3)public EurekaDiscoveryClient discoveryClient(EurekaClient client,
      EurekaClientConfig clientConfig) {
   return new EurekaDiscoveryClient(client, clientConfig);
}

(3)可以看到注册的discoverClientEurekaDiscoveryClient类的实例,其需要另外两个Bean进行初始化,即类型分别为EurekaClientEurekaClientConfig的Bean。这两个Bean是在上一节步骤(2)中介绍的EurekaClientAutoConfiguration类中注册的,代码如下:

@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class,
      search = SearchStrategy.CURRENT)
(4)public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
   EurekaClientConfigBean client = new EurekaClientConfigBean();
   ......
   return client;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnRefreshScope
protected static class RefreshableEurekaClientConfiguration {

   @Bean(destroyMethod = "shutdown")
   @ConditionalOnMissingBean(value = EurekaClient.class,
         search = SearchStrategy.CURRENT)
   @org.springframework.cloud.context.config.annotation.RefreshScope
   @Lazy
   public EurekaClient eurekaClient(ApplicationInfoManager manager,
         EurekaClientConfig config, EurekaInstanceConfig instance,
         @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
      ......
      (5)CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
            config, this.optionalArgs, this.context);
      cloudEurekaClient.registerHealthCheck(healthCheckHandler);
      return cloudEurekaClient;
   }
  ......
}

(4) 注册类型为EurekaClientConfig的Bean,该类是EurekaClientConfig接口的实现类,其对应配置文件中以**eureka.client**开头的配置

(5) 注册类型为CloudEurekaClient的Bean

通过步骤(3)-(5),此便可以梳理出EurekaDiscoveryClient类的关系,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JnwvgE1J-1582968212542)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200227132851820.png)]

图分为三个部分,分别用不同的颜色标记出来:

  • 左边是Spring Cloud定义的接口,定义了用来发现服务的常用方法,Spring Cloud通过该接口可以方便的切换不同的服务治理框架
  • 右边的所有接口和类都是Netflix Eureka开源包的实现,主要定义了针对Eureka的服务发现的抽象方法
  • 中间则是对Netflix Eureka服务的的封装并实现了Spring Cloud的DiscoveryClient接口

沿着CloudEurekaClient类的继承关系看,可以发现在Netflix Eureka开源包中DiscoveryClient是服务发现的主要实现类,从基注释中知道其主要有四个功能:

  • 向Eureka Server(即服务注册中心)注册服务实例(服务提供者)
  • 向Eureka Server续约服务
  • 服务关闭时,向Eureka Server取消租约
  • 查询Eureka Server中注册的服务列表

接着进入DiscoveryClient的构造函数(只摘取比较重要的操作):

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
        ......
        fetchRegistryGeneration = new AtomicLong(0);

        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
        //<!--------------------(6)--------------------------
        if (config.shouldFetchRegistry()) {
            this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }

        if (config.shouldRegisterWithEureka()) {
            this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }
        // -------------------------------------------------!>
      
        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());

        (7)if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            ......

            return;  // no need to setup up an network tasks and we are done
        }

        try {
            // default size of 2 - 1 each for heartbeat and cacheRefresh
            (8)scheduler = Executors.newScheduledThreadPool(2,
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-%d")
                            .setDaemon(true)
                            .build());

            (9)heartbeatExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

            (10)cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

          //<!--------------------(11)--------------------------
            eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);
          //----------------------(11)--------------------------!>

          

        (12)if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            fetchRegistryFromBackup();
        }

        // call and execute the pre registration handler before all background tasks (inc registration) is started
        (13)if (this.preRegistrationHandler != null) {
            this.preRegistrationHandler.beforeRegistration();
        }

        (14)if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                if (!register() ) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable th) {
                logger.error("Registration error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }

        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
        (15)initScheduledTasks();

        try {
        (16)    Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }
}

(6) 初始化监视器

(7) 当前实例不需要注册到Eureka Server和从Eureka Server摘取服务列表时,构造方法到此结束

(8) 初始化调度器,该调度器用于执行心跳线程和缓存刷新线程

(9) 初始化心跳线程池(用于向服务注册中心续约)

(10) 初始化缓存刷新线程池(用于从服务注册中心摘取服务列表)

(11) 初始化EurekaTransport,该类封装的属性用于Eureka Client和Eureka Server通信

(12) 从Eureka Server拉取服务列表,fetchRegistry是第一次拉取注册信息,如果拉取不成功的话则执行fetchRegistryFromBackup从备份注册中心获取

(13) 注册之前的扩展点,转为为null

(14) 向Eureka Server发起注册,由于clientConfig.shouldEnforceRegistrationAtInit()默认为false,因此不执行该注册逻辑,而实际的服务注册是在步骤(15)完成的

(15) 初始化定时任务和向Eureka Server注册服务

(16) 向监视器注册该类

以下就是Eureka客户端的装配和启动流程,Eureka服务注册的流程将在下一篇文章中分析。

Github博客地址
知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值