文章目录
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的服务端很好实现,最少配置就两步:
- 引入Eureka的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 对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;
远程区域注册信息线程数。