服务治理Eureka——生态、实践、源码

Euraka是Netflix的一个核心子模块,他基于Rest实现服务的发现与注册,采用了CS的设计架构

一、SpringCloud微服务生态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsHJU6fJ-1632559554278)(F:\typroa\aimages\image-20210922204205413.png)]
在前面过程中接触到Dubbo、Nacos等微服务技术他们是SpringCloud Alibaba的技术栈,而上面列举出来的应该算是SpringCloud Netflix的技术栈范围,Spring Cloud可以看成是有关微服务的一套规范,这两个技术栈就是对这套规范的不同实现。
在这里插入图片描述

二、分布式相关理论

1、CAP理论

  • Consistency 一致性

    分布式系统不同的节点都是有数据的(相同的数据),保持不同节点中数据的一致即访问的所有节点同是一份最新的数据

  • Availability 可用性

    可用是指服务上的可用,在集群中一部分的节点出现故障之后服务集群是否还能继续相应客户的请求

  • Partition tolerance 分区容错性

    当服务上的一部分出现故障类似于形成数据孤岛,数据通信不连通,有关分区容错性还不是很能理解,感觉与Dubbo中听得不太一样

当我们牺牲一致性的时候,这个一致性可以通过逐级控制的方式来实现一个最终的一致性,就像在双十一抢购的时候我们可以抢到东西、加入购物车、去支付,在这一步步的操作中他可以逐步的去卡控,只要保证已经被抢购完的商品没有进行支付操作即可,可以让他因为数据的不一致性而出现抢到东西的假象。

2、ACID理论

AICD理论就是数据库中的相关理论,他与BASE理论是相关的,这一块也是需要在Mysql中进行详细补充的

3、BASE理论

他是有Ebay提出的一个模型,是在CAP模型上对于C与A的一个权衡的结果:

  • BA 基本可用

    保证核心功能的可用

  • S 软状态

    允许系统数据存在中间状态,就像订单在支付的过程中可能是存在A->B->C->D的几种不同的状态的但是只有D是一个最终的状态,在软状态的模式下是允许我们的不同服务之间可能对于这张订单的状态分别为A、B、C

  • E 最终一致性

三、Eureka使用实例

在这里插入图片描述
这是因为Eureka有自我保护机制,在默认的情况下,如果Eureka server 在一定的时候内,没有接受到某个微服务的心跳,就会注销该实例,默认时间是90秒。但是当网络发生故障的情况下,微服务和EurekaServer就无法通讯,这样就很危险,因为微服务本身是健康的,此时不应该注销该服务,而EurekaServer通过注销该服务来达到自我保护机制,当网络健康的时候,EurekaServer就会自动退出自我保护机制

1、创建单例服务端

将服务端创建为Spring项目,其中添加依赖
在这里插入图片描述
在application.properties中添加相应的配置文件:

# Eureka服务端应用的端口,默认为8761
server.port=8000
# 设置当前Eureka实例的hostname(http://localhost:8000)
eureka.instance.hostname=localhost
# 暴露给其他Eureka Client的注册地址
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
# 由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己,默认为true
eureka.client.register-with-eureka=false
# 由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为false
eureka.client.fetch-registry=false

启动类中添加相应的注解:

@EnableEurekaServer
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2、创建多例服务端——集群

创建项目时候需要引入的依赖是相同的,不同的是在于配置文件。总的来说是需要集群的节点需要相互注册,这时候我们需要将这个注册形成一个环状,在实际的操作过程中试了一下,这样的配置确实是可以的,但是不知道当一个服务的节点断掉之后他们是如何进行同步或是服务的。我们知道Zookeeper中如果集群中的主节点宕掉了这时候需要重新进行选举主节点,集群是不可用的当时Eureka不是这样的:

在项目中我们通过配置三个不同的节点服务来模拟集群,三个服务的配置文件如下,其中要注意的是集群的各个几点的服务名称应该要是一一致的:

  • 8001
# 同一个集群,应用名称保持一致
spring.application.name=eureka-cluster
server.port=8001
# http://27.0.0.1:8001
eureka.instance.hostname=127.0.0.1
# 注册到cluster2:8002里
eureka.client.service-url.defaultZone=http://127.0.0.1:8002/eureka/
  • 8002
