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();
        }
    }
    
相关推荐
千叶寻-1 天前
正则表达式
前端·javascript·后端·架构·正则表达式·node.js
小咕聊编程1 天前
【含文档+源码】基于SpringBoot的过滤协同算法之网上服装商城设计与实现
java·spring boot·后端
追逐时光者1 天前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_1 天前
敏捷开发流程-精简版
前端·后端
苏打水com1 天前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧1 天前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 天前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 天前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 天前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧1 天前
Spring Cloud Gateway详解与应用实战
后端