Eureka源码分析之 Client的启动流程

Spring Cloud Eureka源码分析 前面的文章中,已经简单介绍了Spring Cloud Eureka的用法,下面再说明下详细的功能和源码。 Spring Cloud Eureka分为Server端和Client端,Server端作为应用的注册中心,Client端会向Server端注册自己的服务。 Eureka Server和Eureka Client都是采用Java编写的,所以,Eureka主要适用于通过Java实现的分布式系... 阅读详情

Eureka源码分析之 Client的启动流程

​ Eureka Client 是一个Java 客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询负载算法的负载均衡器。

​ 在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒),如果Eureka Server在多个心跳周期没有收到某个节点的心跳,Eureka Server 将会从服务注册表中把这个服务节点移除(默认90秒)。Eureka Client具有缓存的机制,即使所有的Eureka Server 都挂掉的话,客户端依然可以利用缓存中的信息消费其它服务的API。下面一起来看Client客户端相关操作的流程图。

在这里插入图片描述

图一 Client启动工作流程概览

1.从启动类入手

​ 从启动类的@EnableDiscoveryClient注解入手看调用流程。进入 EnableDiscoveryClient 之后,通过注释知道它的作用是为了激活 DiscoveryClient:

在这里插入图片描述

图二 DiscoveryClient注解图

​ 首先是在类头使用了 import 注解引入了:EnableDiscoveryClientImportSelector。该类的主要作用是实例化:AutoServiceRegistrationConfiguration。

@Order(Ordered.LOWEST_PRECEDENCE - 100)

public class EnableDiscoveryClientImportSelector

 extends SpringFactoryImportSelector<EnableDiscoveryClient> {

 

 @Override

 public String[] selectImports(AnnotationMetadata metadata) {

     **//调用父类的方法,拿到通过父类方法要注入的全路径类名数组**

 String[] imports = super.selectImports(metadata);

 **//获得该注解(@EnableDiscoveryClient)的所有属性参数**

 AnnotationAttributes attributes = AnnotationAttributes.fromMap(

  metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));

  **//获得属性autoRegister的值,该值默认是true的**

 boolean autoRegister = attributes.getBoolean("autoRegister");

 **//根据注解配置来判断是否要实例化下面的那个自动配置类**

 if (autoRegister) {

  List<String> importsList = new ArrayList<>(Arrays.asList(imports));

  importsList.add("org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration");

  imports = importsList.toArray(new String[0]);

 } else {

  Environment env = getEnvironment();

  if(ConfigurableEnvironment.class.isInstance(env)) {

  ConfigurableEnvironment configEnv = (ConfigurableEnvironment)env;

  LinkedHashMap<String, Object> map = new LinkedHashMap<>();

  map.put("spring.cloud.service-registry.auto-registration.enabled", false);

  MapPropertySource propertySource = new MapPropertySource(

   "springCloudDiscoveryClient", map);

  configEnv.getPropertySources().addLast(propertySource);

  }

 }
 
 return imports;

 }


 @Override

 protected boolean isEnabled() {

 return getEnvironment().getProperty(

  "spring.cloud.discovery.enabled", Boolean.class, Boolean.TRUE);

 }


 @Override

 protected boolean hasDefaultFactory() {

 return true;

 }


}

这里最终的目的是想实例化:AutoServiceRegistrationConfiguration,我们来看他做了什么:

@Configuration

@Import(AutoServiceRegistrationConfiguration.class)

@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)

public class AutoServiceRegistrationAutoConfiguration {

 
 @Autowired(required = false)

 private AutoServiceRegistration autoServiceRegistration;

 @Autowired

 private AutoServiceRegistrationProperties properties;

 @PostConstruct

 protected void init() {

 if (autoServiceRegistration == null && this.properties.isFailFast()) {

  throw new IllegalStateException("Auto Service Registration has been requested, but there is no AutoServiceRegistration bean");

 }

 }

}

从这里看主要目的是为了实例化:AutoServiceRegistration,AutoServiceRegistrationProperties这两个类。那么初始化这两个bean的作用是什么呢,查看调用的地方:

@Configuration

@EnableConfigurationProperties

@ConditionalOnClass(EurekaClientConfig.class)

@Import(DiscoveryClientOptionalArgsConfiguration.class)

@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)

@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)

@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,

 CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })

@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",

 "org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",

 "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})

public class EurekaClientAutoConfiguration {

  ......

  ......

  ......

  @Bean

 @ConditionalOnBean(AutoServiceRegistrationProperties.class)

 @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)

 public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient, CloudEurekaInstanceConfig instanceConfig, ApplicationInfoManager applicationInfoManager, ObjectProvider<HealthCheckHandler> healthCheckHandler) {

 return EurekaRegistration.builder(instanceConfig)

  .with(applicationInfoManager)

  .with(eurekaClient)

  .with(healthCheckHandler)

  .build();

 }

 

 @Bean

 @ConditionalOnBean(AutoServiceRegistrationProperties.class)

 @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)

 public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {

 return new EurekaAutoServiceRegistration(context, registry, registration);

 }  

  ......

  ......

  ......

}

原因是在这里实例化bean的时候被作为前置条件。

EurekaClientAutoConfiguration 算是到目前为止比较重要的一个类,主要做的事情包括:

  1. 注册 EurekaClientConfigBean ,初始化client端配置信息;

  2. 注册 EurekaInstanceConfigBean ,初始化客户端实例信息;

  3. 初始化 EurekaRegistration,EurekaServiceRegistry,EurekaAutoServiceRegistration实现Eureka服务自动注册;

  4. 初始化 EurekaClient ,ApplicationInfoManager。EurekaClient 的默认实现是 DiscoveryClient,是我们接下来要分析的重点;

  5. 初始化 EurekaHealthIndicator,为/health 端点提供Eureka相关信息,主要有Status当前实例状态和applications服务列表。

