eureka 缓存更新剖析

本文介绍了服务注册与发现机制中的关键配置参数,包括注册中心刷新响应缓存、客户端拉取实例信息及Ribbon刷新本地缓存的频率。这些配置对于实现服务的平滑发布至关重要。

影响服务注册信息生效主要有三个配置:

  • 注册中心刷新响应缓存的频率,单位ms
  • 客户端从注册中心拉取实例信息的频率,单位为s
  • ribbon刷新本地缓存的频率,单位ms

如果在配合服务的上下线就可以实现平滑发布。

客户端从注册中心拉取实例信息的频率,单位为s

key:eureka.client.registryFetchIntervalSeconds

默认值:30s

初始化源码

新客户端缓存的任务在DiscoveryClient的构造函数中进行初始化

    @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
                      initScheduledTasks();
                    }
                    
                    
                    
                    
  /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            //获取刷新时间,默认为5s
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            cacheRefreshTask = new TimedSupervisorTask(
                    "cacheRefresh",
                    scheduler,
                    cacheRefreshExecutor,
                    registryFetchIntervalSeconds,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new CacheRefreshThread()
            );
            scheduler.schedule(
                    cacheRefreshTask,
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            // Heartbeat timer
            heartbeatTask = new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            );
            scheduler.schedule(
                    heartbeatTask,
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }
    
    
    
    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();

            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;
    }

ribbon刷新本地缓存的频率,单位为ms

key: ribbon.ServerListRefreshInterval
默认值:30 * 1000 ms

初始化源码

ribbon缓存更新在PollingServerListUpdater中实现



    public PollingServerListUpdater() {
        this(LISTOFSERVERS_CACHE_UPDATE_DELAY, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
    }
    
    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };

            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }


而初始化是在RibbonClientConfiguration中进行的

	@Bean
	@ConditionalOnMissingBean
	public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
		return new PollingServerListUpdater(config);
	}

而RibbonClientConfiguration中是在第一次调用是进行初始化的

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
	public SpringClientFactory() {
		super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
	}
}	

注册中心刷新响应缓存的频率,单位ms

key:eureka.server.response-cache-update-interval-ms

默认值:30 * 1000 ms

初始化源码
    ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        this.registry = registry;
        //获取更新间隔时间
        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
        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 (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
        }
    }
    
    
    //同步方法
    private TimerTask getCacheUpdateTask() {
        return new TimerTask() {
            @Override
            public void run() {
                logger.debug("Updating the client cache from response cache");
                for (Key key : readOnlyCacheMap.keySet()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
                                key.getEntityType(), key.getName(), key.getVersion(), key.getType());
                    }
                    try {
                        CurrentRequestVersion.set(key.getVersion());
                        Value cacheValue = readWriteCacheMap.get(key);
                        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);
                    } finally {
                        CurrentRequestVersion.remove();
                    }
                }
            }
        };
    }

