Spring Cloud源码解析一:Eureka源码解析(F版)

介绍

Eureka架构如下图
在这里插入图片描述
在Eureka的服务治理中,涉及到如下几个概念

服务提供者

  • register(服务注册):eureka client 向eureka server注册,提供自身的元数据,如ip地址,端口
  • renew(服务续约):eureka client 每隔30s向eureka server发送一次心跳来续约,如果eureka server在90s没有收到eureka client的续约,它会将实例从注册表中删除
  • cacel(服务下线):eureka client 向eureka server发送下线请求,eureka server会将实例从注册表中删除

相关设置参数如下

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 90 # 如果超过了90秒,就认为服务挂了

服务消费者

  • get registry(获取服务):发送rest请求到注册中心,来获取上面的注册表。为了性能考虑,eureka server会维护一份只读的服务清单来返回给客户端,该缓存清单每隔30s更新一次。
  • 服务调用:服务消费者在获取到服务清单后,根据清单中的信息进行远程调用。在eureka中有region和zone的概念,一个region包含多个zone,一个客户端被注册到一个zone中。在进行服务调用时,优先访问在一个zone中的服务提供者

服务注册中心

  • replicate(服务同步):eureka server之间注册表信息的复制,使eureka server之间注册表保持一致

  • eviction(服务剔除):eureka server会启动一个定时任务,每隔60s将注册表中超时的eureka client(默认90s)没有续约的服务从注册表中删除,即服务剔除

  • 自我保护:eureka在运行期间,会统计心跳失败比例在15分钟之内是否低于85,如果出现低于的情况(生产环境网络不稳定),eureka会将当前注册实例保护起来,不让过期,尽可能保护这些注册信息,因为此时有可能是注册中心出现网络问题

相关设置参数如下


eureka:
  server:
    eviction-interval-timer-in-ms: 6000 #设置清洗的间隔时间,时间使用的是毫秒单位(默认是60s)
    enable-self-preservation: false # 设置false表示关闭保护模式,(保护模式:有一段时间不可用还保留)

很多参数都可以设置,各种参数及默认值可以看类EurekaServerConfigBean

再次梳理一下流程

服务提供者

  1. 启动后,向注册中心发起register请求,注册服务
  2. 在运行过程中,定时向注册中心发送renew 心跳,证明我还或者
  3. 停止服务后,向注册中心发起cancel请求,清空当前服务注册信息

服务消费者

  1. 启动后,从注册信息拉取服务注册信息
  2. 在运行过程中,定时更新服务注册信息
  3. 服务消费者发起远程调用
    a. 服务消费者(北京)会从服务注册信息中选择同机房的服务提供者(北京),发起远程调用。只有同机房的服务提供者挂了才会选择其他机房的的服务提供者(青岛)
    b. 服务消费者(天津)因为同机房内没有服务提供者,则会按负载均衡算法选择北京或青岛的服务提供者,发起远程调用

注册中心

  1. 启动后,从其他节点拉取服务注册信息
  2. 运行过程中,定时运行evict任务,剔除没有按时renew的服务(包括非正常停止和网络故障的服务)
  3. 运行过程中,接收到的register,renew,cancel请求,都会同步至其他注册中心节点

服务端流程

前面的教程提到加入依赖,并且在启动类上加上@EnableEurekaServer注解就能实现一个注册中心,为啥一个注解就是能实现一个注册中心?

1.直接看@EnableEurekaServer这个注解,可以看到引入了EurekaServerMarkerConfiguration配置类

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

}

这个配置类也很普通,往spring容器中注入一个Marker bean,但这是个空类啊,感觉没啥用啊?看上面的解释,为了激活EurekaServerAutoConfiguration配置类,看这个类名一看就是spring boot中的自动装配类。

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

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

	class Marker {
	}
}

了解spring boot自动装配原理的同学,都知道spring boot启动的时候会读META-INF目录下的spring.factories,然后加载里面的配置类
在这里插入图片描述
spring.factories里面果然自动装配了EurekaServerAutoConfiguration类

spring.factories

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

EurekaServerAutoConfiguration类定义如下

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter

EurekaServerAutoConfiguration类上有很多注解,可以看到有这样一句

@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)

