Eureka系列(三)获取服务Client端具体实现
获取服务Client 端流程
我们先看下面这张图片,这张图片简单描述了下我们Client是如何获取到Server已续约实例信息的流程:
从图片中我们可以知晓大致流程就是Client会自己开启一个定时任务,然后根据不同的条件去调用Server端接口得到所有已续约服务的信息,然后合并到自己的缓存数据中。下面我们详情了解下上述流程在源码中的具体实现。
获取服务Client端源码分析
我们先来看看服务获取定时任务的初始化。那我们的服务获取定时任务什么时候会被初始化呢,那肯定是我们启用我们Eureka Client的时候呗,当我们启动Client时,Eureka会先处理相关的配置,然后初始化我们Client的相关信息,我们的定时任务也就是此时进行的初始化,更具体地说我们的服务续约定时任务就是在DiscoveryClient这个类中initScheduledTasks方法中被初始化的。具体代码如下:
private final ScheduledExecutorService scheduler;
private void initScheduledTasks() {
…省略其他代码
// 初始化定时拉取服务注册信息
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
…省略其他代码
}
由此可见,我们的定时任务其实是Client进行初始化完成的,并且还是使用ScheduledExecutorService线程池来完成我们的定时任务。我们下面就看看CacheRefreshThread这个类是如何实现获取服务的流程:
class CacheRefreshThread implements Runnable {
public void run() {
refreshRegistry();
}
}
不多说,我们接着看refreshRegistry()方法:
@VisibleForTesting
void refreshRegistry() {
…省略部分代码
boolean success = fetchRegistry(remoteRegionsModified); // 获取实例信息
if (success) {
registrySize = localRegionApps.get().size();
lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
}
…省略部分代码
}
这里不做太多解释,我们接着看fetchRegistry()方法:
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. 是否对某个region特别关注;
//3. 外部调用时是否通过入参指定全量更新;
//4. 本地还未缓存有效的服务列表信息;
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;
}
由此可见,fetchRegistry 方法主要是判断我们获取实例信息是进行全量更新还是增量更新。如果条件成立,则我们会进行全量更新,否则则进行批量更新。下面我们简单介绍下getAndStoreFullRegistry() 全量更新、getAndUpdateDelta(applications)批量更新方法:
// 全量更新
private void getAndStoreFullRegistry() throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
Applications apps = null;
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
// 调用服务端接口得到全量的实例信息
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
// 将实例信息存进Applications
apps = httpResponse.getEntity();
}
logger.info("The response status is {}", httpResponse.getStatusCode());
if (apps == null) {
logger.error("The application is null for some reason. Not storing this information");
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
localRegionApps.set(this.filterAndShuffle(apps));
logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
} else {
logger.warn("Not updating applications as another thread is updating it already");
}
}
getAndStoreFullRegistry() 简单来说就是通过调用Eureka服务端提供的http接口获取到全部实例信息,然后将实例信息存进我们的Applications中。
// 批量更新
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
Applications delta = null;
// 得到增量的实例信息
EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
delta = httpResponse.getEntity();
}
if (delta == null) { // 如果增量信息为空,则进行一次全量更新
logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
+ "Hence got the full registry.");
getAndStoreFullRegistry();
}
//考虑到多线程同步问题,这里通过CAS来确保请求发起到现在是线程安全的,
//如果这期间fetchRegistryGeneration变了,就表示其他线程也做了类似操作,因此放弃本次响应的数据
else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
try {
// 合并增量实例信息
updateDelta(delta);
// 用合并了增量数据之后的本地数据来生成一致性哈希码
reconcileHashCode = getReconcileHashCode(applications);
} finally {
fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
// There is a diff in number of instances for some reason
//Eureka server在返回增量更新数据时,也会返回服务端的一致性哈希码,
//理论上每次本地缓存数据经历了多次增量更新后,计算出的一致性哈希码应该是和服务端一致的,
//如果发现不一致,就证明本地缓存的服务列表信息和Eureka server不一致了,需要做一次全量更新
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
reconcileAndLogDifference(delta, reconcileHashCode); // this makes a remoteCall
}
} else {
logger.warn("Not updating application delta as another thread is updating it already");
logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
}
}
getAndUpdateDelta 方法简单来说,就是首先调用Eureka服务端提供的增量信息接口得到增量实例信息,然后进行判断,如果增量为null,为了数据准确性,调用一下全量更新实例接口更新实例信息。如果增量信息不为空,则进行一个cas处理,如果成功,则进行增量信息的合并。最后会再进行一次判断,判断从服务端得到的批量实例信息计算得到的HashCode是否和从服务端得到的实例信息HashCode值是否相等,如果不相等则会调用reconcileAndLogDifference 方法,再次进行全量更新实例信息。下面我们就简单看下reconcileAndLogDifference这个方法:
private void reconcileAndLogDifference(Applications delta, String reconcileHashCode) throws Throwable {
logger.debug("The Reconcile hashcodes do not match, client : {}, server : {}. Getting the full registry",
reconcileHashCode, delta.getAppsHashCode());
RECONCILE_HASH_CODES_MISMATCH.increment();
long currentUpdateGeneration = fetchRegistryGeneration.get();
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
// 调用服务端接口得到全量的实例信息
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
Applications serverApps = httpResponse.getEntity();
if (serverApps == null) {
logger.warn("Cannot fetch full registry from the server; reconciliation failure");
return;
}
if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
localRegionApps.set(this.filterAndShuffle(serverApps));
getApplications().setVersion(delta.getVersion());
logger.debug(
"The Reconcile hashcodes after complete sync up, client : {}, server : {}.",
getApplications().getReconcileHashCode(),
delta.getAppsHashCode());
} else {
logger.warn("Not setting the applications map as another thread has advanced the update generation");
}
}
由此可见,reconcileAndLogDifference方法和我们getAndStoreFullRegistry方法调用的接口一样,都是调用Eureka服务端提供的全量实例信息接口,然后更新我们自己的实例信息。
总的来说,获取服务Client端的执行流程就可以分为以下两步:
1.初始化一个定时任务,默认30s
2.定时任务中根据不同的情况调用不同的方法来更新本地实例缓存信息
在定时任务中获取实例信息我们也可以分为以下几步:
1.判断是否需要全量更新
2.条件成立则进行全量更新
a.将全量更新数据更新到本地缓存中
3.条件不成立则进行批量更新
a.判断批量更新数据是否为空,是则进行一次全量更新
b.将批量更新数据合并到本地缓存中
c.判断批量更新数据计算得到的HashCode是否和服务端传过来的HashCode相等,如果不相等,说明数据有问题,需要进行一次全量更新
Eureka系列(三)获取服务Client端具体实现的更多相关文章
- Eureka系列(四) 获取服务Server端具体实现
获取服务 Server端流程 我们先看下面这张图片,这张图片简单描述了下我们EurekaClient在调用EurekaServer 提供的获取服务Http接口,Server端实现接口执行的大致流程 ...
- [Python Study Notes]CS架构远程访问获取信息--Client端v2.0
更新内容: 1.增加内存信息获取 2.增加电池信息获取 3.增加磁盘信息获取 4.重新布局窗体 5.增加窗体名称 6.增加连接成功之前,不可按压 效果图: '''''''''''''''''''''' ...
- [Python Study Notes]CS架构远程访问获取信息--Client端v1.0
更新内容: 1.添加entry栏默认ip和port口 2.修正退出功能 3.添加退出自动关闭窗口功能 4.优化cpu显示为固定保留两位小数 '''''''''''''''''''''''''''''' ...
- [Python Study Notes]CS架构远程访问获取信息--Client端
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ...
- springcloud系列三 搭建服务模块
搭建服务模块为了模拟正式开发环境,只是少写了service层直接在controller里面直接引用,直接上图和代码:更为方便: 创建完成之后加入配置: pom.xml文件: <?xml vers ...
- 第三章 服务治理:Spring Cloud Eureka
Spring Cloud Eureka是Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能.Spri ...
- Eureka系列(二) 服务注册Server端具体实现
服务注册 Server端流程 我们先看下面这张图片,这张图片简单描述了下我们EurekaClient 在调用EurekaServer 提供的服务注册Http接口,Server端实现接口执行的大致流 ...
- SpringCloud系列三:SpringSecurity 安全访问(配置安全验证、服务消费端处理、无状态 Session 配置、定义公共安全配置程序类)
1.概念:SpringSecurity 安全访问 2.具体内容 所有的 Rest 服务最终都是暴露在公网上的,也就是说如果你的 Rest 服务属于一些你自己公司的私人业务,这样的结果会直接 导致你信息 ...
- Spring Cloud微服务笔记(三)服务治理:Spring Cloud Eureka快速入门
服务治理:Spring Cloud Eureka 一.服务治理 服务治理是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册与发现. 1.服务注册: 在服务治理框架中,通常会构 ...
随机推荐
- 思维导图软件MindManager新手入门教程
MindManager是一款创造.管理和交流思想的思维导图软件,其直观清晰的可视化界面和强大的功能可以快速捕捉.组织和共享思维.想法.资源和项目进程等等.MindManager新手入门教程专为新手用户 ...
- Vegas视频的音频叠加效果怎么实现,可以用其他软件吗
有时我们会用Vegas为某段影片配音,我们要怎么把配音和背景声融合在一起呢?想必马上会有人反应过来:让配音和背景声分别置于两条轨道上就好了.这当然是一个相当好的方式. 可是,如果我想要把两段音频合成一 ...
- Java蓝桥杯练习题——求小数n位后3个数
求整数除法小数点后第n位开始的3位数 位数不足的补0,如0.125小数第3位后三位:0.12500→500 输入格式:a b n,空格分开,a是被除数,b是除数,n是小数后的位置 输出格式:3位数字, ...
- 在运行tsc编译.ts文件时,“因为在此系统上禁止运行脚本” 怎么解决?
tsc : 无法加载文件 C:\Users\Administrator\AppData\Roaming\npm\tsc.ps1,因为在此系统上禁止运行脚本.有关详细信息,请参阅 https:/go.m ...
- JZOJ 【NOIP2017提高A组模拟9.14】捕老鼠
JZOJ [NOIP2017提高A组模拟9.14]捕老鼠 题目 Description 为了加快社会主义现代化,建设新农村,农夫约(Farmer Jo)决定给农庄里的仓库灭灭鼠.于是,猫被农夫约派去捕 ...
- 记一次MongoDB的失败导出
MongoDB用的是阿里云的,今天想着把原来的数据导出进行一次去重处理,整理下数据.操作了好几个小时,还是未能成功导出. MongoDB用的是阿里云的专有网络连接,本想通过公网直接访问,申请了公网地址 ...
- Python文件学习遇到的问题
关于open函数文件打开模式的有意思的一个现象 关于Python中中文文本文件使用二进制方式读取后的解码UnicodeDecodeError问题 Python中str类型的字符串写入二进制文件时报Ty ...
- PyQt(Python+Qt)学习随笔:图例解释QFrame类的lineWidth、midLineWidth以及frameWidth属性
老猿Python博文目录 老猿Python博客地址 QFrame类有四个跟宽度相关的属性,分别是width.lineWidth.midLineWidth以及frameWidth属性.width是整个Q ...
- 深入分析 Java 乐观锁
前言 激烈的锁竞争,会造成线程阻塞挂起,导致系统的上下文切换,增加系统的性能开销.那有没有不阻塞线程,且保证线程安全的机制呢?--乐观锁. 乐观锁是什么? 操作共享资源时,总是很乐观,认为自己可以成功 ...
- NOIP2020 浙江 游记
day - ? 由于 CSP-S 的失利,感觉这一次 NOIP 的心态反而是非常的淡定,感觉反正已经炸过一次了,再炸一次好像也没什么,就抱着这样的心态去考试的. day 1 考试当天起晚了,到考场的时 ...