SpringCloud之Eureka(一)

8 篇文章 1 订阅

Eureka是什么

Spring Cloud Eureka是Spring Cloud Netflix微服务套件的一部分,它是基于Netflix Eureka的二次封装,主要负责完成微服务中各个的服务注册与发现。假设我们有两个服务A和B,其中A服务需要调用到B服务来完成某个业务操作,如果没有注册中心,我们可能会将B服务的地址和接口API以配置文件的形式注入到应用中,但随着业务的增长,单个的服务B可能没法抗住压力,需要进行扩容和高可用,这时就需要频繁的去调整服务A代码中关于B服务的配置,这是很笨的。
我们假设这样一种场景:我们有多个服务B的实例,有一个特殊的服务C可以帮我们列出B服务的所有实例,并给这些服务起一个服务名,我们在A服务中通过这个服务名调用服务B的某个实例。这样若是服务B的实例发生变化,服务A并不需要编码或配置上的修改,Eureka在微服务架构中也恰恰起到了这个作用。

Spring Cloud、Spring Boot的版本问题

根据前面我们举得服务A、服务B以及管理服务B的特殊服务C,我们可以发现Eureka是分为两部分的,第一部分是承担维护服列表的服务C,我们称之为Eureka服务端,第二部分是服务A、服务B之类了的普通服务,我们称之为Eureka客户端。Eureka客户端会被注册到Eureka服务端,被其他服务进行调用,Eureka在微服务架构中起到了微服务中服务治理的作用。
这里讲一下Spring Cloud版本与Spring Boot的对应关系,如果版本之间搭配的有问题,就可能引出各种奇怪的问题,开始之前还是应该把版本统一好。我们可以通过访问这个地址https://start.spring.io/actuator/info来获取Spring Cloud与Spring Boot的对应关系,确定一下自己选的Spring Cloud与Spring Boot是互相适配没有问题的。在这里插入图片描述
我们这里用的Spring Cloud版本是Finchley.M2,Spring Boot版本是2.0.0.M3。至于Spring Cloud与其他几大服务组件的关系,在Spring Cloud的依赖中已经指定声明了,不需要我们再去操心。我们看一个Eureaka服务端的POM文件:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.0.M3</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		</dependency>
	</dependencies>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<spring-cloud.version>Finchley.M2</spring-cloud.version>
	</properties>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

能看出我们用的Spring Boot版本是2.0.0.M3,Spring Cloud版本是Finchley.M2,引入了一个eureka-server依赖,却没有指定版本。我们点到spring-cloud-dependencies这个依赖的pom中去,发现定义了spring-cloud-netflix的版本,而Eureka就是netflix套件中的一部分。因此我们不必再操心Spring Cloud与各个微服务组件搭配时的版本问题。
在这里插入图片描述

Eureka架构图及描述

这一段摘自别人的博客
在这里插入图片描述
1.服务注册(register):Eureka Client会通过发送REST请求的方式向Eureka Server注册自己的服务,提供自身的元数据,比如ip地址、端口、运行状况指标的url、主页地址等信息。Eureka Server接收到注册请求后,就会把这些元数据信息存储在一个双层的Map中。
2.服务续约(renew):在服务注册后,Eureka Client会维护一个心跳来持续通知Eureka Server,说明服务一直处于可用状态,防止被剔除。Eureka Client在默认的情况下会每隔30秒(eureka.instance.leaseRenewallIntervalInSeconds)发送一次心跳来进行服务续约。
3.服务同步(replicate):Eureka Server之间会互相进行注册,构建Eureka Server集群,不同Eureka Server之间会进行服务同步,用来保证服务信息的一致性。
4.获取服务(get registry):服务消费者(Eureka Client)在启动的时候,会发送一个REST请求给Eureka Server,获取上面注册的服务清单,并且缓存在Eureka Client本地,默认缓存30秒(eureka.client.registryFetchIntervalSeconds)。同时,为了性能虑,Eureka Server也会维护一份只读的服务清单缓存,该缓存每隔30秒更新一次。
5.服务调用:服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行远程调用。Eureka有Region和Zone的概念,一个Region可以包含多个Zone,在进行服务调用时,优先访问处于同一个Zone中的服务提供者。
6.服务下线(cancel):当Eureka Client需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前先发送REST请求给Eureka Server,告诉Eureka Server自己要下线了,Eureka Server在收到请求后,就会把该服务状态置为下线(DOWN),并把该下线事件传播出去。
7.服务剔除(evict):有时候,服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给Eureka Server来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server在启动的时候会创建一个定时任务,每隔一段时间(默认60秒),从当前服务清单中把超时没有续约(默认90秒,eureka.instance.leaseExpirationDurationInSeconds)的服务剔除。
8.自我保护:既然Eureka Server会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了异常,所有的服务都没能够进行续约,Eureka Server就把所有的服务都剔除了,这样显然不太合理。所以,就有了自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下,Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enable-self-preservation: false)

