本文会详细描述EurekaServer关于注册表信息的缓存机制。
Eureka 之所以是一个遵循 AP 原则的注册中心,内部设计的缓存结构肯定是不可或缺的,有了缓存、自我保护机制等特性,使得 EurekaServer 可以在集群中一些节点宕机时还可以让整体继续提供服务,而不至于因为像 Zookeeper 那样进行 Leader 的重新选举导致集群不可用。
缓存的设计 本身不是C(一致性)的设计,用来维持高可用。
ApplicationsResource #getContainers
@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) {
boolean isRemoteRegionRequested = null != regionsStr && !regionsStr.isEmpty();
String[] regions = null;
if (!isRemoteRegionRequested) {
EurekaMonitors.GET_ALL.increment();
} else {
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();
}
// 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)) {
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 {
//responseCache:缓存相关类
response = Response.ok(responseCache.get(cacheKey))
.build();
}
return response;
}
EurekaServer设计的缓存
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
// 配置是否开启读写缓存
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
// 缓存更新的时间间隔,默认30秒
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
// 使用CacheBuilder构建读写缓存,构建时需要设置缓存加载器CacheLoader
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()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
// 缓存加载策略
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
// 配置是否开启只读缓存,如果开启则会启用定时任务,将读写缓存的数据定期同步到只读缓存
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} // catch ......
}
读写缓存readWriteCacheMap
private final LoadingCache<Key, Value> readWriteCacheMap;
读写缓存可读可写,用来做实时数据更新,这里是用Guava Cache来实现的。
只读缓存readOnlyCacheMap
与 readWriteCacheMap 的类型不同,readOnlyCacheMap 的类型仅为 ConcurrentHashMap :
private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<>();
只读缓存里的数据是不存在数据有效期的。
每次服务实例的状态发生变动时,都是只影响
readWriteCacheMap,不影响readOnlyCacheMap,所以会出现可用性强但数据可能不强一致的情况
如果开启只读缓存,则会使用一个定时任务来同步读写缓存中的数据
// 配置是否开启只读缓存,如果开启则会启用定时任务,将读写缓存的数据定期同步到只读缓存
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
getCacheUpdateTask
private TimerTask getCacheUpdateTask() {
return () -> {
logger.debug("Updating the client cache from response cache");
for (Key key : readOnlyCacheMap.keySet()) {
// logger
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} // catch ......
}
};
}
默认情况下,我们会从只读缓存中获取值,如果获取不到,则从读写缓存中获取,并且将获取的数据写入只读缓存中。
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
// ......
}
小结
- EurekaServer 的内部缓存包括两部分:读写缓存、只读缓存;
- 读写缓存中的数据在添加时会同时放入只读缓存;
- 默认情况下,只读缓存每隔一段时间会同步读写缓存中最新的数据
从EurekaServer的缓存设计来看,不难得知,Eureka是很典型的AP设计,抛弃了C,利用多级缓存提高性能,可以允许数据的不一致性。Eureka没有实现数据的强一致性,不过高可用的体现在于EurekaServer集群中的各节点相互注册,保证每台EurekaServer中都有一份完整的注册表,这样某台机器宕机后,与之关联的EurekaClient会选择另一台机器作为注册中心来注册服务、发现服务以及维持心跳,不会影响正常的服务注册与发现流程。