看到这个Marker类,有没有特别熟悉,就是上面我们分析到的类,只有spring容器中有Marker这个bean,这个配置类才会其作用。

1.加了@EnableEurekaServer这个注解,spring容器才会注入Marker这个bean
2.只有spring注入了Marker这个bean,EurekaServerAutoConfiguration自动配置类才会起作用,才会有后续一系列的操作

EurekaServerAutoConfiguration类的注入的一个bean如下

@Bean
public FilterRegistrationBean jerseyFilterRegistration(
		javax.ws.rs.core.Application eurekaJerseyApp) {
	FilterRegistrationBean bean = new FilterRegistrationBean();
	bean.setFilter(new ServletContainer(eurekaJerseyApp));
	bean.setOrder(Ordered.LOWEST_PRECEDENCE);
	bean.setUrlPatterns(
			Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));

	return bean;
}

这是Jersey框架的类,Jersey是一个restful框架,你可以类比spring mvc。为什么要注入一个restful框架的类呢?因为前面说到的注册,续约,集群同步是通过http来实现的,所以应该有一个类似spring mvc的controller类,ApplicationResource就是这样一个类。

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
							@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
	registry.register(info, "true".equals(isReplication));
	return Response.status(204).build();  // 204 to be backwards compatible
}

注册的http请求会发送到这个方法,可以看到实例信息被封装到InstanceInfo这个类中(我省略了很多代码,只分析重点)

接着调用InstanceRegistry类的register方法

@Override
public void register(final InstanceInfo info, final boolean isReplication) {
	handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
	super.register(info, isReplication);
}

代码先执行handleRegistration方法,然后调用PeerAwareInstanceRegistryImpl的register方法

handleRegistration发布了一个ApplicationEvent事件

private void handleRegistration(InstanceInfo info, int leaseDuration,
		boolean isReplication) {
	log("register " + info.getAppName() + ", vip " + info.getVIPAddress()
			+ ", leaseDuration " + leaseDuration + ", isReplication "
			+ isReplication);
	publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,
			isReplication));
}

如果你对eureka的注册数据感兴趣,如进行持久化,就可以监听这个事件,register,cancel,renew都是这个套路,都会发布一个事件

PeerAwareInstanceRegistryImpl的register方法又会调用AbstractInstanceRegistry的register方法。register,renew,cancel的大部分逻辑都在这个类中

public void register(final InstanceInfo info, final boolean isReplication) {
	int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
	if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
		leaseDuration = info.getLeaseInfo().getDurationInSecs();
	}
	super.register(info, leaseDuration, isReplication);
	replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

最后这个replicateToPeers方法是发送注册信息给eureka集群的其他节点,入口就是就是前面提到的ApplicationResource类的addInstance方法。可以看到这个方法还有一个isReplication参数。如果是集群发送的注册信息,isReplication=true,不会在向
其他节点同步注册信息(避免循环注册,造成死循环)。客户端发送的注册信息,isReplication=true,会向其他节点同步注册信息。

注册表保存在一个ConcurrentHashMap中,是AbstractInstanceRegistry的一个成员变量

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
	= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

InstanceInfo这个类前面提到了,主要保存了实例的信息。而Lease则是一个续约对象。保存了实例的注册时间,失效时间。

这个双层map也很好理解。最外层是服务名,里面是一个具体的实例名
在这里插入图片描述
AbstractInstanceRegistry的register和cancel就是往这个map中放节点信息和删除节点信息,细节逻辑不再介绍。

renew方法也比较简单,就是更新Lease对象的lastUpdateTimestamp属性,

public void renew() {
    lastUpdateTimestamp = System.currentTimeMillis() + duration;
}

如果客户端传了duration的值,就用客户端的,没有传默认90s,就是前面的这个配置

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90 # 如果超过了90秒,就认为服务挂了

eviction(服务剔除)操作,就会用到Lease对象的lastUpdateTimestamp属性

在AbstractInstanceRegistry中定义了一个定时器,定时器会定制执行EvictionTask这个类的方法,最终调用evict方法

具体判断失效的逻辑如下

public boolean isExpired(long additionalLeaseMs) {
    return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}

duration这个前面提到了,默认90s。而additionalLeaseMs这个为集群同步的时间