继续看 EurekaClientAutoConfiguration 又在哪里被使用:

@ConditionalOnClass(ConfigServicePropertySourceLocator.class)

@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false)

@Configuration

@Import({ EurekaDiscoveryClientConfiguration.class, **// this emulates @EnableDiscoveryClient, the import selector doesn't run before the bootstrap phase**

 EurekaClientAutoConfiguration.class })

public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {

}

在 EurekaDiscoveryClientConfigServiceBootstrapConfiguration 类中被作为注入的对象。

而 EurekaDiscoveryClientConfigServiceBootstrapConfiguration 被引用的分地方就比较特殊,被配置在配置文件中。

org.springframework.cloud.bootstrap.BootstrapConfiguration=\

org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

这个配置的Key部分对应着一个注解类 BootstrapConfiguration:

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface BootstrapConfiguration {

 **/****

 \* Exclude specific auto-configuration classes such that they will never be applied.

 */

 Class<?>[] exclude() default {};

}

他被使用的地方是:BootstrapApplicationListener 的 164行,在这里拿到类的全路径之后,186行进行加载类。

public class BootstrapApplicationListener

 implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

  

  ......

  @Override

 public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {

 ConfigurableEnvironment environment = event.getEnvironment();

 ......

 ConfigurableApplicationContext context = null;

 String configName = environment

  .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");

 for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()

  .getInitializers()) {

  if (initializer instanceof ParentContextApplicationContextInitializer) {

  context = findBootstrapContext(

   (ParentContextApplicationContextInitializer) initializer,

   configName);

  }

 }

 if (context == null) {

      **//在这里被调用**

  context = bootstrapServiceContext(environment, event.getSpringApplication(),

   configName);

 }

 apply(context, event.getSpringApplication(), environment);

 }

  

  private ConfigurableApplicationContext bootstrapServiceContext(

    ConfigurableEnvironment environment, final SpringApplication application,

    String configName) {

    StandardEnvironment bootstrapEnvironment = new StandardEnvironment();

    MutablePropertySources bootstrapProperties = bootstrapEnvironment

      .getPropertySources();

    for (PropertySource<?> source : bootstrapProperties) {

      bootstrapProperties.remove(source.getName());

    }

    String configLocation = environment

      .resolvePlaceholders("${spring.cloud.bootstrap.location:}");

    Map<String, Object> bootstrapMap = new HashMap<>();

    bootstrapMap.put("spring.config.name", configName);

    **// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap will fail**

    **// force the environment to use none, because if though it is set below in the builder**

   **// the environment overrides it**

    bootstrapMap.put("spring.main.web-application-type", "none");

    if (StringUtils.hasText(configLocation)) {

      bootstrapMap.put("spring.config.location", configLocation);

    }

    bootstrapProperties.addFirst(

      new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));

    for (PropertySource<?> source : environment.getPropertySources()) {

      if (source instanceof StubPropertySource) {

        continue;

      }

      bootstrapProperties.addLast(source);

    }

    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

    **// 在这里扫描BootstrapConfiguration注解**

    List<String> names = new ArrayList<>(SpringFactoriesLoader

                       .loadFactoryNames(BootstrapConfiguration.class, classLoader));

    for (String name : StringUtils.commaDelimitedListToStringArray(

      environment.getProperty("spring.cloud.bootstrap.sources", ""))) {

     names.add(name);

    }

    **//** **TODO:** **is it possible or sensible to share a ResourceLoader?**

    SpringApplicationBuilder builder = new SpringApplicationBuilder()

      .profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)

      .environment(bootstrapEnvironment)

      **// Don't use the default properties in this builder**

      .registerShutdownHook(false).logStartupInfo(false)

      .web(WebApplicationType.NONE);

    if (environment.getPropertySources().contains("refreshArgs")) {

      **// If we are doing a context refresh, really we only want to refresh the**

      **// Environment, and there are some toxic listeners (like the**

      **// LoggingApplicationListener) that affect global static state, so we need a**

      **// way to switch those off.**

      builder.application()

        .setListeners(filterListeners(builder.application().getListeners()));

    }

    List<Class<?>> sources = new ArrayList<>();

    for (String name : names) {

      Class<?> cls = ClassUtils.resolveClassName(name, null);

      try {

        cls.getDeclaredAnnotations();

      }

      catch (Exception e) {

        continue;

      }

      sources.add(cls);

    }

    AnnotationAwareOrderComparator.sort(sources);

    builder.sources(sources.toArray(new Class[sources.size()]));

    final ConfigurableApplicationContext context = builder.run();

    **// gh-214 using spring.application.name=bootstrap to set the context id via**

    **// `ContextIdApplicationContextInitializer` prevents apps from getting the actual**

    **// spring.application.name**

    **// during the bootstrap phase.**

    context.setId("bootstrap");

    **// Make the bootstrap context a parent of the app context**

    addAncestorInitializer(application, context);

    **// It only has properties in it now that we don't want in the parent so remove**

    **// it (and it will be added back later)**

    bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);

    mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);

    return context;

  }

  

  

  ......

  ......

   

}

BootstrapApplicationListener 实现了ApplicationEnvironmentPreparedEvent,作为监听器在项目启动的时候被加载。Spring根据应用启动的过程,提供了四种事件供我们使用:

· ApplicationStartedEvent :Spring Boot启动开始时执行的事件;

