Eureka三级缓存架构

Eureka Server缓存机制概述

Eureka是Netflix开源的服务发现框架,它具有高可用、可扩展、易于部署等优点,被广泛应用于微服务架构中。其中一个重要的组件就是Eureka Server,它负责维护服务注册表,以及向客户端提供服务注册信息。

在Eureka Server中,缓存机制是一个非常重要的优化点,它可以显著提升服务注册表的性能和可靠性。本文将深入剖析Eureka Server的缓存机制,并通过源码分析,探讨其实现原理和优化策略。

三级缓存

一级缓存:只读缓存 readOnlyCacheMap,数据结构 ConcurrentHashMap。相当于数据库。

二级缓存:读写缓存 readOnlyCacheMap,Guava Cache。相当于 Redis 主从架构中主节点,既可以进行读也可以进行写。

三级缓存:本地注册表 registry,数据结构 ConcurentHashMap。相当于 Redis 主从架构的从节点,只负责读。

多级缓存的意义

这里为什么要设计多级缓存呢?原因很简单,就是当存在大规模的服务注册和更新时,如果只是修改一个ConcurrentHashMap数据,那么势必因为锁的存在导致竞争,影响性能。

而Eureka又是AP模型,只需要满足最终可用就行。所以它在这里用到多级缓存来实现读写分离。注册方法写的时候直接写内存注册表,写完表之后主动失效读写缓存。

获取注册信息接口先从只读缓存取,只读缓存没有再去读写缓存取,读写缓存没有再去内存注册表里取(不只是取,此处较复杂)。并且,读写缓存会更新回写只读缓存

  • responseCacheUpdateIntervalMs :readOnlyCacheMap 缓存更新的定时器时间间隔,默认为30秒

  • responseCacheAutoExpirationInSeconds : readWriteCacheMap 缓存过期时间,默认为 180 秒 。

Eureka Server内存缓存实现原理

Eureka Server的内存缓存机制是通过EurekaRegistry类实现的。EurekaRegistry类是Eureka Server中最核心的组件之一,它负责维护服务注册表,并提供服务注册、注销、查询等接口。在EurekaRegistry类中,内存缓存主要通过以下两个类实现:ReadOnlyClusterRegistry和PeerAwareInstanceRegistry。

ReadOnlyClusterRegistry类是EurekaRegistry的一个内部类,它主要用于向客户端提供服务注册信息。当客户端向Eureka Server发起查询请求时,Eureka Server会从ReadOnlyClusterRegistry中获取服务注册信息,并返回给客户端。同时,ReadOnlyClusterRegistry还负责监控服务注册表的变化,并及时更新缓存中的服务注册信息。

PeerAwareInstanceRegistry类则是EurekaRegistry的另一个内部类,它主要用于维护服务注册表。当Eureka Server收到客户端的服务注册请求时,PeerAwareInstanceRegistry会将服务注册信息添加到内存缓存中。当客户端发起注销请求时,PeerAwareInstanceRegistry会将服务注册信息从内存缓存中删除。同时,PeerAwareInstanceRegistry还负责定时从其他Eureka Server节点获取服务注册信息,并更新本地内存缓存。

PeerAwareInstanceRegistry的内存缓存是通过ConcurrentHashMap实现的。ConcurrentHashMap是Java中线程安全的HashMap实现,它可以支持多个线程同时对同一个HashMap进行操作,从而保证线程安全。在PeerAwareInstanceRegistry中,ConcurrentHashMap主要用于存储服务注册信息。当服务注册信息发生变化时,PeerAwareInstanceRegistry会通过ConcurrentHashMap的put、remove等方法更新内存缓存中的服务注册信息。

下面是一段示例代码,演示了如何使用Eureka Server的内存缓存机制:

public class EurekaServerDemo {

    public static void main(String[] args) throws Exception {
        // 创建Eureka Server实例
        EurekaServerContext serverContext = EurekaServerContextHolder.getInstance().getServerContext();
        EurekaServerConfig serverConfig = serverContext.getServerConfig();
        EurekaServer eurekaServer = new EurekaServer(serverConfig);

        // 启动Eureka Server
        eurekaServer.start();

        // 获取Eureka Server的内存缓存
        PeerAwareInstanceRegistry registry = serverContext.getRegistry();

        // 添加服务注册信息
        InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder()
                .setAppName("demo")
                .setInstanceId("demo-1")
                .setHostName("localhost")
                .setIPAddr("127.0.0.1")
                .setPort(8080)
                .build();
        registry.register(instanceInfo);

        // 查询服务注册信息
        Applications applications = registry.getApplications();
        Application application = applications.getRegisteredApplications("demo");
        InstanceInfo registeredInstance = application.getInstances().get(0);

        // 输出服务注册信息
        System.out.println("Registered instance: " + registeredInstance);
        
        // 关闭Eureka Server
        eurekaServer.shutdown();
    }
}

Eureka客户端缓存实现原理

Eureka客户端的缓存机制是通过DiscoveryClient类实现的。DiscoveryClient类是Eureka客户端中最核心的组件之一,它负责向Eureka Server发起查询请求,并将获取到的服务注册信息缓存到内存中,以便后续快速访问。

在DiscoveryClient中,内存缓存主要通过以下两个类实现:Applications和InstanceInfo。