# 同一个集群,应用名称保持一致
spring.application.name=eureka-cluster
server.port=8002
# http://27.0.0.1:8001
eureka.instance.hostname=127.0.0.1
# 注册到27.0.0.1:8003里
eureka.client.service-url.defaultZone=http://127.0.0.1:8003/eureka/
  • 8003
# 同一个集群,应用名称保持一致
spring.application.name=eureka-cluster
server.port=8003
# http://27.0.0.1:8001
eureka.instance.hostname=127.0.0.1
# 注册到8001里
eureka.client.service-url.defaultZone=http://127.0.0.1:8001/eureka/

3、创建Provider客户端

客户端创建项目的方式与服务端的一样都是创建为Spring项目,要引入的依赖在上面也已经说过,这里面主要介绍的是配置文件以及接口如何写,实现一个远程的服务调用

#这都是一些基本的配置
spring.application.name=eureka-client-producer
server.port=7005
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
# eureka.instance.lease-renewal-interval-in-seconds=30
# eureka.client.fetch-registry=true
# eureka.client.registry-fetch-interval-seconds=30
# eureka.instance.lease-expiration-duration-in-seconds=90

创建controller,这里面的controller是给consumer提供服务的,这样的调用形式与Fegin与Nacos结合时候的调用范式有些类似,都是通过接口地址进行调用个人的话还是喜欢Dubbo的调用方式——直接通过接口将服务之间的调用对接上

@RestController
public class HelloController {
    @Resource
    private Registration registration;
    @Resource
    private DiscoveryClient client;
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd  HH:mm:ss");
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index() {
        List<ServiceInstance> instances = client.getInstances(registration.getServiceId());
        for (ServiceInstance instance : instances) {
            System.out.println(String.format("%s INFO /hello host=%s serviceId=%s port=%s",
                    sdf.format(new Date()), instance.getHost(), instance.getServiceId(), instance.getPort()));
        }
        return "Hello World!";
    }
}

启动项目的时候需要在项目的启动类上加注解,当然这个注解也是可以不加的,因为框架默认也会给我们开启,其中的为什么这样在源码部分进行了讲解:

@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientProducerApplication.class, args);
    }
}
4、创建Provider客户端
spring.application.name=eureka-client-consumer
server.port=7003
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

远程服务调用,这个时候他调用的并不是真实的地址而是服务的名称,这一点是需要注意的

@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientConsumerApplication {
    //创建RestTemplate的Spring Bean实例,并通过@LoadBalanced注解开启客户端的负载均衡
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientConsumerApplication.class, args);
    }
}

四、Eureka Client源码解析

1、表识Eureka客户端

在这里插入图片描述

2、@EnableDiscoveryClient开启注册属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cddwWwz-1632559554287)(F:\typroa\aimages\image-20210925114950622.png)]

3、spring.factories中的EurekaClientAutoConfiguratio

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lQ84Tx4M-1632559554288)(F:\typroa\aimages\image-20210925114607991.png)]
在这里插入图片描述
在Spring启动的时候他会对所有spring.factory中的Bean进行加载,这时候在EurekaClientAutoConfiguration的一个注解中实际上还有一个开启默认注册功能的注解。所以说在我们的项目中也是可以不添加@EnableDiscoveryClient注解的。

上面所说的差不多就是我们客户端启动的一个机理。

五、Eureka Server源码解析

1、spring.factory

在服务端的spring.factory文件中自动注册的Bean只有一个:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

所以我们服务端程序的一个入口就是EurekaServerAutoConfiguration,这个方法上有一个@Import的注解,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GOvDGkyq-1632559554290)(F:\typroa\aimages\image-20210925123728634.png)]

2、程序入口的注解开启

在这里插入图片描述
在这个注解中会通过@Import的注解引入EurekaServerMarkerConfiguration,在这个方法中会有一个Mark,这个Mark将是程序Eureka服务端启动的一个重要前置条件:

@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
   @Bean
   public Marker eurekaServerMarkerBean() {
      return new Marker();
   }
   class Marker {
   }
}

3、start()方法