<think>嗯,用户这次想深入了解Eureka缓存机制原理和实现方式。从历史对话看,用户之前已经了解过Eureka的缺点,特别是服务发现延迟问题,现在想聚焦在缓存机制这个技术细节上。用户应该是个开发者或架构师,正在评估注册中心技术选型。 用户引用的资料质量不错,都是讲Eureka缓存机制的专业内容。引用[1][2]提到二级缓存机制,引用[3]指出缓存导致状态更新滞后,引用[4]说明客户端缓存,引用[5]给出优化建议。这些正好能系统解答用户的疑问。 需要重点解释三个层面:首先是服务端的两级缓存设计(读写缓存+只读缓存),这是Eureka实现AP特性的核心;其次是客户端的本地缓存机制,这是造成延迟的关键;最后要说明这些缓存如何相互作用导致服务状态更新滞后。用户可能特别关心“为什么下线服务还会被调用”这个痛点问题。 图表展示会很有帮助,特别是缓存数据流动的示意图。另外要给出具体优化方案,比如怎么调整默认的30秒更新周期。用户引用[5]里提到的kill命令差异和延迟下线技巧很实用,应该重点展开。 最后要自然引出相关问题,比如和Nacos缓存机制对比,或者如何验证缓存配置生效。这些问题都能帮助用户深入理解机制。</think>Eureka缓存机制是其实现 AP 特性(高可用、分区容忍)的核心设计,但也导致服务状态更新滞后。其核心原理基于**服务端二级缓存**和**客户端本地缓存**的协同工作,具体实现如下: --- ### 一、服务端二级缓存机制 #### 1. **读写缓存(ReadWriteCacheMap)** - **作用**:存储服务实例的**实时注册信息**,直接对接注册表(Registry) - **数据写入**:服务实例注册/下线时,数据**先写入注册表,再同步到读写缓存** - **数据读取**:客户端拉取服务列表时,默认**不直接读取该缓存**(避免并发压力) - **过期策略**:基于**定时任务**主动刷新,默认每 **30 秒** 同步注册表数据[^1][^2] #### 2. **只读缓存(ReadOnlyCacheMap)** - **作用**:对外提供**最终一致性的服务列表**,客户端实际读取的缓存层 - **数据同步**:通过定时任务从 `ReadWriteCacheMap` **增量复制**数据,默认周期 **30 秒** - **关键设计**: ```mermaid graph LR A[服务注册/下线] --> B(Registry 注册表) B --> C[ReadWriteCacheMap] C -->|定时同步| D[ReadOnlyCacheMap] D --> E[Client 获取服务列表] ``` #### 3. 缓存更新延迟分析 | 阶段 | 默认延迟 | 最坏情况累计延迟 | |---------------------|----------|------------------| | 服务下线到注册表更新 | 0 | - | | ReadWriteCache 刷新 | 30 秒 | 30 秒 | | ReadOnlyCache 同步 | 30 秒 | 60 秒 | | 客户端缓存更新 | 30 秒 | **90 秒** | > 💡 服务下线后,客户端最长可能 **90 秒** 才能感知(3×30秒)[^3][^5] --- ### 二、客户端缓存机制 1. **本地服务列表缓存** - Ribbon 等客户端组件会将服务列表**缓存在内存**中 - 默认每 **30 秒** 向 Eureka Server 拉取更新(通过 `ribbon.ServerListRefreshInterval` 配置) - 更新前所有请求使用**旧服务列表**,导致调用已下线实例[^4] 2. **容错重试机制** - 客户端访问故障节点时(如 ConnectTimeout),可通过 Spring Cloud Retry **重试其他节点** - 需配合 Hystrix 等熔断器使用,示例配置: ```yaml ribbon: MaxAutoRetries: 1 # 同一节点重试次数 MaxAutoRetriesNextServer: 2 # 切换节点次数 ``` --- ### 三、缓存机制导致的典型问题 #### 场景:服务下线后仍被调用 ```mermaid sequenceDiagram participant Provider as 服务提供者 participant Server as Eureka Server participant Consumer as 服务消费者 Provider->>Server: 发送下线请求 Server->>Server: 更新 Registry 和 ReadWriteCacheMap Note right of Server: 延迟 30 秒 Server->>Server: 同步到 ReadOnlyCacheMap Consumer->>Server: 定时拉取服务列表(缓存更新) Consumer->>Provider: 调用已下线服务 → 失败 ``` > 原因:客户端在三级缓存(ReadOnlyCacheMap、客户端缓存、Ribbon缓存更新前持续使用旧列表[^3][^5] --- ### 四、优化缓存延迟的方案 1. **缩短缓存刷新周期** ```yaml # Eureka Server 配置 eureka: server: response-cache-update-interval-ms: 3000 # ReadWriteCache刷新周期(默认30s) response-cache-auto-expiration-in-seconds: 3 # ReadOnlyCache自动过期时间 # 客户端配置 ribbon: ServerListRefreshInterval: 2000 # Ribbon服务列表刷新周期(默认30s) ``` 2. **服务优雅下线流程** - 先调用 `PUT /eureka/apps/{appId}/{instanceId}/status?value=OUT_OF_SERVICE` 标记服务不可用 - 等待 **90 秒**(覆盖缓存周期)后再停止服务进程 - 使用 `kill -15` 触发 `shutdown hook`,确保主动注销[^5] 3. **禁用只读缓存**(牺牲可用性换取实时性) ```yaml eureka: server: use-read-only-response-cache: false ``` --- > **关键结论**:Eureka 通过二级缓存实现 AP 特性,但带来秒级延迟。生产环境中需根据业务容忍度调整缓存参数,并结合服务优雅下线和重试机制规避调用故障[^1][^5]。 --- ### 相关问题 1. **如何验证 Eureka 服务端缓存是否按预期更新?** 2. **Eureka 与 Nacos 的缓存机制设计有何本质区别?** 3. **在 Kubernetes 中如何优化 Eureka 客户端的缓存更新策略?** 4. **禁用 ReadOnlyCacheMap 会对 Eureka Server 性能产生多大影响?** 5. **服务优雅下线时,如何动态计算最优等待时间?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值