Applications类是Eureka客户端缓存的服务注册表,它包含了所有已注册的服务信息。当DiscoveryClient向Eureka Server发起查询请求时,Eureka Server会返回完整的服务注册表信息,并由DiscoveryClient将其缓存到Applications中。同时,DiscoveryClient还会定时从Eureka Server获取服务注册表的增量信息,并更新Applications中的缓存。

InstanceInfo类则是服务注册信息的抽象表示,它包含了服务的基本信息,例如应用名、实例ID、主机名、IP地址、端口号等。当DiscoveryClient从Eureka Server获取服务注册信息时,会将其转换成InstanceInfo,并缓存到内存中。此外,InstanceInfo还包含了服务的健康状态、元数据信息等,这些信息也会被缓存到内存中,以便后续快速访问。

下面是一段示例代码,演示了如何使用Eureka客户端的缓存机制:

public class EurekaClientDemo {

    public static void main(String[] args) throws Exception {
        // 创建Eureka客户端实例
        DiscoveryClientConfig clientConfig = new DefaultEurekaClientConfig();
        EurekaClient eurekaClient = new DiscoveryClient(new MyInstanceConfig(), clientConfig);

        // 获取服务注册信息
        List<ServiceInstance> instances = eurekaClient.getInstancesById("demo");
        ServiceInstance instance = instances.get(0);

        // 输出服务注册信息
        System.out.println("Service instance: " + instance);

        // 关闭Eureka客户端
        eurekaClient.shutdown();
    }

    private static class MyInstanceConfig extends MyDataCenterInstanceConfig {
        @Override
        public String getAppname() {
            return "demo-client";
        }
    }
}

Eureka缓存优化策略

Eureka缓存机制的性能和可靠性对于微服务架构的稳定运行至关重要。为了提升缓存的性能和可靠性,Eureka Server采用了以下优化策略:

  1. 客户端缓存时间:Eureka客户端会将从Eureka Server获取的服务注册表信息缓存在本地内存中。为了避免本地缓存的信息过期,Eureka客户端会定时从Eureka Server获取服务注册表的增量信息,以更新本地缓存。这个定时更新的时间间隔可以通过配置项eureka.client.registryFetchIntervalSeconds来设置,默认为30秒。

  2. 服务端缓存时间:Eureka Server会将服务注册信息缓存在内存中,以便快速响应客户端的查询请求。为了避免缓存的信息过期,Eureka Server会定时清理过期的缓存信息,并从其他Eureka Server节点获取最新的服务注册信息。这个定时清理的时间间隔可以通过配置项eureka.server.responseCacheAutoExpirationInSeconds来设置,默认为180秒。

  3. 客户端缓存大小:Eureka客户端缓存的大小对于性能和可靠性都有很大的影响。如果缓存太小,会影响客户端的查询性能;如果缓存太大,会占用过多的内存资源。Eureka客户端的缓存大小可以通过配置项eureka.client.fetchRegistry=true和eureka.client.registryRefreshSingleVipAddress来设置。

  4. 服务端缓存大小:Eureka Server缓存的大小也对于性能和可靠性有很大的影响。如果缓存太小,会影响服务端的响应性能;如果缓存太大,会占用过多的内存资源。Eureka Server的缓存大小可以通过配置项eureka.server.responseCacheMaxSize来设置,默认为1000。

缓存同步

ResponseCacheImpl这个类的构造实现中,初始化了一个定时任务,这个定时任务每个

ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
    //省略...
    if (shouldUseReadOnlyResponseCache) {
        timer.schedule(getCacheUpdateTask(),
                       new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
                                + responseCacheUpdateIntervalMs),
                       responseCacheUpdateIntervalMs);
    }
}

默认每30s从readWriteCacheMap更新有差异的数据同步到readOnlyCacheMap中

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

缓存失效

在AbstractInstanceRegistry.register这个方法中,当完成服务信息保存后,会调用invalidateCache失效缓存

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    //....
     invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    //....
}

最终调用ResponseCacheImpl.invalidate方法,完成缓存的失效机制

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());

        readWriteCacheMap.invalidate(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);
            }
        }
    }
}
相关推荐
程序员三木10 分钟前
[gpt胡说八道篇] 使用Docker快速启动Doris
gpt·docker·eureka
dami_king1 小时前
新手怎么使用GitLab?
运维·git·svn·云原生·gitlab·github
阿维同学1 小时前
今天不看明天付费------中国AGI(人工智能)的发展趋势
汇编·人工智能·ai·云原生·开源·agi
字节跳动数据平台1 小时前
3个企业级最佳实践,教你ByteHouse云数仓这么用
大数据·数据库·云原生
xiaogengtongxu2 小时前
MySQL架构和性能优化
mysql·性能优化·架构
MobotStone2 小时前
架构之道:合作与分工的核心架构模式
后端·架构·设计
2401_857638034 小时前
跨越地域界限:Eureka实现跨区域服务发现全解析
java·eureka
江湖有缘4 小时前
云原生之使用Docker部署RabbitMQ消息中间件
docker·云原生·rabbitmq
研究司马懿4 小时前
【云原生】Kubernetes资源配额+HPA+节点选择器+亲和性+污点
云原生·容器·kubernetes·资源配额·污点·自动伸缩
林晞5 小时前
【NFS】【部署】NFS文件系统Server端部署,及客户端挂载
linux·云原生