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();
        }
    }
    
相关推荐
2401_857622662 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589362 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没4 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch4 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码5 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries6 小时前
读《show your work》的一点感悟
后端
A尘埃6 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23076 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code6 小时前
(Django)初步使用
后端·python·django
代码之光_19806 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端