处理客户端全量下载请求
1. 检查服务器是否允许访问注册表
入口ApplicationsResource,getContainers方法:
//ApplicationsResource.java
@GET
public Response getContainers(@PathParam("version") String version,
@HeaderParam(HEADER_ACCEPT) String acceptHeader,
@HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
@HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
@Context UriInfo uriInfo,
@Nullable @QueryParam("regions") String regionsStr) {
//regionsStr不空,则isRemoteRegionRequested为true
//代表这次请求需要下载包括远程region中的注册表
boolean isRemoteRegionRequested = null != regionsStr && !regionsStr.isEmpty();
String[] regions = null;
if (!isRemoteRegionRequested) {
EurekaMonitors.GET_ALL.increment();//全量下载计数器+1
} else {
//regionsStr逗号分割,打乱
regions = regionsStr.toLowerCase().split(",");
Arrays.sort(regions); // So we don't have different caches for same regions queried in different order.
//因此,对于以不同顺序查询的相同区域,我们没有不同的缓存。
EurekaMonitors.GET_ALL_WITH_REMOTE_REGIONS.increment();//远程region全量下载计数器+1
}
// Check if the server allows the access to the registry. The server can
// restrict access if it is not
// ready to serve traffic depending on various reasons.
// 检查服务器是否允许访问注册表。 如果服务器由于各种原因尚未准备好服务流量,则可以限制访问。
if (!registry.shouldAllowAccess(isRemoteRegionRequested)) {
//判断是否允许访问注册表,不允许就返回403,拒绝请求
return Response.status(Status.FORBIDDEN).build();
}
CurrentRequestVersion.set(Version.toEnum(version));
KeyType keyType = Key.KeyType.JSON;
String returnMediaType = MediaType.APPLICATION_JSON;
if (acceptHeader == null || !acceptHeader.contains(HEADER_JSON_VALUE)) {
keyType = Key.KeyType.XML;
returnMediaType = MediaType.APPLICATION_XML;
}
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);
Response response;
if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
response = Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
response = Response.ok(responseCache.get(cacheKey))
.build();
}
return response;
}
先看shouldAllowAccess方法,判断是否允许访问:
//PeerAwareInstanceRegistryImpl.java
//检查是否允许注册表访问,或者服务器是否处于无法全部获取注册表信息的情况。
//如果服务器无法在启动时从对等eureka节点获取注册表信息,则在
//{@link com.netflix.eureka.EurekaServerConfig#getWaitTimeInMsWhenSyncEmpty()}中指定的期限内,服务器不会返回注册表信息。
public boolean shouldAllowAccess(boolean remoteRegionRequired) {
if (this.peerInstancesTransferEmptyOnStartup) {
//如果服务器无法在启动时从对等eureka节点获取注册表信息,
//集群中第一个服务端实例启动的时候注册表数据肯定是空的,这个时候在
//getWaitTimeInMsWhenSyncEmpty指定的期限内,是不允许访问注册表的
if (!(System.currentTimeMillis() > this.startupTime + serverConfig.getWaitTimeInMsWhenSyncEmpty())) {
return false;
}
}
//如果这次下载,包括远程region的注册表
if (remoteRegionRequired) {
for (RemoteRegionRegistry remoteRegionRegistry : this.regionNameVSRemoteRegistry.values()) {
//如果需要远程region,那么
//所有remoteRegion中,只要有任何一个没有做好准备,就不允许访问
if (!remoteRegionRegistry.isReadyForServingData()) {
return false;
}
}
}
//如果副本传输中的实例计数返回零,并且等待时间尚未过去, 返回false,否则返回true
return true;
}
2. 全量下载
2.1 定义缓存key
回到Resouece,继续往后看:
//ApplicationsResource.java
@GET
public Response getContainers(@PathParam("version") String version,
@HeaderParam(HEADER_ACCEPT) String acceptHeader,
@HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
@HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
@Context UriInfo uriInfo,
@Nullable @QueryParam("regions") String regionsStr) {
...
//定义缓存key --start
CurrentRequestVersion.set(Version.toEnum(version));
KeyType keyType = Key.KeyType.JSON;
String returnMediaType = MediaType.APPLICATION_JSON;
if (acceptHeader == null || !acceptHeader.contains(HEADER_JSON_VALUE)) {
keyType = Key.KeyType.XML;
returnMediaType = MediaType.APPLICATION_XML;
}
//Key.EntityType.Application: 实体类型,Application代表获取注册表数据
//ResponseCacheImpl.ALL_APPS:实体名称,ALL_APPS表示全量下载
//keyType:key的数据类型,默认json
//CurrentRequestVersion.get():当前请求的版本号
//EurekaAccept.fromString(eurekaAccept):就两个值full, compact(紧凑的),应该是控制返回的数据是否需要紧凑的格式
//regions:要获取的region的列表
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS,
keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
);
//定义缓存key --end
//后面就是通过这个缓存key获取对应的注册表缓存信息的
Response response;
//acceptEncoding是从请求头中传过来的
//acceptEncoding是GIZP,代表需要将数据压缩后返回
if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
response = Response.ok(responseCache.getGZIP(cacheKey))
.header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
.header(HEADER_CONTENT_TYPE, returnMediaType)
.build();
} else {
//不需要压缩,则正常获取返回
response = Response.ok(responseCache.get(cacheKey))
.build();
}
return response;
}
2.2 从缓存中获取注册表
我们看responseCache.getGZIP方法,压缩方式获取注册表:
//ResponseCacheImpl.java
public byte[] getGZIP(Key key) {
//根据key获取注册表缓存信息
//shouldUseReadOnlyResponseCache:是否使用只读的响应缓存(配置文件可配,默认true)
Value payload = getValue(key, shouldUseReadOnlyResponseCache);
if (payload == null) {
return null;
}
//返回压缩后的数据
return payload.getGzipped();
}
看getValue方法:
//ResponseCacheImpl.java
@VisibleForTesting
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {//如果true,从只读缓存获取
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
//只读缓存为空的话,先从读写缓存获取,再保存到只读缓存
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
//否则直接从读写缓存中获取
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
三个问题:
- 读写缓存readWriteCacheMap、只读缓存readOnlyCacheMap,在哪构建的?有缓存key,缓存value是在哪构建的?
- 只读缓存readOnlyCacheMap,怎么从读写缓存中同步数据的?(上面已经看到读取的时候先从只读缓存获取,为空再从读写缓存中获取,然后放入只读缓存,但是之后如果读写缓存的数据更新了,怎么同步到只读缓存?其实还有一个定时任务)
- 用只读缓存为了解决什么问题?集合迭代稳定性:
一个线程在读取集合的过程中,另一个线程同时对集合中的元素进行修改、增删,那么读的线程在遍历的过程中就会发现集合中的数据一直在变化,是不稳定的,就会造成一些问题,用只读缓存可以解决这个问题。
读的线程读的是只读缓存的数据,写的线程写的是读写缓存中的数据,这样写的操作就不会对读有影响,只要写完以后把新的数据替换掉原来只读缓存中的数据即可。
2.3 读写缓存、只读缓存的构建
找到ResponseCacheImpl的构造器:
//ResponseCacheImpl.java的构造器
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
//可以看到读写缓存map就是在这构建的
//serverConfig.getInitialCapacityOfResponseCache : 配置文件配置的响应缓存的初始大小
//serverConfig.getResponseCacheAutoExpirationInSeconds():配置文件配置的缓存的过期时间,由此可以看出缓存value是有时效的
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
//指定缓存的过期时间
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
//指定一个缓存被移除的监听器
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
//一但过期被删除,会触发当前监听方法
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
//注意regionSpecificKeys是一个Multimap,它的一个键对应多个值
//cloneWithNoRegion的
// key是 不带区域的键
// value是 带区域的键的列表
// (key和value中的键除了region,其他参数完全一样)
//这里如果发现当前移除的key是带区域的,就把其和不带区域的键的
//映射关系移除
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
//这里就是根据key构建具体缓存内容的方法
//这个readWriteCacheMap我们是不需要put值的
//直接readWriteCacheMap.get(key)即可
//如果对应key的value不存在就会调用下面方法生成然后缓存
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
//如果发现key是带区域的,保存维护
//不带区域的键 与 带区域的键 的关系
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
//主要关注generatePayload方法
Value value = generatePayload(key);
return value;
}
});
if (shouldUseReadOnlyResponseCache) {//如果开启了使用只读响应缓存
//开启一个定时任务,这个任务就是用来处理 只读缓存 和 读写缓存 之间数据同步的
//是repeated定时任务,固定的间隔时间一直循环执行
//入参:
//缓存更新任务
//第一次执行的延迟
//每次定时循环执行的时间间隔
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
}
}
看一下默认的响应缓存的自动过期时间:
2.3.1 缓存清除
在读写缓存中,我们看到生成缓存,或者缓存过期移除的时候都修改了一个map,regionSpecificKeys:
看下这个map:
这个map维护了不带区域的缓存key 和 带区域的缓存key 之间的关系(缓存key中除了region其他参数完全一样)
那这个map是干啥用的呢?主要是清除缓存的时候用的,前面几章分析的流程,除了处理客户端续约请求,其他所有操作最后都调用了invalidateCache方法,让缓存失效:
现在我们看invalidateCache方法:
//AbstractInstanceRegistry.java
private void invalidateCache(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
// invalidate cache
responseCache.invalidate(appName, vipAddress, secureVipAddress);
}
//ResponseCacheImpl#invalidate.java
public void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
//可以看到,这里其实是想把 指定的微服务 所相关的 所有注册表类型的缓存 变为失效。
//并且注意这些缓存key都是不带region的!
for (Key.KeyType type : Key.KeyType.values()) {
for (Version v : Version.values()) {
invalidate(
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.compact),
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.compact),
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.compact)
);
if (null != vipAddress) {
invalidate(new Key(Key.EntityType.VIP, vipAddress, type, v, EurekaAccept.full));
}
if (null != secureVipAddress) {
invalidate(new Key(Key.EntityType.SVIP, secureVipAddress, type, v, EurekaAccept.full));
}
}
}
}
注意这些缓存key都是不带region的
!继续看invalidate:
/**
* Invalidate the cache information given the list of keys.
* 指定键列表的缓存信息无效。
*
* @param keys the list of keys for which the cache information needs to be invalidated.
*/
public void invalidate(Key... keys) {
for (Key key : keys) {
logger.debug("Invalidating the response cache key : {} {} {} {}, {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
//先将不带region的缓存key对应的缓存失效
readWriteCacheMap.invalidate(key);
//根据不带region的缓存key,获取映射关系中所有带region的key
//让带region的所有key的缓存失效
Collection<Key> keysWithRegions = regionSpecificKeys.get(key);
if (null != keysWithRegions && !keysWithRegions.isEmpty()) {
for (Key keysWithRegion : keysWithRegions) {
logger.debug("Invalidating the response cache key : {} {} {} {} {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
readWriteCacheMap.invalidate(keysWithRegion);
}
}
}
}
2.3.2 缓存value的创建(全量下载的数据内容)
现在我们要看一下缓存中的value到底是啥:
//ResponseCacheImpl.java
private Value generatePayload(Key key) {
Stopwatch tracer = null;
try {
String payload;
switch (key.getEntityType()) {//对应有三种类型的缓存
case Application:
//我们主要关注这个注册表类型
boolean isRemoteRegionRequested = key.hasRegions();
if (ALL_APPS.equals(key.getName())) {//ALL_APPS代表全量下载
if (isRemoteRegionRequested) {//是否包含远程region的注册表
tracer = serializeAllAppsWithRemoteRegionTimer.start();
//直接看getApplicationsFromMultipleRegions方法
//从多个region中获取注册表
payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeAllAppsTimer.start();
payload = getPayLoad(key, registry.getApplications());
}
} else if (ALL_APPS_DELTA.equals(key.getName())) {
//ALL_APPS_DELTA代表增量下载
...
} else {
tracer = serializeOneApptimer.start();
payload = getPayLoad(key, registry.getApplication(key.getName()));
}
break;
case VIP:
case SVIP:
tracer = serializeViptimer.start();
payload = getPayLoad(key, getApplicationsForVip(key, registry));
break;
default:
logger.error("Unidentified entity type: {} found in the cache key.", key.getEntityType());
payload = "";
break;
}
return new Value(payload);
} finally {
if (tracer != null) {
tracer.stop();
}
}
}
从多个region中获取注册表:
//AbstractInstanceRegistry.java
public Applications getApplicationsFromMultipleRegions(String[] remoteRegions) {
//判断是否包含远程region
boolean includeRemoteRegion = null != remoteRegions && remoteRegions.length != 0;
logger.debug("Fetching applications registry with remote regions: {}, Regions argument {}",
includeRemoteRegion, remoteRegions);
if (includeRemoteRegion) {
GET_ALL_WITH_REMOTE_REGIONS_CACHE_MISS.increment();//对应计数器+1
} else {
GET_ALL_CACHE_MISS.increment();//对应计数器+1
}
//新增一个注册表对象,这个是返回给客户端的注册表、也是缓存中的value
Applications apps = new Applications();
apps.setVersion(1L);
//registry是服务端本地注册表
for (Entry<String, Map<String, Lease<InstanceInfo>>> entry : registry.entrySet()) {
//先遍历本地注册表,key是微服务名称,value是内层map
Application app = null;
if (entry.getValue() != null) {
for (Entry<String, Lease<InstanceInfo>> stringLeaseEntry : entry.getValue().entrySet()) {
//遍历本地注册表的内层map,key是instanceInfoId,value是实例信息
Lease<InstanceInfo> lease = stringLeaseEntry.getValue();
if (app == null) {
//微服务对应的Application为空,则新建一个
app = new Application(lease.getHolder().getAppName());
}
//添加实例到缓存(Application是客户端使用的注册表相关的数据结构)
//decorateInstanceInfo:以前跟过,把lease包装成InstanceInfo
app.addInstance(decorateInstanceInfo(lease));
}
}
if (app != null) {
//添加到缓存的注册表中(apps,Applications是客户端使用的注册表的数据结构)
apps.addApplication(app);
}
}
if (includeRemoteRegion) {
//本地处理完以后,处理远程region的注册表
//注意此时处理的是已经缓存在Server端本地的远程region注册表信息
//(Server端什么时候从远程region下载的,暂时不清楚)
//遍历需要获取的region
for (String remoteRegion : remoteRegions) {
//获取远程region对应的Registry
RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);
if (null != remoteRegistry) {
//获取远程region的注册表
Applications remoteApps = remoteRegistry.getApplications();
//遍历注册表中的每个Application
for (Application application : remoteApps.getRegisteredApplications()) {
//判断这个微服务的注册表是否允许从这个region中获取
if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {
logger.info("Application {} fetched from the remote region {}",
application.getName(), remoteRegion);
//apps就是当前缓存key对应的缓存value,也是返回给客户端的注册表
//可以看到,这里将本地的和远程的是整合在一起处理的
Application appInstanceTillNow = apps.getRegisteredApplications(application.getName());
if (appInstanceTillNow == null) {
//如果Application不存在进行构建
appInstanceTillNow = new Application(application.getName());
apps.addApplication(appInstanceTillNow);
}
for (InstanceInfo instanceInfo : application.getInstances()) {
//添加远程region中的实例信息
appInstanceTillNow.addInstance(instanceInfo);
}
} else {
logger.debug("Application {} not fetched from the remote region {} as there exists a "
+ "whitelist and this app is not in the whitelist.",
application.getName(), remoteRegion);
}
}
} else {
logger.warn("No remote registry available for the remote region {}", remoteRegion);
}
}
}
apps.setAppsHashCode(apps.getReconcileHashCode());
return apps;
}
2.3.3 只读缓存 和 读写缓存的数据同步
最后看一下只读缓存和读写缓存的同步逻辑,回到ResponseCacheImpl的构造器:
//ResponseCacheImpl.java的构造器
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
...
if (shouldUseReadOnlyResponseCache) {//如果开启了使用只读响应缓存
//开启一个定时任务,是repeated定时任务,固定的间隔时间一直循环执行
//入参:
//缓存更新任务
//第一次执行的延迟
//每次定时循环执行的时间间隔
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
}
}
默认应该是30秒同步一次
直接看缓存更新任务:
//ResponseCacheImpl.java
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
logger.debug("Updating the client cache from response cache");
for (Key key : readOnlyCacheMap.keySet()) {
//遍历只读缓存map的key
if (logger.isDebugEnabled()) {
logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType());
}
try {
//ThreadLocal中设置请求版本号
CurrentRequestVersion.set(key.getVersion());
//根据key从读写缓存map中获取cacheValue
Value cacheValue = readWriteCacheMap.get(key);
//从只读缓存map中获取cacheValue
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
//直接比较地址值即可
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
}
}
}
};
}
这里可以看到定时任务主要是将只读缓存map中已经存在的所有key对应的value进行更新,至于什么时候向只读缓存map中添加值,其实之前我们已经看到了,在一开始获取的时候: