SpringCloud之Eureka原理分析

1.服务注册与发现

注册表数据结构

arduino 复制代码
// <应用名,Map<实例ID,租约<实例信息>>>
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry

//租约信息
public class Lease<T> {
    //实例信息
    private T holder;
    //服务下线时间
    private long evictionTimestamp;
    //注册时间
    private long registrationTimestamp;
    //服务上线时间
    private long serviceUpTimestamp;
    //最新续约时间
    private volatile long lastUpdateTimestamp;
    private long duration;
}

//实例信息
public class InstanceInfo {
    //实例ID
    private volatile String instanceId;
    //服务名称
    private volatile String appName;
    //分组名称
    private volatile String appGroupName;
    //服务ip地址
    private volatile String ipAddr;
    //服务端口
    private volatile int port = DEFAULT_PORT;
    
    ... ...
}

注册

  1. 客户端发送一个 REST 请求到 Eureka 服务器,包含应用程序的元数据信息,如应用名称、主机地址、端口等。
  2. Eureka 服务器接收到注册请求后,给该应用程序生成一个租约信息,将租约信息、服务信息存储在注册表中。

发现

客户端应用程序会周期性地从 EurekaServer 拉取注册表信息,缓存到本地

拉取方式:

  • 全量拉取:拉取注册表数据
  • 增量拉取:增量数据维护recentlyChangedQueue队列中,定时任务每3分钟清除最新变更时间之前的数据

这两种方式都先尝试查询响应缓存(ResponseCache)

2.服务续约

一旦服务注册成功,Eureka 客户端会周期性地向 Eureka 服务器发送心跳续约请求,以表明该服务仍然处于运行状态。

  1. 客户端定期发送心跳请求,告知 Eureka 服务器该服务的存活状态。
  2. Eureka 服务器在接收到心跳请求后,更新最新续约时间。

3.服务下线

服务下线代码:

typescript 复制代码
protected boolean internalCancel(String appName, String id, boolean isReplication) {
        try {
            read.lock();
            CANCEL.increment(isReplication);
            //将服务冲注册表中移除
            Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
            Lease<InstanceInfo> leaseToCancel = null;
            if (gMap != null) {
                leaseToCancel = gMap.remove(id);
            }
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
            ... ...
            if (leaseToCancel == null) {
                CANCEL_NOT_FOUND.increment(isReplication);
                return false;
            } else {
                //租期取消
                leaseToCancel.cancel();
                ... ...
                //清除缓存
                invalidateCache(appName, vip, svip);
            }
        } finally {
            read.unlock();
        }
        ... ...
        return true;
    }

下线方式

主动下线

调用EurekaServer接口强制某个服务下线

定时剔除

EurekaServer在启动时,会开启一个EvictionTask 定时任务,定期去清理租约过期的服务

定时剔除相关配置:

yaml 复制代码
eureka:
  server:
    # 剔除过期服务的时间间隔
    eviction-interval-timer-in-ms: 1000
    # 自我保护开启
    enable-self-preservation: true
    # 自我保护-续约阈值
    renewal-percent-threshold: 0.85
定时剔除时的自我保护机制

由于网络波动导致一部分服务短时间内无法续约,保护这部分服务不会被剔除,提高Eureka服务注册中心的可用性和稳定性。

自我保护生效要同时满足:

  • enable-self-preservation = true
  • 在续约期最后一分钟内,已经续约的服务数量低于 renewal-percent-threshold 这个比例(默认为 0.85,即 85%)

4.响应缓存

Eureka Server 将请求的响应结果缓存起来。这样,当下一个相同的请求到达时,可以直接返回缓存的响应,而无需再次处理请求。这样可以大大减少网络通信和服务器资源的消耗,提高性能和可伸缩性。

缓存分类

缓存以压缩和非压缩的形式维护三类请求:

  • 所有应用
  • 增量更改
  • 单个应用

就网络流量而言,压缩后的效率比较高,尤其是在查询所有应用程序时。

go 复制代码
//单个应用缓存
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)

缓存实现

使用了两级缓存策略来处理响应。一个带有过期策略的读写缓存,以及一个不带过期的只读缓存。

一级缓存:readWriteCacheMap
  • 缓存写入:从注册表中获取
  • 缓存更新:发生注册、下线、服务状态变更时会清除缓存
二级缓存:readOnlyCacheMap

从一级缓存中同步

代码实现:
ini 复制代码
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        this.registry = registry;

        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
        
        //LoadingCache 是基于 Guava 缓存库的一种缓存框架
        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);
        }
    }
二级缓存配置
yaml 复制代码
    # 是否开启readOnly读缓存
    use-read-only-response-cache: true
    # readOnly 刷新时间间隔,默认为30秒
    response-cache-update-interval-ms: 1000

5.集群同步

当服务有以下操作时会进行集群间数据同步:

  • Cancel:取消(服务下线)
  • Heartbeat:服务心跳
  • Register:服务注册
  • StatusUpdate:服务状态变更
  • DeleteStatusOverride:删除缓存状态
typescript 复制代码
private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this will create a poison replication
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }
    
相关推荐
IT_陈寒4 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码5 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo5 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志6 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..6 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects6 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰6 小时前
一些基础和问题解决
后端
陈随易7 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN7 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana7 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端