介绍
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
再次梳理一下流程
服务提供者
- 启动后,向注册中心发起register请求,注册服务
- 在运行过程中,定时向注册中心发送renew 心跳,证明我还或者
- 停止服务后,向注册中心发起cancel请求,清空当前服务注册信息
服务消费者
- 启动后,从注册信息拉取服务注册信息
- 在运行过程中,定时更新服务注册信息
- 服务消费者发起远程调用
a. 服务消费者(北京)会从服务注册信息中选择同机房的服务提供者(北京),发起远程调用。只有同机房的服务提供者挂了才会选择其他机房的的服务提供者(青岛)
b. 服务消费者(天津)因为同机房内没有服务提供者,则会按负载均衡算法选择北京或青岛的服务提供者,发起远程调用
注册中心
- 启动后,从其他节点拉取服务注册信息
- 运行过程中,定时运行evict任务,剔除没有按时renew的服务(包括非正常停止和网络故障的服务)
- 运行过程中,接收到的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
- @EnableDiscoveryClient注解在spring-cloud-commons包中,作用于多种注册中心
- @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/