一、Eureka Client源码(在eureka-client-1.9.13.jar中,com.netflix.discovery下)
1. client启动时候去Eureka Server注册服务,在启动类上添加@EnableDiscoveryClient增加注解,这个注解是为了开启一个DiscoveryClient实例,这个类实现了EurekaClient接口,EurekaClient接口又继承了LookupService接口
2. DiscoveryClient这个类是帮助和Eureka Server互相协作的,可以进行服务注册,服务续约,服务下线,获取服务列表;列表就是我们在配置文件中配置的eureka.client.service-url.defaultZone这一选项,这个地址就是Eureka Server的地址,服务注册、服务续约以及其他的操作,都是向这个地址发送请求的
3. DiscoveryClient类构造器里面调用了initScheduledTasks()方法,这个方面里面有两层判断
a. 第一层判断是shouldFetchRegistry(),这个取值是配置文件中eureka.client.fetch-registry,默认是true,这层里面会初始化一个服务获取的定时任务,这个默认30s,也就是这个配置:eureka.client.registry-fetch-interval-seconds=30,意思是从eureka获取注册表信息的频率(秒)是30秒;cacheRefreshExecutorExponentialBackOffBound代表缓存刷新重试。这里面这个定时任务会定时获取服务注册列表
b. 第二层判断是shouldRegisterWithEureka(),这个取值是配置文件中的eureka.client.register-with-eureka。这里面有个定时续约的定时任务,续约时间默认30s,这个时间点会不断的发送请求来维持心跳的。而这个InstanceInfoReplicator类实现了Runnable接口,他的run方法里调用了discoveryClient.register()方法进行服务注册,会通过REST请求把客户端的元数据发送给Eureka Server。
上面涉及源码如下:
private void initScheduledTasks() {
//获取服务注册列表信息
if (clientConfig.shouldFetchRegistry()) {
//服务注册列表更新的周期时间
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
//定时更新服务注册列表
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread() //该线程执行更新的具体逻辑
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
if (clientConfig.shouldRegisterWithEureka()) {
//服务续约的周期时间
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
//应用启动可见此日志,内容是:Starting heartbeat executor: renew interval is: 30
logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
// 定时续约
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread() //该线程执行续约的具体逻辑
),
renewalIntervalInSecs, TimeUnit.SECONDS);
//这个Runable中含有服务注册的逻辑
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
//服务注册
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
所以,在initScheduledTasks方法中,做了两个操作,向Eureka Server注册服务,并且在条件满足的情况下,创建服务获取和服务续约两个定时任务
4. 客户端要注册到注册中心,首先需要知道注册中心的地址,在DiscoveryClient类中getEurekaServiceUrlsFromConfig这个方法,这个会找到注册中心的地址,源码如下:
@Deprecated
public static List<String> getEurekaServiceUrlsFromConfig(String instanceZone, boolean preferSameZone) {
return EndpointUtils.getServiceUrlsFromConfig(staticClientConfig, instanceZone, preferSameZone);
}
这个方法有两个参数会调用EndpointUtils.getServiceUrlsFromConfig,这个方法有如下三个参数:
clientConfig:客户端配置类(配置文件)
instanceZone:客户端所在的zone
preferSameZone:如果为True,将选择与客户端所在同一个zone的注册中心
源码分析如下:
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List<String> orderedUrls = new ArrayList<String>();
//第一步,返回客户端所在region,一个微服务应用只属于一个region,默认是default,可以通过eureka.client.region设置
String region = getRegion(clientConfig);
//第二步,该方法是通过客户端的region返回所有可用的该region下的人zone,region和zone是一对多的关系。如果返回的zone的数组为空,说明该region下没有可用的zone,那就给他一个默认的zone
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[1];
availZones[0] = DEFAULT_ZONE;
}
logger.debug("The availability zone for the given region {} are {}", region, availZones);
//第三步,拿到客户端的region、所有该region的zone,接下来就要加载Eurekaserver的具体地址了,该方法的作用是:返回要使用的zone在刚才获取的该region下所有的zone数组的位置,返回的是一个int型
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
//第四步,获取serviceUrl了,返回的是一个String的List
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
//第五步
//1.1将所有的zone的serviceUrl全部获取,只不过是与客户端相同zone的serviceUrl放到List<String>的前面,其他的zone的serviceUrl会加入到该List中
//1.2如果availZone中只有一个zone,而且是客户端相同的zone,上面的这段代码就会跳过了,因为没有其他的zone,也就没有serviceUrl了
//1.3假如我们availZone = [“zone1”,”zone2”,”zoneClient”,”zone4”],当前region的可用zone有4个,恰好与客户端相同的zone在availZone的第三个位置
//1.4如果myZoneOffset不是availZone数组的最后一个值时,currentOffSet = myZoneOffset+1,否则currentOffSet = 0
//1.5我们可以得到下列变量的值myZOneOffset= 2,currentOffSet= 3
int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
while (currentOffset != myZoneOffset) {
//第六步,调用clientConfig.getEurekaServerServiceUrls(availZones[3])方法,可以看出我们获取了第四个zone的所有serviceUrl,并把他们加到orderdUrls中
serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
//第七步,我们已经遍历了availZone数组的最后一个值,但是现在availZone的前两个值的zone的serviceUrl还没有获取到,如果当前的currentOffSet是availZone的最后一个值,那么就讲currenOffSet设置为0,否则currentOffSet就继续加一,到这里就可以从头开始遍历了,一直到currentOffSet =myZoneOffSet,最后,所有的zone的serviceUrl都被获取到了
if (currentOffset == (availZones.length - 1)) {
currentOffset = 0;
} else {
currentOffset++;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
}
return orderedUrls;
}
5. region和zone概念梳理:
a. region:可以简单理解为地理上的分区,比如亚洲地区,或者华北地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region
b. zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone
c. 服务调用会优先调用同一个zone内的服务进行调用
d.注册中心选择逻辑:
1). 如果prefer-same-zone-eureka为false,按照service-url下的 list取第一个注册中心来注册,并和其维持心跳检测。不会再向list内的其它的注册中心注册和维持心跳。只有在第一个注册失败的情况下,才会依次向其它的注册中心注册,总共重试3次,如果3个service-url都没有注册成功,则注册失败。每隔一个心跳时间,会再次尝试。
2). 如果prefer-same-zone-eureka为true,先通过region取availability-zones内的第一个zone,然后通过这个zone取service-url下的list,并向list内的第一个注册中心进行注册和维持心跳,不会再向list内的其它的注册中心注册和维持心跳。只有在第一个注册失败的情况下,才会依次向其它的注册中心注册,总共重试3次,如果3个service-url都没有注册成功,则注册失败。每隔一个心跳时间,会再次尝试
e. 服务调用的配置文件
eureka:
instance:
metadata-map:
zone: zone-1
服务消费者和服务提供者分别属于哪个zone,均是通过eureka.instance.metadata-map.zone来判定的。
服务消费者会先通过ribbon去注册中心拉取一份服务提供者的列表,然后通过eureka.instance.metadata-map.zone指定的zone进行过滤,过滤之后如果同一个zone内的服务提供者有多个实例,则会轮流调用。
只有在同一个zone内的所有服务提供者都不可用时,才会调用其它zone内的服务提供者
f. 拓展
eureka.instance.lease-renewal-interval-in-seconds: 30
服务和注册中心的心跳间隔时间,默认为30s
eureka.instance.lease-expiration-duration-in-seconds: 90
服务和注册中心的心跳超时时间,默认为90s
也就是说,当一个服务异常down掉后,90s之后注册中心才会知道这个服务不可用了。在此期间,依旧会把这个服务当成正常服务。ribbon调用仍会把请求转发到这个服务上。为了避免这段期间出现无法提供服务的情况,要开启ribbon的重试功能,去进行其它服务提供者的重试。
二、Eureka-server的源码(com.netflix.eureka:eureka-core-1.4.6.jar中)
1. 这个包下有EurekaBootStrap这个类,这个类实现了ServletContextListener接口,它能够监听 ServletContext 对象的生命周期,当Servlet 容器启动或终止Web 应用时,会触发ServletContextEvent 事件,该事件由ServletContextListener 来处理。在 ServletContextListener 接口中定义了处理ServletContextEvent 事件的两个方法:contextInitialized和contextDestroyed
2. EurekaServerContext方法,可以看到,在方法中,新建了几个类,PeerAwareInstanceRegistryImpl和PeerEurekaNodes
3. 在resources目录下有个ApplicationResource类,类中有个方法,addInstance,这个方法就是接收注册服务请求的,这个方法会调用registry.register(info, “true”.equals(isReplication))这个方法,进行服务注册。进入这个方法就是在EurekaBootStrap中初始化的PeerAwareInstanceRegistryImpl类中的方法,在方法中,会获取InstanceInfo的续约时间信息,默认是90秒。然后调用父类的register方法注册,注册完后,会调用replicateToPeers方法,把这个节点的注册信息告诉其它Eureka Server节点。注册的信息会存放在map中,而且还是个两层的ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
,外层map的key是appName,也就是服务名,内层map的key是instanceId,也就是实例名。更新完map信息后,还会更新缓存信息;replicateToPeers方法中会通过for循环遍历所有的PeerEurekaNode,调用replicateInstanceActionsToPeers方法,把信息复制给其他的Eureka Server节点,replicateInstanceActionsToPeers方法中会调用node.register(info),这个方法通过启动了一个任务,来向其它节点同步信息的,不是实时同步的
参考文章,感谢博主的宝贵分享:
1. https://blog.csdn.net/chayangdz/article/details/82012937
2. https://blog.csdn.net/qq_23213739/article/details/78800857
3. https://blog.csdn.net/Michaelwubo/article/details/81449191