· ApplicationEnvironmentPreparedEvent:Spring Boot 对应Enviroment已经准备完毕,但此时上下文context还没有创建;

· ApplicationPreparedEvent:Spring Boot 上下文context创建完成,但此时spring中的bean是没有完全加载完成的;

· ApplicationFailedEvent:Spring Boot 启动异常时执行事件。

即这里的BootstrapApplicationListener 是在项目启动加载环境变量完成,还没有创建bean的时候去加载的。

分析到这里,我们把整个的EnableDiscoveryClient注解的初始化链路都走了一遍。大致时序图如下图所示:

在这里插入图片描述

​ 1. 初始化配置文件;

​ 2. 激活 DiscoveryClient。

下面就开始分析DiscoveryClient的作用。

2. DiscoveryClient

启动客户端的时候查看启动日志你会看到服务注册也是从 DiscoveryClient 类中发出的:
在这里插入图片描述

足以见得这个类在服务注册过程中应该做了一些重要的事情。下面一起来分析一下具体实现。

2.1 服务注册

DiscoveryClient 是一个接口,继续观看它的实现类,可以看到每个实现类中都有一个:DESCRIPTION字段,这个字段明确描述了当前类的作用。

  1. EurekaDiscoveryClient:client 的主要实现逻辑类;

  2. CompositeDiscoveryClient:会装载别的服务注册客户端,顺序查找;

  3. NoopDiscoveryClient:已经被废弃;

  4. SimpleDiscoveryClient:具体的服务实例从 SimpleDiscoveryProperties 配置中获取。

从描述上看 EurekaDiscoveryClient 是 client 的主要实现类。而在 EurekaDiscoveryClient 中,获取client实例主要是从 EurekaClient 中查找的:

@Override

public List<ServiceInstance> getInstances(String serviceId) {

  List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,false);

  List<ServiceInstance> instances = new ArrayList<>();

  for (InstanceInfo info : infos) {

​    instances.add(new EurekaServiceInstance(info));

  }

  return instances;

}

DiscoveryClient 是 EurekaClient 的唯一实现类,他有一个很重要的构造方法:

@Inject

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,Provider<BackupRegistry> backupRegistryProvider) {

  if (args != null) {

    this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;

    this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;

    this.eventListeners.addAll(args.getEventListeners());

    this.preRegistrationHandler = args.preRegistrationHandler;

  } else {

    this.healthCheckCallbackProvider = null;

    this.healthCheckHandlerProvider = null;

    this.preRegistrationHandler = null;

  }

 

  this.applicationInfoManager = applicationInfoManager;

  InstanceInfo myInfo = applicationInfoManager.getInfo();

 

  clientConfig = config;

  staticClientConfig = clientConfig;

  transportConfig = config.getTransportConfig();

  instanceInfo = myInfo;

  if (myInfo != null) {

    appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();

  } else {

    logger.warn("Setting instanceInfo to a passed in null value");

  }

 

  this.backupRegistryProvider = backupRegistryProvider;

 

  this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);

  localRegionApps.set(new Applications());

 

  fetchRegistryGeneration = new AtomicLong(0);

 

  remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());

  remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));

 **//上面主要是初始化一些参数**

  **//如果 shouldFetchRegistry= true,注册监控**

  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;

  }

 **//如果shouldRegisterWithEureka=true,注册监控**

  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());

 **//如果shouldRegisterWithEureka = false && shouldFetchRegistry=false**

  **//就不做初始化的工作,直接返回**

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

    logger.info("Client configured to neither register nor query for data.");

    scheduler = null;

    heartbeatExecutor = null;

    cacheRefreshExecutor = null;

    eurekaTransport = null;

    instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());

 

    **// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()**

    **// to work with DI'd DiscoveryClient**

    DiscoveryManager.getInstance().setDiscoveryClient(this);

    DiscoveryManager.getInstance().setEurekaClientConfig(config);

 

    initTimestampMs = System.currentTimeMillis();

    logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",

          initTimestampMs, this.getApplications().size());

 

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

  }

 **//从这里开始创建各种任务的线程池**

  try {

    **// default size of 2 - 1 each for heartbeat and cacheRefresh**

    **//创建定时线程池,线程数量为2个,分别用来维持心跳连接和刷新其他eureka client实例缓存**

    scheduler = Executors.newScheduledThreadPool(2,

                           new ThreadFactoryBuilder()

                           .setNameFormat("DiscoveryClient-%d")

                           .setDaemon(true)

                          .build());

 **//创建一个线程池,线程池大小默认为2个,用来维持心跳连接**

   heartbeatExecutor = new ThreadPoolExecutor(

      1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,

      new SynchronousQueue<Runnable>(),

     new ThreadFactoryBuilder()

      .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")

     .setDaemon(true)

      .build()

    ); **// use direct handoff**

 **//创建一个线程池,线程池大小默认为2个,用来刷新其他eureka client实例缓存**

    cacheRefreshExecutor = new ThreadPoolExecutor(

      1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,

      new SynchronousQueue<Runnable>(),

      new ThreadFactoryBuilder()

      .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")

      .setDaemon(true)

      .build()

   ); **// use direct handoff**

 

    eurekaTransport = new EurekaTransport();

    scheduleServerEndpointTask(eurekaTransport, args);

 ......

    ......

    ......

  } catch (Throwable e) {

   throw new RuntimeException("Failed to initialize DiscoveryClient!", e);

  }

   **//抓取远程实例注册信息,fetchRegistry()方法里的参数,这里为false,意思是要不要强制抓取所有实例注册信息**

  **//这里获取注册信息,分两种方式,一种是全量获取,另一种是增量获取,默认是增量获取**

  if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {

    **//如果配置的是要获取实例注册信息,但是从远程获取失败,从备份获取实例注册信息**

    fetchRegistryFromBackup();

  }

 

  **// call and execute the pre registration handler before all background tasks (inc registration) is started**

  if (this.preRegistrationHandler != null) {

    this.preRegistrationHandler.beforeRegistration();

  }

    **//如果client配置注册到eureka server 且 强制 初始化就注册到eureka 那么就注册到eureka server,默认是不初始化就注册到eureka**

  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**

  **//初始化维持心跳连接、更新注册信息缓存的定时任务**

  initScheduledTasks();

 

  try {

    Monitors.registerObject(this);

  } catch (Throwable e) {

    logger.warn("Cannot register timers", e);

  }

 

  **// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()**

  **// to work with DI'd DiscoveryClient**

  DiscoveryManager.getInstance().setDiscoveryClient(this);

  DiscoveryManager.getInstance().setEurekaClientConfig(config);

 

  initTimestampMs = System.currentTimeMillis();

  logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",

        initTimestampMs, this.getApplications().size());

}