@Override
public void start() {
   new Thread(() -> {
      try {
         // TODO: 进行一个容器初始化
         eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
         log.info("Started Eureka Server");
		//通过上面的这个日志内容可以看出上在这之前Eureka实际上已经启动成功了,所以启动过程中的主要任务就在contextInitialized
          
         //下面的这些方法都是对事件的一个发布
         publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
         EurekaServerInitializerConfiguration.this.running = true;
         publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
      }
      catch (Exception ex) {
         // Help!
         log.error("Could not initialize Eureka servlet context", ex);
      }
   }).start();
}

4、contextInitialized()

public void contextInitialized(ServletContext context) {
   try {
   	  //在这个方法中只是执行了一条日志语句og.info("Setting the eureka configuration..");
      initEurekaEnvironment();
      initEurekaServerContext();
	  //执行的是一个属性赋值操作
      context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
   }
   catch (Throwable e) {
      log.error("Cannot bootstrap eureka server :", e);
      throw new RuntimeException("Cannot bootstrap eureka server :", e);
   }
}

那么在初始化过程中扮演重要角色的就是初始化Eureka服务上下文的initEurekaServerContext方法

5、initEurekaServerContext()

protected void initEurekaServerContext() throws Exception {
   // For backward compatibility 为了保证对前兼容
   JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
   XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
   //兼容亚马逊服务器而设计
   if (isAws(this.applicationInfoManager.getInfo())) {
      this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig, this.eurekaClientConfig, this.registry,
            this.applicationInfoManager);
      this.awsBinder.start();
   }
   //想要了解这句话应该要清楚EurekaServerContextHolder的作用,他就是用serverContext对EurekaServerContextHolder
   //的属性做了一个复制操作,而EurekaServerContextHolder应该又是为程序的其他地方提供了服务
   EurekaServerContextHolder.initialize(this.serverContext);
   log.info("Initialized server context");

   //下面的这两个方法分别是用来做相邻服务节点的同步与服务节点的剔除
   int registryCount = this.registry.syncUp();
   this.registry.openForTraffic(this.applicationInfoManager, registryCount);

   // Register all monitoring statistics.
   EurekaMonitors.registerAllStats();
}

6、syncUp()服务节点直之间的同步

@Override
public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;

    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
        if (i > 0) {
            try {
                Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
            } catch (InterruptedException e) {
                logger.warn("Interrupted during registry transfer..");
                break;
            }
        }
        //获取所有的应用,从这里也可以看出Eureka中实际上是一种双层的Map结构,这里的这里个写法也可以为以后
        //自己写Map的遍历提供一个参考
        Applications apps = eurekaClient.getApplications();
        for (Application app : apps.getRegisteredApplications()) {
            for (InstanceInfo instance : app.getInstances()) {
                try {
                    if (isRegisterable(instance)) {
                        register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        count++;
                    }
                } catch (Throwable t) {
                    logger.error("During DS init copy", t);
                }
            }
        }
    }
    return count;
}

调用服务端获相应的信息肯定要走网络请求但是这个网络请求时如何走的,就确实搞得不很清楚了,后面回头学的时候在研究吧

有关的博客参照:

Eureka源码分析 - 文集 - 简书 (jianshu.com)

节点之间的同步实际上并不是直接从远程拉取的,他会走一个缓存之类的机制以减少服务端的压力,大概的顺序是这样的,读/写的情况是相反的:

内存——》读写缓存——》只读缓存

7、openForTraffic()节点剔除

@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
    // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
    this.expectedNumberOfClientsSendingRenews = count;
    updateRenewsPerMinThreshold();
    logger.info("Got {} instances from neighboring DS node", count);
    logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
    this.startupTime = System.currentTimeMillis();
    if (count > 0) {
        this.peerInstancesTransferEmptyOnStartup = false;
    }
    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
    boolean isAws = Name.Amazon == selfName;
    if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
        logger.info("Priming AWS connections for all replicas..");
        primeAwsReplicas(applicationInfoManager);
    }
    logger.info("Changing status to UP");
    applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
    super.postInit();
}

在postInit中可以看出这里有一个定时任务,用来执行这个剔除的监控,这里有关定时任务的使用方法也是一个非常值得学习的地方。

8、第三步中的start()又是如何被执行的呢

Spring容器启动的时候会加载一个AbstractApplicationContext类,这个类中会执行一个refresh()的方法,这个方法的中文名意思就是刷新,虽然用自己的语言很难描述他到底是个什么东西但是他源码不难看出他就是对相关环境的一个准备,就像初始化Bean

@Override
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
      // Prepare this context for refreshing.
      prepareRefresh();
      // Tell the subclass to refresh the internal bean factory.
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
      // Prepare the bean factory for use in this context.
      prepareBeanFactory(beanFactory);
      try {
         // Allows post-processing of the bean factory in context subclasses.
         postProcessBeanFactory(beanFactory);
         StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
         // Invoke factory processors registered as beans in the context.
         invokeBeanFactoryPostProcessors(beanFactory);
         // Register bean processors that intercept bean creation.
         registerBeanPostProcessors(beanFactory);
         beanPostProcess.end();
         // Initialize message source for this context.
         initMessageSource();
         // Initialize event multicaster for this context.
         initApplicationEventMulticaster();
         // Initialize other special beans in specific context subclasses.
         onRefresh();
         // Check for listener beans and register them.
         registerListeners();
         // Instantiate all remaining (non-lazy-init) singletons.
         finishBeanFactoryInitialization(beanFactory);
         // Last step: publish corresponding event  发布符合条件的事件
         finishRefresh();
      }
      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }
         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);
         // Propagate exception to caller.
         throw ex;
      }
      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
         contextRefresh.end();
      }
   }
}

try代码块的最后一句是finishRefresh();,他的意思就是发布符合条件的事件,我的理解就是他要通知相应的时间做一些该要做的事情,就像要调用与Eureka中有关的start方法一样:

  • finishRefresh()
protected void finishRefresh() {
   // Clear context-level resource caches (such as ASM metadata from scanning).
   clearResourceCaches();
   // Initialize lifecycle processor for this context.
   initLifecycleProcessor();
   // Propagate refresh to lifecycle processor first这个方法是用来传播与生命周期有关的时间
    //通过getLifecycleProcessor()获取生命周期相关的处理器,在调用onRefresh进行一个刷新
   getLifecycleProcessor().onRefresh();
   // Publish the final event.
   publishEvent(new ContextRefreshedEvent(this));
   // Participate in LiveBeansView MBean, if active.
   if (!IN_NATIVE_IMAGE) {
      LiveBeansView.registerApplicationContext(this);
   }
}
  • onRefresh()
public void onRefresh() {
   startBeans(true);
   this.running = true;
}

在执行了startBeans(true)之后,running的状态就变味了true,所以这个onRefresh的主要操作还是在startBeans()

  • startBeans()
private void startBeans(boolean autoStartupOnly) {
   Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
   Map<Integer, LifecycleGroup> phases = new TreeMap<>();

   lifecycleBeans.forEach((beanName, bean) -> {
      if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
         int phase = getPhase(bean);
         phases.computeIfAbsent(
               phase,
               p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
         ).add(beanName, bean);
      }
   });
   if (!phases.isEmpty()) {
      phases.values().forEach(LifecycleGroup::start);
   }
}

这个过程中他会将符合条件的遍历出来并进行执行:
在这里插入图片描述

  • start()
public void start() {
   if (this.members.isEmpty()) {
      return;
   }
   if (logger.isDebugEnabled()) {
      logger.debug("Starting beans in phase " + this.phase);
   }
   Collections.sort(this.members);
   for (LifecycleGroupMember member : this.members) {
      doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);
   }
}
  • dostart()
private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName, boolean autoStartupOnly) {
    //相当于是拿出一个lifecycle的Bean,线面就会有对这个bean的一个调用
   Lifecycle bean = lifecycleBeans.remove(beanName);
   if (bean != null && bean != this) {
      String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
      for (String dependency : dependenciesForBean) {
         doStart(lifecycleBeans, dependency, autoStartupOnly);
      }
      if (!bean.isRunning() &&
            (!autoStartupOnly || !(bean instanceof SmartLifecycle) || ((SmartLifecycle) bean).isAutoStartup())) {
         if (logger.isTraceEnabled()) {
            logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
         }
         try {
            bean.start();
         }
         catch (Throwable ex) {
            throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex);
         }
         if (logger.isDebugEnabled()) {
            logger.debug("Successfully started bean '" + beanName + "'");
         }
      }
   }
}

通过bean.start();的调用我们可以看出这时候就会执行到我们的Eureka的start方法中来
在这里插入图片描述
这也解释了为什么我们Eurake中启动的start方法会被执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值