Eureka的服务端

Eureka的服务端很好实现,最少配置就两步:

  1. 引入Eureka的依赖
	<dependency>
   		<groupId>org.springframework.cloud</groupId>
   		<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
   	</dependency>
  1. 对Spring Boot的启动类增加@EnableEurekaServer注解

@EnableEurekaServer的深入探究


/**
 * Annotation to activate Eureka Server related configuration {@link EurekaServerAutoConfiguration}
 */

@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {

}

类注释的意思是:这个注解是用来激活Eeureka Server相关配置EurekaServerAutoConfiguration的。同时我们发现这个注解还用到了@EnableDiscoveryClient,这说明Eeureka服务端同时也是一个Eeureka客户端,这里的@Import起到的作用是导入配置bean(@Import的详细作用我会再写一篇),我们看一下EurekaServerMarkerConfiguration的源码:

/**
 * Responsible for adding in a marker bean to activate
 * {@link EurekaServerAutoConfiguration}
 */
@Configuration
public class EurekaServerMarkerConfiguration {

	@Bean
	public Marker eurekaServerMarkerBean() {
		return new Marker();
	}

	class Marker {
	}
}

就单纯是为了实例化一个Marker的bean,用来激活EurekaServerAutoConfiguration这个类,我们看一下这个类的一些代码:

// 说明这是一个配置类
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
// 这里体现了@EnableEurekaServer注解要引入一个EurekaServerMarkerConfiguration.Marker对象的原因,
// 是用来激活EurekaServerAutoConfiguration这个配置类
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
// @ConfigurationProperties注解可谓是SpringBoot最常用的属性配置相关的注解,但如果你自己尝试可以发现,
// 单一个@ConfigurationPropertie是不够的(如果没有同时被@Compent修饰)是无法将Bean注入进来,必须要用@EnableConfigurationProperties开启
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
// 用于加载指定配置文件
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
}

可以看到上面我们还有一行没有介绍,就是通过@Import导入的EurekaServerInitializerConfiguration这个配置类,从名字可以看出这个与Eureka服务端配置初始化相关。我们看一下这个类的代码:

@Configuration
public class EurekaServerInitializerConfiguration
		implements ServletContextAware, SmartLifecycle, Ordered {
	private ServletContext servletContext;
	@Autowired
	private EurekaServerBootstrap eurekaServerBootstrap;
	@Override
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
	}
	@Override
	public void start() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
					log.info("Started Eureka Server");
					publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
					EurekaServerInitializerConfiguration.this.running = true;
					publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
				}
				catch (Exception ex) {
					log.error("Could not initialize Eureka servlet context", ex);
				}
			}
		}).start();
	}
}

由于篇幅原因,这里只粘贴了一部分源码。EurekaServerInitializerConfiguration实现了ServletContextAware和SmartLifecycle两个接口,第一个接口使用来获取Spring上下文ServletContext的,具体的实现可以看一下这篇http://www.pinhuba.com/spring/101051.htm
第二个接口是用来在Spring加载和初始化所有bean后,接着执行一些任务或者启动需要的异步服务。我们通过源码的start()方法可以看到确实是启动了一个新的线程去调用EurekaServerBootstrap.contextInitialized方法启动初始化Eureka环境并启动Eureka。
整个流程下来是这个样子的(图片摘自):在这里插入图片描述

EurekaServer的启动