初始化的过程主要做了两件事:

  1. 创建了 scheduler 定时任务的线程池,heartbeatExecutor 心跳检查线程池(服务续约),cacheRefreshExecutor 服务获取线程池 ;

  2. 调用 initScheduledTasks()方法开启线程池,往上面3个线程池分别添加相应任务。然后创建了一个instanceInfoReplicator(Runnable任务),然后调用InstanceInfoReplicator.start方法,把这个任务放进上面scheduler定时任务线程池(服务注册并更新)。

接着看 initScheduledTasks做了哪些事情 :

private void initScheduledTasks() {

  **//获取服务列表信息**

  if (clientConfig.shouldFetchRegistry()) {

    **// registry cache refresh timer**

    **//获取默认的注册频率信息,默认30S**

    int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();

    **//如果缓存刷新超时,下一次执行的delay最大是registryFetchIntervalSeconds的几倍(默认10),默认每次执行是上一次的2倍**

    int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();

    **//执行CacheRefreshThread,服务列表缓存刷新任务**

    scheduler.schedule(

      new TimedSupervisorTask(

        "cacheRefresh",

        scheduler,

       cacheRefreshExecutor,

        registryFetchIntervalSeconds,

       TimeUnit.SECONDS,

       expBackOffBound,

       new CacheRefreshThread()

     ),

     registryFetchIntervalSeconds, TimeUnit.SECONDS);

  }

 **//注册到eureka server**

  if (clientConfig.shouldRegisterWithEureka()) {

   **//续租时间间隔,默认30s**

    int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();

    **// 如果心跳任务超时,下一次执行的delay最大是renewalIntervalInSecs的几倍(默认10),默认每次执行是上一次的2倍**

    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();

    logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

 

    **// Heartbeat timer**

    **//执行HeartbeatThread,发送心跳数据**

    scheduler.schedule(

      new TimedSupervisorTask(

        "heartbeat",

        scheduler,

        heartbeatExecutor,

        renewalIntervalInSecs,

        TimeUnit.SECONDS,

        expBackOffBound,

        new HeartbeatThread()

      ),

      renewalIntervalInSecs, TimeUnit.SECONDS);

 

    **// 客户端实例信息复制**

    instanceInfoReplicator = new InstanceInfoReplicator(

      this,

      instanceInfo,

      clientConfig.getInstanceInfoReplicationIntervalSeconds(),

      2); **// burstSize**

 **//注册监听器**

    statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {

      @Override

      public String getId() {

        return "statusChangeListener";

      }

 

      @Override

      public void notify(StatusChangeEvent statusChangeEvent) {

        if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||

         InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {

         **// log at warn level if DOWN was involved**

          logger.warn("Saw local status change event {}", statusChangeEvent);

        } else {

          logger.info("Saw local status change event {}", statusChangeEvent);

        }

        instanceInfoReplicator.onDemandUpdate();

      }

    };

 

    if (clientConfig.shouldOnDemandUpdateStatusChange()) {

      applicationInfoManager.registerStatusChangeListener(statusChangeListener);

    }

 **//进行服务刷新**

    instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

  } else {

    logger.info("Not registering with Eureka server per configuration");

  }

}

总的来说initScheduledTasks()做了以下几件事:

· 如果shouldFetchRegistry=true,即要从Eureka Server获取服务列表:

启动刷新服务列表定时线程(DiscoveryClient-CacheRefreshExecutor-%d),默认registryFetchIntervalSeconds=30s执行一次,任务为CacheRefreshThread,即从Eureka Server获取服务列表,也刷新客户端缓存。

· 如果shouldRegisterWithEureka=true,即要注册到Eureka Server。

启动heartbeat心跳定时线程(DiscoveryClient-HeartbeatExecutor-%d),默认renewalIntervalInSecs=30s续约一次,任务为HeartbeatThread,即客户端向Eureka Server发送心跳;

启动InstanceInfo复制器定时线程(DiscoveryClient-InstanceInfoReplicator-%d),开启定时线程检查当前Instance的DataCenterInfo、LeaseInfo、InstanceStatus,如果发现变更就执行discoveryClient.register(),将实例信息同步到Server端。

上面有一个需要关注的点是:InstanceInfoReplicator。它会去定时刷新客户端实例的最新信息:当前实例最新数据,租约信息,实例状态。InstanceInfoReplicator 是一个线程类,关注 run()方法:

public void run() {

  try {

    **/****

     \* 刷新 InstanceInfo

     \* 1、刷新 DataCenterInfo

     \* 2、刷新 LeaseInfo 租约信息

     \* 3、根据HealthCheckHandler获取InstanceStatus,并更新,如果状态发生变化会触发所有StatusChangeListener

     */

    discoveryClient.refreshInstanceInfo();

 **//刷新完之后,当前服务有变更,还未同步给server,发起注册**

    Long dirtyTimestamp = instanceInfo.isDirtyWithTime();

    if (dirtyTimestamp != null) {

      **//发起注册**

      discoveryClient.register();

      instanceInfo.unsetIsDirty(dirtyTimestamp);

    }

  } catch (Throwable t) {

    logger.warn("There was a problem with the instance info replicator", t);

  } finally {

    Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);

    scheduledPeriodicRef.set(next);

  }

}

看一下register()的实现:

 **/****

   \* Register with the eureka service by making the appropriate REST call.

   \* 使用http的方式注册eureka服务

   */

boolean register() throws Throwable {

  logger.info(PREFIX + "{}: registering service...", appPathIdentifier);

  EurekaHttpResponse<Void> httpResponse;

  try {

    httpResponse = eurekaTransport.registrationClient.register(instanceInfo);

  } catch (Exception e) {

    logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);

    throw e;

  }

  if (logger.isInfoEnabled()) {

    logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());

  }

  return httpResponse.getStatusCode() == 204;

}

往下跟踪到 RestTemplateEurekaHttpClient类:

public class RestTemplateEurekaHttpClient implements EurekaHttpClient {

 

 protected final Log logger = LogFactory.getLog(getClass());

 

 private RestTemplate restTemplate;

 private String serviceUrl;

 

 public RestTemplateEurekaHttpClient(RestTemplate restTemplate, String serviceUrl) {

 this.restTemplate = restTemplate;

 this.serviceUrl = serviceUrl;

 if (!serviceUrl.endsWith("/")) {

  this.serviceUrl = this.serviceUrl+"/";

 }

 }

  

  @Override

 public EurekaHttpResponse<Void> register(InstanceInfo info) {

 String urlPath = serviceUrl + "apps/" + info.getAppName();

 

 HttpHeaders headers = new HttpHeaders();

 headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip");

 headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

 

 ResponseEntity<Void> response = restTemplate.exchange(urlPath, HttpMethod.POST,

  new HttpEntity<>(info, headers), Void.class);

 

 return anEurekaHttpResponse(response.getStatusCodeValue())

  .headers(headersOf(response)).build();

 }

  

  ......

  ......

  ......

   

}

封装了RestTemplate http client 模板方法,给 server 端发送一个post 请求。所以启动 client 的时候,向服务端发送注册请求的地方就在这里。

2.2 服务续约

服务续约的入口在DiscoveryClient 类initScheduledTasks()方法的heartBeat timer定时器任务中:

// Heartbeat timer**

**//开启定时任务每隔30s发送一次 心跳请求**

scheduler.schedule(

  new TimedSupervisorTask(

    "heartbeat",

    scheduler,

    heartbeatExecutor,

    renewalIntervalInSecs,

    TimeUnit.SECONDS,

    expBackOffBound,

    new HeartbeatThread()

  ),

  renewalIntervalInSecs, TimeUnit.SECONDS);

 

 **/****

   \* The heartbeat task that renews the lease in the given intervals.

   */

private class HeartbeatThread implements Runnable {

 

  public void run() {

    if (renew()) {

      lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();

    }

  }

}

 

 **/****

   \* Renew with the eureka service by making the appropriate REST call

   */

boolean renew() {

  EurekaHttpResponse<InstanceInfo> httpResponse;

  try {

    httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);

    logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());

    if (httpResponse.getStatusCode() == 404) {

      REREGISTER_COUNTER.increment();

      logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());

      long timestamp = instanceInfo.setIsDirtyWithTime();

      boolean success = register();

      if (success) {

        instanceInfo.unsetIsDirty(timestamp);

      }

      return success;

    }

    return httpResponse.getStatusCode() == 200;

  } catch (Throwable e) {

    logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);

    return false;

  }

}

 

@Override

public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id,

                           InstanceInfo info, InstanceStatus overriddenStatus) {

  String urlPath = serviceUrl + "apps/" + appName + '/' + id + "?status="

    \+ info.getStatus().toString() + "&lastDirtyTimestamp="

   \+ info.getLastDirtyTimestamp().toString() + (overriddenStatus != null

                           ? "&overriddenstatus=" + overriddenStatus.name() : "");

 

  ResponseEntity<InstanceInfo> response = restTemplate.exchange(urlPath,

                                HttpMethod.PUT, null, InstanceInfo.class);

 

  EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(

    response.getStatusCodeValue(), InstanceInfo.class)

    .headers(headersOf(response));

 

  if (response.hasBody())

    eurekaResponseBuilder.entity(response.getBody());

 

  return eurekaResponseBuilder.build();

}

上面贴出来了客户端发送心跳请求的完整调用过程,每隔30s客户端向服务端发送一次请求,向服务端重新注册自己。

2.3 服务下线

服务下线比较好理解,在服务关闭的时候取消本机的各种定时任务,给服务端发送请求告知自己下线。

	 **/****

   \* Shuts down Eureka Client. Also sends a deregistration request to the

   \* eureka server.

   */

@PreDestroy

@Override

