1.源码定位
com.netflix.discovery.DiscoveryClient#fetchRegistry
这个方法是用于从eureka服务端拉取服务信息使用的,那么拉取的流程是怎样的呢?我们先简单的读一下源码
源码版本: v1.7x
源码地址:https://github.com/Netflix/eureka/tree/v1.7.x
2. 源码简读
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
// 获取本地缓存
Applications applications = getApplications();
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);
}
// 设置注册表一致性hash值
applications.setAppsHashCode(applications.getReconcileHashCode());
// debug日志记录总实例数
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
// Notify about cache refresh before updating the instance remote status
// 在更新向事件总线发布注册表缓存更新通知
onCacheRefreshed();
// 基于eureka server 更新自身服务状态
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
上面总结一下:
- 本地没有拉全量覆盖
- 不允许增量更新拉全量覆盖
- 增量覆盖+eureka client和eureka server的一致性hash校验,校验不通过还得全量覆盖
- 更新一下注册表的一致性hash校验值
- 发布注册表更新事件通知
- 基于eureka server来更新自身的InstanceInfo状态
3.拉取注册表之全量拉取源码解读com.netflix.discovery.DiscoveryClient#getAndStoreFullRegistry
private void getAndStoreFullRegistry() throws Throwable {
// 版本控制器: 以确保过时的线程不会将注册表重置为旧版本
long currentUpdateGeneration = fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
Applications apps = null;
// 从eureka server 拉取到全量注册表
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
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");
}
}
小结:
- 注册表更新会有个版本控制,如果版本不对,那么就不会更新
- 拉取到全量列表后要进行是否过滤掉离线服务和打乱注册表(打乱顺序的原因我觉得吧,应该是为了让人在查看eureka 监控面板时上能够感受到注册表发生了拉取行为,你觉得呢?)
4.增量拉取com.netflix.discovery.DiscoveryClient#getAndUpdateDelta
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();
}
// 当delta为空时,表示eureka server 不允许增量拉取
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();
// 拉取注册版本信息比较,多线程执行时防止低版本覆盖高版本注册表
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
// 获取增量注册表携带的hashcode ,这个适用于检测客户端数据是否与服务端一致
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
// 拉取注册并并行操作加锁
try {
// 对增,删,改的服务实例进行更新
updateDelta(delta);
// 计算增量更新后的调和hash值,用来比对本地注册表和服务器注册表是否一致
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
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());
}
}
小结:
- queryClient.getDelta 拉取增量列表
- 判断增量列表是否为null,为null说明eureka server 不允许增量更新
- updateDelta增量更新(第五点详细分析)
- 比较本地注册表的reconcileHashCode 和 服务器的是否一致
- 不一致则拉取全量注册表进行替换
5.增量更新com.netflix.discovery.DiscoveryClient#updateDelta
/**
* Updates the delta information fetches from the eureka server into the
* local cache.
* 这里才是服务注册表增量更新的具体更新内容
* @param delta
* the delta information received from eureka server in the last
* poll cycle.
*/
private void updateDelta(Applications delta) {
// 首先说明一下 app表示服务(集群名称) instance表示服务实例 比如一个ServcieA服务有多个服务实例SserverA1和ServerA2
int deltaCount = 0;
for (Application app : delta.getRegisteredApplications()) {
// 一个应用可能有多个实例,比如服务A部署了多台机器
for (InstanceInfo instance : app.getInstances()) {
// 获取本地缓存的注册表
Applications applications = getApplications();
// region相关的不看,非AWS服务走不到
String instanceRegion = instanceRegionChecker.getInstanceRegion(instance);
if (!instanceRegionChecker.isLocalRegion(instanceRegion)) {
// 走不到 ,不看
Applications remoteApps = remoteRegionVsApps.get(instanceRegion);
if (null == remoteApps) {
remoteApps = new Applications();
remoteRegionVsApps.put(instanceRegion, remoteApps);
}
applications = remoteApps;
}
++deltaCount;
if (ActionType.ADDED.equals(instance.getActionType())) {
// 增量注册表的实例行为: 新增服务实例
// 从本地注册表中查看是否存在
Application existingApp = applications.getRegisteredApplications(instance.getAppName());
if (existingApp == null) {
// 新增实例到本地注册表
applications.addApplication(app);
}
logger.debug("Added instance {} to the existing apps in region {}", instance.getId(), instanceRegion);
// 这里其实是存在重复操作的,第一次注册该服务时,服务实例会被重复注册两次
applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);
} else if (ActionType.MODIFIED.equals(instance.getActionType())) {
//增量注册表的服务实例行为: 修改服务实例
// 这个跟新增的代码好像没啥区别啊
Application existingApp = applications.getRegisteredApplications(instance.getAppName());
if (existingApp == null) {
applications.addApplication(app);
}
logger.debug("Modified instance {} to the existing apps ", instance.getId());
applications.getRegisteredApplications(instance.getAppName()).addInstance(instance);
} else if (ActionType.DELETED.equals(instance.getActionType())) {
//增量注册表的服务实例行为: 删除服务实例
Application existingApp = applications.getRegisteredApplications(instance.getAppName());
if (existingApp == null) {
applications.addApplication(app);
}
logger.debug("Deleted instance {} to the existing apps ", instance.getId());
// 跟新增和修改唯一的差别
applications.getRegisteredApplications(instance.getAppName()).removeInstance(instance);
}
}
}
logger.debug("The total number of instances fetched by the delta processor : {}", deltaCount);
getApplications().setVersion(delta.getVersion());
// 打乱注册表和过滤下线实例,这里打乱的意义是啥呢?
getApplications().shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
// AWS云服务使用的,国内用不到
for (Applications applications : remoteRegionVsApps.values()) {
applications.setVersion(delta.getVersion());
applications.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
}
}
小结:
- 遍历增量注册列表,然后看每个服务实例是否在本地缓存中存在
- 如果存在则基于实例的getActionType来做出对应的行为,否则进行新增
- 其中新增和修改是进行服务实例删除再新增
- 删除是移除服务实例
小问题:
其实updateDelta这里有点问题,如果一个服务实例第一次进行增量新增时,会先加到注册表再从注册表移除,再新增到注册表
而删除会出现的情况是,我本地没有这个,但是你服务器发布的增量是删除,那么我就要先添加,再删除。