前面我们讲到EurekaServerInitializerConfiguration的start方法会新跑一个线程去初始化并启动Eureka,其中初始化是调用EurekaServerBootstrap.contextInitialized方法,我们研究一下这个方法,首先先看一下EurekaServerBootstrap是怎么注入到IOC中的,在我们前面提到过的EurekaServerAutoConfiguration配置类中有一段代码:

	@Bean
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		this.eurekaClient.getApplications(); // force initialization
		return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
				serverCodecs, this.eurekaClient,
				this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
				this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
	}
	@Bean
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}

记住传进去的registry对象是InstanceRegistry类的实例,下面会用到。我们深入看一下EurekaServerBootstrap.contextInitialized方法,都干了哪些事儿:

public void contextInitialized(ServletContext context) {
		try {
			// 初始化Eureka Environment和Eureka Data Center
			// 没有配置eureka.environment和eureka.datacenter就都是默认的
			initEurekaEnvironment();
			// 初始化Eureka服务上下文
			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);
		}
	}

initEurekaEnvironment方法这里我就不列举代码了,它的作用就是向AbstractConfiguration中注入archaius.deployment.environment和archaius.deployment.datacenter这两个属性,默认值是"test"和"default"。AbstractConfiguration是由ConfigurationManager进行管理和初始化的(初始化时先查询是否有配置系统属性"archaius.default.configuration.class"和"archaius.default.configuration.factory",我们可以在启动应用时指定这两个参数,使用自己的配置类,如果没有则走默认初始化逻辑),其中还包含zone、region等信息。
我们仔细分析一下initEurekaServerContext方法:

	protected void initEurekaServerContext() throws Exception {
		// 向后兼容
		// 客户端服务端Json、XML通信的转换
		JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);
		XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);
		// Aws跳过,这里isAws是根据server实例的dataCenter的Name来判断是否是Aws,默认名称是MyOwn
		if (isAws(this.applicationInfoManager.getInfo())) {
			this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
					this.eurekaClientConfig, this.registry, this.applicationInfoManager);
			this.awsBinder.start();
		}

		EurekaServerContextHolder.initialize(this.serverContext);

		log.info("Initialized server context");

		// 从对等的Eureka节点复同步已注册服务的列表
		int registryCount = this.registry.syncUp();
		// 将eureka服务的状态改为up,启动一个后台线程清理没有续约的服务		
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

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

这里看一下registry对象,我们之前说过他的类型是InstanceRegistry,继承于PeerAwareInstanceRegistryImpl,syncUp方法是其从父类继承过来的。

	public int syncUp() {
        // Copy entire entry from neighboring DS node
        int count = 0;
		// serverConfig是EurekaServerConfigBean类型
		// 在EurekaServerConfigBeanConfiguration被注入,如果我们通过eureka.client.register-with-eureka=false
		// 那么EurekaServerConfigBean.registrySyncRetries就不会被初始化为5次,而是默认0次,这个方法将会跳过循环直接返回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;
                }
            }
            // Applications是一个容器,包含所有注册在localRegion的应用
            Applications apps = eurekaClient.getApplications();
            // 循环所有注册的应用
            for (Application app : apps.getRegisteredApplications()) {
            	// 循环该应用的所有实例
                for (InstanceInfo instance : app.getInstances()) {
                    try {
                    	// 检查是否已被注册到该Eureka Server
                        if (isRegisterable(instance)) {
                            register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                            count++;
                        }
                    } catch (Throwable t) {
                        logger.error("During DS init copy", t);
                    }
                }
            }
        }
        return count;
    }

openForTraffic在InstanceRegistry一级对入参做了非0的处理,然后调用父类PeerAwareInstanceRegistryImpl的具体实现。做非0处理的原因注释有给出:如果调用父类PeerAwareInstanceRegistryImpl的openForTraffic方法,传入参数为0,这意味着,如果实例最近没有发送任何续订,则租约不会自动取消。这种情况通常发生在单个Eureka Server的情况。

	public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
        // 每分钟期望续约次数 = 实例数 * 每30s一次心跳
        this.expectedNumberOfRenewsPerMin = count * 2;
        // 每分钟最少续约次数 = 每分钟期望续约次数 * 续约百分比(默认0.85)
        this.numberOfRenewsPerMinThreshold =
                (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
        logger.info("Got " + count + " instances from neighboring DS node");
        logger.info("Renew threshold is: " + numberOfRenewsPerMinThreshold);
        this.startupTime = System.currentTimeMillis();
        if (count > 0) {
            this.peerInstancesTransferEmptyOnStartup = false;
        }
        DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
        // Aws跳过
        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);
        // 启动线程用于剔除60s没有续约的服务
        super.postInit();
    }