public synchronized void shutdown() {

  if (isShutdown.compareAndSet(false, true)) {

    logger.info("Shutting down DiscoveryClient ...");

 

    if (statusChangeListener != null && applicationInfoManager != null) {

      applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());

    }

 **//取消各种定时任务**

    cancelScheduledTasks();

 

    **// If APPINFO was registered**

    if (applicationInfoManager != null

      && clientConfig.shouldRegisterWithEureka()

      && clientConfig.shouldUnregisterOnShutdown()) {

      applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);

      **//向服务端发送请求告知自己下线**

      unregister();

    }

 

    if (eurekaTransport != null) {

      eurekaTransport.shutdown();

    }

 **//关闭监控**

    heartbeatStalenessMonitor.shutdown();

    registryStalenessMonitor.shutdown();

 

    logger.info("Completed shut down of DiscoveryClient");

  }

}

 

 

 **/****

   \* unregister w/ the eureka service.

   */

void unregister() {

  **// It can be null if shouldRegisterWithEureka == false**

  if(eurekaTransport != null && eurekaTransport.registrationClient != null) {

    try {

      logger.info("Unregistering ...");

      EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());

      logger.info(PREFIX + "{} - deregister status: {}", appPathIdentifier, httpResponse.getStatusCode());

    } catch (Exception e) {

      logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);

    }

  }

}

 

 

 

@Override

public EurekaHttpResponse<Void> cancel(String appName, String id) {

  String urlPath = serviceUrl + "apps/" + appName + '/' + id;

 

  ResponseEntity<Void> response = restTemplate.exchange(urlPath, HttpMethod.DELETE,

                             null, Void.class);

 

  return anEurekaHttpResponse(response.getStatusCodeValue())

    .headers(headersOf(response)).build();

}

2.4 服务获取 和 服务刷新

服务启动的时候会去服务端全量拉取所有已经注册过的其余client实例信息,增量的时候就是在initScheduledTasks() 方法中每30s增量跑一次。

private void initScheduledTasks() {

  if (clientConfig.shouldFetchRegistry()) {

    **// registry cache refresh timer**

    int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();

    int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();

    scheduler.schedule(

      new TimedSupervisorTask(

        "cacheRefresh",

        scheduler,

        cacheRefreshExecutor,

        registryFetchIntervalSeconds,

        TimeUnit.SECONDS,

        expBackOffBound,

        new CacheRefreshThread()

      ),

      registryFetchIntervalSeconds, TimeUnit.SECONDS);

  }

  

  ......

  ......

  ......

}

 

 **/****

   \* The task that fetches the registry information at specified intervals.

   *

   */

class CacheRefreshThread implements Runnable {

  public void run() {

    refreshRegistry();

  }

}

 

@VisibleForTesting

void refreshRegistry() {

  try {

    boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

 

    boolean remoteRegionsModified = false;

    **// This makes sure that a dynamic change to remote regions to fetch is honored.**

    String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();

    ......

      ......

      ......

 

      boolean success = fetchRegistry(remoteRegionsModified);

    if (success) {

      registrySize = localRegionApps.get().size();

      lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();

    }

 

    ......

      ......

      ......

  } catch (Throwable e) {

    logger.error("Cannot fetch registry from server", e);

  }    

}

 

 

 **/****

   \* Fetches the registry information.

   *

   \* <p>

   \* This method tries to get only deltas after the first fetch unless there

   \* is an issue in reconciling eureka server and client registry information.

   \* </p>

   *

   \* @param forceFullRegistryFetch Forces a full registry fetch.

   *

   \* @return true if the registry was fetched

   */

private boolean fetchRegistry(boolean forceFullRegistryFetch) {

  Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

 

  try {

    **// 如果现在增量服务获取不可用,或者是第一次获取服务的时候,拉去所有的应用**

    Applications applications = getApplications();

 

    if (clientConfig.shouldDisableDelta()

      || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))

      || forceFullRegistryFetch

      || (applications == null)

      || (applications.getRegisteredApplications().size() == 0)

      || (applications.getVersion() == -1)) **//Client application does not have latest library supporting delta**

    {

      logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());

      logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());

      logger.info("Force full registry fetch : {}", forceFullRegistryFetch);

     logger.info("Application is null : {}", (applications == null));

      logger.info("Registered Applications size is zero : {}",

            (applications.getRegisteredApplications().size() == 0));

      logger.info("Application version is -1: {}", (applications.getVersion() == -1));

      getAndStoreFullRegistry();

    } else {

      getAndUpdateDelta(applications);

    }

    applications.setAppsHashCode(applications.getReconcileHashCode());

    logTotalInstances();

  } catch (Throwable e) {

    logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);

    return false;

  } finally {

    if (tracer != null) {

      tracer.stop();

    }

  }

 

  **// Notify about cache refresh before updating the instance remote status**

  onCacheRefreshed();

 

  **// Update remote status based on refreshed data held in the cache**

  updateInstanceRemoteStatus();

 

  **// registry was fetched successfully, so return true**

  return true;

}

​ 客户端拉取服务端保存的所有客户端节点信息保存时间为3分钟,Eureka client取得的数据虽然是增量更新,仍然可能和30秒前取的数据一样,所以Eureka client要自己来处理重复信息。

另外,注意到在fetchRegistry()方法中:

 applications.setAppsHashCode(applications.getReconcileHashCode());

​ 每次增量更新,服务端都会带过来一个一致性hash码。Eureka client的增量更新,其实获取的是Eureka server最近三分钟内的变更,如果Eureka client有超过三分钟没有做增量更新的话(例如网络问题),这就造成了Eureka server和Eureka client之间的数据不一致。正常情况下,Eureka client多次增量更新后,最终的服务列表数据应该Eureka server保持一致,但如果期间发生异常,可能导致和Eureka server的数据不一致,为了暴露这个问题,Eureka server每次返回的增量更新数据中,会带有一致性哈希码,Eureka client用本地服务列表数据算出的一致性哈希码应该和Eureka server返回的一致,若不一致就证明增量更新出了问题导致Eureka client和Eureka server上的服务列表信息不一致了,此时需要全量更新。