到此位置register,renew,cancel,evict方法的整体逻辑都梳理清楚了,各种细节后续完善

客户端流程

先说2个注解@EnableDiscoveryClient和@EnableEurekaClient

spring cloud中的discovery service有多种实现,比如:eureka, consul, zookeeper

  1. @EnableDiscoveryClient注解在spring-cloud-commons包中,作用于多种注册中心
  2. @EnableEurekaClient注解在spring-cloud-netflix包中,只作用于eureka注册中心

如何你用的注册中心是eureka,则它们的作用是一样的。

在以前的版本中需要在启动类上加@EnableDiscoveryClient或者@EnableEurekaClient注解,在F版中,只需要加入相应的依赖就行了,这2个注解不用再加。

按照spring boot自动配置的思想来找,eureka客户端自动加载了哪些类?
在这里插入图片描述
自动加载了EurekaClientAutoConfiguration类

@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 {

既然加入了依赖就变成eureka client,那么如何关闭。不让它成为一个eureka client呢?

看上面的条件注解@AutoConfigureAfter,这个配置类是在EurekaDiscoveryClientConfiguration类加载后才加载的,EurekaDiscoveryClientConfiguration的定义如下

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
public class EurekaDiscoveryClientConfiguration

eureka.client.enabled这个属性默认为true,如果设置为false,则EurekaDiscoveryClientConfiguration不会加载,导致EurekaClientAutoConfiguration不会加载,这个就不会变成eureka client

EurekaClientAutoConfiguration最主要的部分就是注入了一个如下的bean

@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) {
	manager.getInfo(); // force initialization
	return new CloudEurekaClient(manager, config, this.optionalArgs,
			this.context);
}

可以看到@Bean(destroyMethod = “shutdown”),说明这个bean被销毁的时候会调用com.netflix.discovery.DiscoveryClient这个方法,这个方法后续分析。

顺着构造函数一直点上去,会发现来到com.netflix.discovery.DiscoveryClient这个类的构造函数

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

ApplicationInfoManager封装了一些实例信息
EurekaClientConfig封装了客户端的一些配置
Provider<BackupRegistry>则是备用注册表的回调函数,一般用不上

构造函数里里面初始化了

1.刷新服务列表的定时任务
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

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

分析在构造函数中2个比较重要的函数,获取注册表信息,有两种获取方式

1.全量获取
2.增量获取

默认情况下第一次全量获取,后续增量获取

先从缓存中取,缓存为空,发送http请求,再把结果放到缓存中

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
	Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

	try {
		// If the delta is disabled or if it is the first time, get all
		// applications
		// 先从缓存中取数据
		Applications applications = getApplications();
		// 可以看到是否全量获取数据的条件很多
		// 1.禁用增量获取
		// 2.从缓存中是否取到数据
		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;
}

注册信息,发送http请求即可

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;
}

续约,1.返回200续约成功,2.返回404重新注册

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;
	}
}

最后来分析我们前面提到的的shutdown方法

@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);
			// 通过http发送cancel请求,即服务下线
			unregister();
		}

		if (eurekaTransport != null) {
			eurekaTransport.shutdown();
		}

		heartbeatStalenessMonitor.shutdown();
		registryStalenessMonitor.shutdown();

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

shutdown的方法其实也很简单
1.停止各种定时任务
2.通过http发送cancel请求,即服务下线等

至此大概流程分析完了,各种细节后续补充。

推荐关注

在这里插入图片描述

参考博客

eureka深入解读
[0]https://www.infoq.cn/article/jlDJQ*3wtN2PcqTDyokh
[1]https://www.cnblogs.com/rickiyang/p/11802413.html
[2]https://www.cnblogs.com/rickiyang/p/11802434.html
[3]https://www.fangzhipeng.com/springcloud/2017/08/11/eureka-resources.html
@EnableDiscoveryClient和@EnableEurekaClient注解的区别
[4]https://blog.csdn.net/Ezreal_King/article/details/72594535
视频教程
[5]https://www.bilibili.com/video/av58866715/

发布了385 篇原创文章 · 获赞 1471 · 访问量 90万+

猜你喜欢

转载自blog.csdn.net/zzti_erlie/article/details/104088914