Eureka的常用配置

Eureka Server的配置全部在org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean里,是com.netflix.eureka.EurekaServerConfig的实现类,替代了netflix的默认实现DefaultEurekaServerConfig。

@ConfigurationProperties(EurekaServerConfigBean.PREFIX)
public class EurekaServerConfigBean implements EurekaServerConfig {
	public static final String PREFIX = "eureka.server";
}

所以我们要在application.yml中配置Eureka服务端的属性时,要以eureka.server开头。
配置列表以及默认值(来源博客):

enableSelfPreservation=true

是否开启自我保护

renewalPercentThreshold = 0.85

自我保护续约百分比阀值因子。如果实际续约数小于续约数阀值,则开启自我保护

renewalThresholdUpdateIntervalMs = 15 * 60 * 1000

续约数阀值更新频率。

peerEurekaNodesUpdateIntervalMs = 10 * 60 * 1000

Eureka Server节点更新频率。

enableReplicatedRequestCompression = false

是否启用复制请求压缩。

waitTimeInMsWhenSyncEmpty=5 * 60 * 1000

当从其他节点同步实例信息为空时等待的时间。

peerNodeConnectTimeoutMs=200

节点间连接的超时时间。

peerNodeReadTimeoutMs=200

节点间读取信息的超时时间。

peerNodeTotalConnections=1000

节点间连接总数。

peerNodeTotalConnectionsPerHost = 500;

单个节点间连接总数。

peerNodeConnectionIdleTimeoutSeconds = 30;

节点间连接空闲超时时间。

retentionTimeInMSInDeltaQueue = 3 * MINUTES;

增量队列的缓存时间。

deltaRetentionTimerIntervalInMs = 30 * 1000;

清理增量队列中过期的频率。

evictionIntervalTimerInMs = 60 * 1000;

剔除任务频率。

responseCacheAutoExpirationInSeconds = 180;

注册列表缓存超时时间(当注册列表没有变化时)

responseCacheUpdateIntervalMs = 30 * 1000;

注册列表缓存更新频率。

useReadOnlyResponseCache = true;

是否开启注册列表的二级缓存。

disableDelta=false。

是否为client提供增量信息。

maxThreadsForStatusReplication = 1;

状态同步的最大线程数。

maxElementsInStatusReplicationPool = 10000;

状态同步队列的最大容量。

syncWhenTimestampDiffers = true;

当时间差异时是否同步。

registrySyncRetries = 0;

注册信息同步重试次数。

registrySyncRetryWaitMs = 30 * 1000;

注册信息同步重试期间的时间间隔。

maxElementsInPeerReplicationPool = 10000;

节点间同步事件的最大容量。

minThreadsForPeerReplication = 5;

节点间同步的最小线程数。

maxThreadsForPeerReplication = 20;

节点间同步的最大线程数。

maxTimeForReplication = 30000;

节点间同步的最大时间,单位为毫秒。

disableDeltaForRemoteRegions = false;

是否启用远程区域增量。

remoteRegionConnectTimeoutMs = 1000;

远程区域连接超时时间。

remoteRegionReadTimeoutMs = 1000;

远程区域读取超时时间。

remoteRegionTotalConnections = 1000;

远程区域最大连接数

remoteRegionTotalConnectionsPerHost = 500;

远程区域单机连接数

remoteRegionConnectionIdleTimeoutSeconds = 30;

远程区域连接空闲超时时间。

remoteRegionRegistryFetchInterval = 30;

远程区域注册信息拉取频率。

remoteRegionFetchThreadPoolSize = 20;

远程区域注册信息线程数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值