关于客户端的代码分析就到这里,可从以下两个角度去分析:

1. 从启动类入手,查看初始化了什么;

2. 从启动日志入手,查看启动类做了什么。

最后,放上在微服务中Eureka的Server与Client启动流程汇总图,图示过程如下图所示:

在这里插入图片描述
在这里插入图片描述

实战监听 Eureka client 的缓存更新 复制代码 复制代码 复制代码 复制代码 复制代码 复制代码 阅读详情

相关推荐

Eureka Client源码分析

作者:陈刚,叩丁狼高级讲师。本文为原创文章,转载请注明出处。     对于一个优秀的程序员而言,一个技术不仅要会用,还要知道他的实现原理和思想,即不仅要知其然还要知其所以然,这样我们写代码才会特别自信,出现bug才能很快定位到问题所在。接下来我们就来简单探讨一下SpringCloud的实现原理,即:源码分析 一.服务注册 服务注册与发现是SpringCloud最基础的部分,我们就从这部分开始...

wolfcode_cn的博客 1470

SpringCloud中Eureka的配置及详细使用

SpringCloud中Eureka的配置及详细使用,讲解了消费者和提供者,Ribbon是解决了什么问题

Demolist的博客 6747

小鹏P7车载以太网诊断实战:从OBD口定义到DoIP协议验证

本文详细解析了小鹏P7车载以太网诊断的实战过程。从OBD接口的针脚定义入手,依据ISO13400标准,确认了以太网线对位置,并指导制作专用的RJ45转OBD线束。通过搭建测试环境,使用Ping、端口扫描及Wireshark抓包,最终成功验证了车辆对DoIP协议的支持,为基于以太网的高速诊断与刷写提供了完整的技术路径。

CAT789的专栏 521

SpringCloud中的Eureka使用

1.首先需要引入依赖 <!--eureka的客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--eureka-服务端--> <depend

qq_56360015的博客 426

OpenGL纹理贴图

OpenGL纹理1. 基本概念2. 纹理环绕方式3. 纹理过滤3.1 GL_NEAREST3.2 GL_LINEAR3.3 多级渐远纹理 (Mipmap)4. 纹理加载与创建4.1 纹理加载4.2 纹理创建4.3 纹理的使用 1. 基本概念 纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来表示物体的细节特征,希望绘制一个有足够多细节的物体,当然可以用足够多的顶点颜色来描述,但是无疑也增加了许多额外的开销;另一种办法就是使用纹理。 学习之前先展示一张实现的纹理图以更好理解纹理! 还是这张图为了将纹

牵LV小猪精的博客 8922

minist手写数字的识别代码复现(pytorch 版本)

电脑系统:Windows 10语言环境:Python 3.8.18编译器:jupyter notebook深度学习环境:torch == 1.13.1+cu116显卡及显存: Tesla V100-PCIE-32GB卷积神经网络(Convolutional Neural Network,CNN或ConvNet)是一种 具有局部连接、权重共享等特性的深层前馈神经网络. 目前的卷积神经网络一般是由卷积层、汇聚层和全连接层交叉堆叠而成的 前馈神经网络。

2202_75521293的博客 2616

SpringCloud源码解析之eureka-client客户端

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Mar

酸菜鱼_sky的博客 1126

对角两对双差速轮AGV的平移、直行、转弯和原地旋转之PID控制

本示例将展示如何使用PID控制算法,控制具有对角线布置的两对双差速轮(左前和右后)的AGV,实现平移、直行、转弯和原地旋转等运动。通过对车辆的运动学和PID控制器的设计,我们可以精确地控制AGV的运动

xiaoc100200的博客 1510

EureKa注册中心 的搭建及流程图 和Ribbon(负载均衡)的引入整合day01

原理图: 两大功能 ①负责服务的注册 ②负责服务的发现 EureKa 服务 两个客户端的搭建 一、搭建EureKa 服务的三步: 1.搭建EureKa的服务端 2.配置 3.添加引导类 1)搭建EureKa服务端用于启动 2)添加pom依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" x

weixin_48641534的博客 810

点云从入门到精通技术详解100篇-基于点云语义分割的电力线提取及聚类研究

在现代社会,电力一直扮演着最重要的角色之一。它不仅与人民的生活息息相关,更是一个国家发展的命脉。近年来,随着我国各特高压输电线路建成(比如2022年1月份白鹤滩江苏特高压输电线路的放线)以及电网规模的不断扩大,我国年发电量稳步上升,人民的生活水平越来越高。我国土地辽阔,输电线路分布广泛,再加上地形环境复杂多样,电力线可能遭受风吹雨晒甚至是冰雪寒灾的影响。因此,为了保障电力线的可靠供电,防止停电意外发生,电力部门要定期对输电线路进行检查。目前我国主要。

getusushu的博客 1010

AI OS与LLM as OS:人工智能操作系统的崛起

近年来,随着大语言模型(Large Language Model, LLM)技术的突飞猛进,人工智能领域掀起了新的革命浪潮。以ChatGPT、GPT-4等为代表的LLM模型展现出了惊人的自然语言理解和生成能力,使得人机交互前所未有地拟人化、情境化。在此背景下,业界开始探索将LLM作为操作系统(Operating System, OS)的可能性,由此催生出"AI OS"和"LLM as OS"的概念。本文将深入解读这两个前沿概念,分析其内涵、技术原理、应用前景及面临的挑战。

