EurekaServer的缓存机制原理

271 阅读3分钟

本文会详细描述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);
            }
    // ......
}

小结

  1. EurekaServer 的内部缓存包括两部分:读写缓存、只读缓存
  2. 读写缓存中的数据在添加时会同时放入只读缓存;
  3. 默认情况下,只读缓存每隔一段时间会同步读写缓存中最新的数据

从EurekaServer的缓存设计来看,不难得知,Eureka是很典型的AP设计,抛弃了C,利用多级缓存提高性能,可以允许数据的不一致性。Eureka没有实现数据的强一致性,不过高可用的体现在于EurekaServer集群中的各节点相互注册,保证每台EurekaServer中都有一份完整的注册表,这样某台机器宕机后,与之关联的EurekaClient会选择另一台机器作为注册中心来注册服务、发现服务以及维持心跳,不会影响正常的服务注册与发现流程。