AI天才研究院 993

spring cloud eureka client 启动分析

1.自动装配原则下client加载 配置类简单描述: 1.EurekaClientConfigServerAutoConfiguration :如果配置服务器和eureka 实例在同一个服务上,这里需要处理一下服务实例的元数据信息 2.EurekaDiscoveryClientConfigServiceAutoConfiguration:eureka 与 配置中心的联合使用处理 3.EurekaClientAutoConfiguration:eureka client 服务启动的核心 4.RibbonEu

欢谷悠扬 975

目标检测YOLO实战应用案例100讲-跨模态、多级别融合的RGB-D显著目标检测方法研究(续)

在第二章介绍了本研究的跨模态特征融合方法,第二章对于多级别特征的融合, 使用的是与基线模型baseline相同的方法:相邻级别的特征中,高级特征进行上采 样到与低级特征相同的尺度,再将高级特征通过降维得到与低级特征相同的通道数 量,最后对二者进行特征叠加,如此自顶向下的进行多级别的特征融合。本章主要 介绍本研究在特征层面的另一发现,并基于此发现引出的本文的第二个创新点:多 级别特征的融合方法。另外,在网络最深层,本文设计了一个新的模块来获取大的 感受野,对于大尺度目标能够增加更多语义信息,提升检测效果。 骨

qq_36130719的博客 466

Eureka client启动流程图和源码分析

Eureka server启动流程图和源码分析 流程图 源码解析 由流程图看出,Eureka client启动时大致会做三件事,服务注册,心跳续约,服务更新,本质上都是通过模拟http请求,调用服务端提供的对应接口来实现 服务注册 通过服务端提供的“服务注册”接口,将当前client信息注册到注册中心。client初始化时,会将服务注册线程延迟40秒执行,并且每次注册结束后,会再次将当前线程延迟30秒(可配置)执行。这样当eureca服务内存丢失后(如重启服务),在一定时间内还能得到client信息。 配

qq_34210372的博客 555

代谢组数据分析(四):多种差异分析方法筛选代谢物标记物

本文详细介绍了代谢组学中常用的差异分析方法,包括倍数变化法、T检验、VIP值在OPLS-DA中的应用。通过这些方法,研究者能够筛选出在不同组别间具有显著差异的代谢物,为疾病诊断和治疗提供线索。文章还提供了一个整合的R函数run_metabolomeDA,便于研究人员进行一站式差异分析,并展示了如何生成火山图来可视化结果。

专注生信领域 6770

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Eureka Client启动源码分析(2)

Eureka Client启动源码分析(2) 上篇文章主要分析了Eureka Client启动的启动流程 最终都是调用EurekaHttpClient接口的方法。 可以理解为Eureka Client为Server层,调用EurekaHttpClient(dao层的方法),进行发送HTTP请求。 下面来分析EurekaHttpClient的实现细节 EurekaHttpClient public ...

plumblum的博客 551

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Eureka源码分析之Eureka Client获取实例信息流程

下方是Eureka Client从Eureka Server获取实例信息的总体流程图,后面会详细介绍每个步骤。 Eureka Client在刚启动的时候会从Eureka Server全量获取一次注册信息,同时初始化Eureka Client本地实例信息缓存定时更新任务,默认30s一次 registryFetchIntervalSeconds = 30。 @Inject Discovery...

king 3169

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Eureka 源码分析之 Eureka Client

文章首发于微信公众号《程序员果果》 地址:mp.weixin.qq.com/s/47TUd96NM… 简介 Eureka是一种基于REST(Representational State Transfer)的服务,主要用于AWS云,用于定位服务,以实现中间层服务器的负载平衡和故障转移。我们将此服务称为Eureka Server。Eureka还附带了一个基于Java的客户端组件Eureka Cli...

weixin_33700350的博客 392

spring cloud eureka client 源码分析

spring cloud eureka client源码分析和常用配置简绍 eureka 的起源,项目的快速迭代和产品和快速发展导致微服务的兴起原来的单体架构满足不了项目的快速迭代,许多公司慢慢转向微服务架构的升级,服务动辄成百上千我们急需一种服务治理框架 eureka 诞生了 我们本次源码基于 spring-cloud Hoxton.SR4 版本 pom入下 <dependencyManagement> <dependencies> <d

m0_38097114的博客 184

Eureka server启动流程图和源码分析

Eureka server启动流程图和源码分析 流程图 源码解析 由流程图看出,Eureka server启动时会做两件事 1.服务同步 因为eureka服务可以搭建集群,所以每个服务节点启动时,会从相邻的eureka节点中同步实例数据,源码如下: @Override public int syncUp() { // Copy entire entry from neighboring DS node int count = 0; //当我们配置r

qq_34210372的博客 3918

领导让我研究 Eureka 源码 | 启动过程

来一份 Eureka 启动的整体流程图。

程序员高级码农的博客 1200

Eureka源码分析

Eureka架构图 Eureka核心功能点 服务注册(register):Eureka Client会通过发送REST请求的方式向Eureka Server注册自己的服务,提供自身的元数据,比如ip地址、端口、运行状况指标的url、主页地址等信息。Eureka Server接收到注册请求后,就会把这些元数据信息存储在一个双层的Map中。 服务续约(renew):在服务注册后,Eureka Client会维护一个心跳来持续通知Eureka Server,说明服务一直处于可用状态,防止被剔除。Eureka

哪有什么天生王者,不过是一步一个脚印 2008
上一篇: Linux内存修改
下一篇: 使用 Typora+Gitee 设置图床功能
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值