eureka 源码阅读

Eureka 是什么?要解决什么问题?

Eureka 是 REST 风格的服务,主要是用于定位服务,目的是解决中间层服务的负载均衡故障转移

Eureka 整体结构

eureka_architecture.png

角色划分

  • Eureka Server: Eureka 服务端

  • Eureka Client:Eureka 客户端

    • Application Service: 服务生产者

    • Application Client:服务消费者

功能描述

服务注册 Register:Client 端主动向 Server 端注册。提供自身的信息,比如 IP 地址、端口,主页…等

心跳续约 Renew:Client 默认会每隔30秒发送一次心跳到 Server 来完成续约。这样来告诉Server说我还活着

主动下线 Cancel:Client 端在停机时(正常停止)主动向 Server 发送下线,告诉 Server 把自己剔除

获取服务列表 Get Registry:Client 从 Server 获取注册表,并将其缓存在本地。缓存信息默认每 30s 更新一次。

注册表同步 Replicate:多个 Server 之间通过 P2P 复制的方式完成服务注册表的同步。

服务剔除 Eviction:在默认的情况下,当 Client 连续 90 秒没有向 Server 发送服务续约,Server 会将该 Client 从服务注册列表删除。

Eureka 核心概念

实例 Instance: 对应类:InstanceInfo,InstanceInfo 代表一个实例的信息,趋近 POJO。

应用 Application: 对应类:Application,同名 InstanceInfo 集合

注册表 Applications: 对应类:Applications,Application 集合

Eureka 组件

定时任务组件 TimedSupervisorTask

TimedSupervisorTask 是一个固定间隔的周期性任务(继承了Runable接口),一旦遇到超时,下一次执行的间隔时间就会翻倍,如果继续超时,继续翻倍,直到达到设置的上限(达到上限之后,以上限作为固定时间间隔),当任务正常执行之后,时间间隔又会还原成初始值

public class TimedSupervisorTask extends TimerTask {

    private final String name;
    private final ScheduledExecutorService scheduler; // 延迟执行 this 线程池
    private final ThreadPoolExecutor executor; // task执行线程池
    private final long timeoutMillis; // 默认超时时间 也是默认延迟时间
    private final Runnable task; // 具体task
    private final AtomicLong delay; // 延迟时间
    private final long maxDelay; // 最大延迟时间

    @Override
    public void run() {
        Future future = null;
        try {
            future = executor.submit(task);
            future.get(timeoutMillis, TimeUnit.MILLISECONDS);
            delay.set(timeoutMillis);
        } catch (TimeoutException e) {
            long currentDelay = delay.get();
            long newDelay = Math.min(maxDelay, currentDelay * 2);
            delay.compareAndSet(currentDelay, newDelay);
        } catch (RejectedExecutionException e) { // 略
        } catch (Throwable e) { // 略
        } finally {
            if (future != null) { future.cancel(true); }
            if (!scheduler.isShutdown()) {
                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
            }
        }
    }
}
image-20211006151251101.png

例如起始间隔是2秒,上限是15秒,效果如下

image-20211006153716010.png

集群解析器 ClusterResolver

// 接口定义, 可以获取 EurekaEndpoint 列表,即服务接口地址
public interface ClusterResolver {
    String getRegion();
    List getClusterEndpoints();
}

EurekaEndpoint 可以理解成 Server 接口地址的描述


image-20211006161408857.png

基于配置的集群解析器 ConfigClusterResolver

ConfigClusterResolver 基于配置service-url,实现获取 EurekaEndpoint 列表

@Override
public List getClusterEndpoints() {
    if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
        return getClusterEndpointsFromDns();
    } else {
        return getClusterEndpointsFromConfig();
    }
}
private List getClusterEndpointsFromConfig() {
    String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
    String myZone = InstanceInfo.getZone(availZones, myInstanceInfo);

    Map> serviceUrls = EndpointUtils
            .getServiceUrlsMapFromConfig(clientConfig, myZone, clientConfig.shouldPreferSameZoneEureka());

    List endpoints = new ArrayList<>();
    for (String zone : serviceUrls.keySet()) {
        for (String url : serviceUrls.get(zone)) {
            try {
                endpoints.add(new AwsEndpoint(url, getRegion(), zone));
            } catch (Exception ignore) {
                logger.warn("Invalid eureka server URI: {}; removing from the server pool", url);
            }
        }
    }
}

Zone亲和的集群解析器 ZoneAffinityClusterResolver

ZoneAffinityClusterResolver 持有 ConfigClusterResolver 对象,是静态代理,对 ConfigClusterResolver 返回的 EurekaEndpoint 做了随机排序。

public List getClusterEndpoints() {

    // 获取当前zone和其它zone的Endpoint
    List[] parts = ResolverUtils.splitByZone(delegate.getClusterEndpoints(), myZone);
    List myZoneEndpoints = parts[0];
    List remainingEndpoints = parts[1];
    List randomizedList = randomizeAndMerge(myZoneEndpoints, remainingEndpoints);
    
    // 如果非zone亲和,倒序排列
    if (!zoneAffinity) {
        Collections.reverse(randomizedList);
    }
    return randomizedList;
}

// 将Endpoint随机排列并合并,合并策略是将当前zone的Endpoint放在其它zone的Endpoint前面
private List randomizeAndMerge(List myZoneEndpoints, List remainingEndpoints) {
    if (myZoneEndpoints.isEmpty()) {
        return randomizer.randomize(remainingEndpoints);
    }
    if (remainingEndpoints.isEmpty()) {
        return randomizer.randomize(myZoneEndpoints);
    }
    List mergedList = randomizer.randomize(myZoneEndpoints);
    mergedList.addAll(randomizer.randomize(remainingEndpoints));
    return mergedList;
}

异步解析器 AsyncResolver

异步使用定时任务组件 TimedSupervisorTask 实现

具体解析过程由委托对象 ZoneAffinityClusterResolver 完成

public class AsyncResolver implements ClosableResolver {
    private static final Logger logger = LoggerFactory.getLogger(AsyncResolver.class);

    private final AtomicBoolean warmedUp = new AtomicBoolean(false);
    private final AtomicBoolean scheduled = new AtomicBoolean(false);
    private final String name;    // a name for metric purposes
    private final ClusterResolver delegate;  // 上文提到 ZoneAffinityClusterResolver
    private final ScheduledExecutorService executorService;
    private final ThreadPoolExecutor threadPoolExecutor;
    private final TimedSupervisorTask backgroundTask; // 上文提到 TimedSupervisorTask
    private final AtomicReference> resultsRef; // 缓存
    private final int refreshIntervalMs;
    private final int warmUpTimeoutMs;
    private volatile long lastLoadTimestamp = -1;

    // 构造方法,初始化各种线程池
    AsyncResolver(String name,
                  ClusterResolver delegate,
                  List initialValue,
                  int executorThreadPoolSize,
                  int refreshIntervalMs,
                  int warmUpTimeoutMs) {
        this.name = name;
        this.delegate = delegate;
        this.refreshIntervalMs = refreshIntervalMs;
        this.warmUpTimeoutMs = warmUpTimeoutMs;

        this.executorService = Executors.newScheduledThreadPool(1,
                new ThreadFactoryBuilder()
                        .setNameFormat("AsyncResolver-" + name + "-%d")
                        .setDaemon(true)
                        .build());

        this.threadPoolExecutor = new ThreadPoolExecutor(
                1, executorThreadPoolSize, 0, TimeUnit.SECONDS,
                new SynchronousQueue(),  // use direct handoff
                new ThreadFactoryBuilder()
                        .setNameFormat("AsyncResolver-" + name + "-executor-%d")
                        .setDaemon(true)
                        .build()
        );

        this.backgroundTask = new TimedSupervisorTask(
                this.getClass().getSimpleName(),
                executorService,
                threadPoolExecutor,
                refreshIntervalMs,
                TimeUnit.MILLISECONDS,
                5,
                updateTask
        );

        this.resultsRef = new AtomicReference<>(initialValue);
        Monitors.registerObject(name, this);
    }

    @Override
    public List getClusterEndpoints() {
        long delay = refreshIntervalMs;
        // 如果未预热,开始预热,并标记已预热
        if (warmedUp.compareAndSet(false, true)) {
            if (!doWarmUp()) {
                delay = 0;
            }
        }
        
        // 如果未开始过定时任务,开始定时任务并做标记已开始
        if (scheduled.compareAndSet(false, true)) {
            scheduleTask(delay);
        }
        return resultsRef.get();
    }
    
    void scheduleTask(long delay) {
        executorService.schedule(backgroundTask, delay, TimeUnit.MILLISECONDS);
    }

    // 具体任务,获取 EurekaEndpoint 
    private final Runnable updateTask = new Runnable() {
        @Override
        public void run() {
            try {
                // ZoneAffinityClusterResolver 获取 EurekaEndpoint 列表
                List newList = delegate.getClusterEndpoints();
                if (newList != null) {
                    resultsRef.getAndSet(newList);
                    lastLoadTimestamp = System.currentTimeMillis();
                } else {
                    logger.warn("Delegate returned null list of cluster endpoints");
                }
                logger.debug("Resolved to {}", newList);
            } catch (Exception e) {
                logger.warn("Failed to retrieve cluster endpoints from the delegate", e);
            }
        }
    };
}
image-20211105151013103.png

优秀的代理使用

通信

底层通信接口 EurekaHttpClient


image-20211006163632598.png

image-20211006163928620.png

EurekaHttpClient 系统的实现图 如上,左边是 Eureka 自身使用 Jersey 实现的Rest风格, 右边是 Spring Cloud 使用 RestTemplate 实现 Rest 风格

http 访问实际是通过 apache http client 实现,此处忽略。

右边这一块通过命名可以看出使用的是一个装饰器模式。

SessionedEurekaHttpClient:实现重连接,隔一段时间会重新断开连接,并重新建立,实现客户端负载均衡

RetryableEurekaHttpClient: 重试一定次数,并有对 server 的黑名单机制

RedirectingEurekaHttpClient:实现重定向

RestTemplateTransportClient: 基于RestTemplate的 Rest 风格的访问

SessionedEurekaHttpClient 核心代码

@Override
protected  EurekaHttpResponse execute(RequestExecutor requestExecutor) {
    long now = System.currentTimeMillis();
    long delay = now - lastReconnectTimeStamp;
    // 当 (now - lastReconnectTimeStamp)大于 currentSessionDurationMs 时,
    // 端开连接并将eurekaHttpClientRef置null
    // currentSessionDurationMs 取值范围:sessionDurationMs+-[0, sessionDurationMs/2]
    if (delay >= currentSessionDurationMs) {
        logger.debug("Ending a session and starting anew");
        lastReconnectTimeStamp = now;
        currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
        TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
    }

    EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
    if (eurekaHttpClient == null) {
        eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
    }
    return requestExecutor.execute(eurekaHttpClient);
}

RetryableEurekaHttpClient 核心代码

private List getHostCandidates() {
    // 黑名单: quarantineSet 候选名单:candidateHosts 
    // 候选名单顺序是随机的 clusterResolver 为 ZoneAffinityClusterResolver
    // 黑名单对候选名单求交集,确保黑名单元素都在候选名单中
    List candidateHosts = clusterResolver.getClusterEndpoints();
    quarantineSet.retainAll(candidateHosts);

    // 算出阈值, 默认阈值比例 0.66
    // 如果计算结果过大,则为candidateHosts.size
    int threshold = (int) (candidateHosts.size() * transportConfig.getRetryableClientQuarantineRefreshPercentage());
    if (threshold > candidateHosts.size()) {
        threshold = candidateHosts.size();
    }
    
    // 如黑名单多余阈值,则将黑名单清空,即如Server损坏太多,清空黑名单,这就是黑名单恢复机制
    // 如没有达到阈值,正常过滤
    if (quarantineSet.isEmpty()) {
        // no-op
    } else if (quarantineSet.size() >= threshold) {
        logger.debug("Clearing quarantined list of size {}", quarantineSet.size());
        quarantineSet.clear();
    } else {
        List remainingHosts = new ArrayList<>(candidateHosts.size());
        for (EurekaEndpoint endpoint : candidateHosts) {
            if (!quarantineSet.contains(endpoint)) {
                remainingHosts.add(endpoint);
            }
        }
        candidateHosts = remainingHosts;
    }

    return candidateHosts;
}

protected  EurekaHttpResponse execute(RequestExecutor requestExecutor) {
    List candidateHosts = null;
    // 按顺序连接 Server
    int endpointIdx = 0;
    // 重试次数限制 numberOfRetries 默认3
    for (int retry = 0; retry < numberOfRetries; retry++) {
        // 如果缓存的  currentHttpClient 不存在, 则通过候选名单建立新连接
        EurekaHttpClient currentHttpClient = delegate.get();
        EurekaEndpoint currentEndpoint = null;
        if (currentHttpClient == null) {
            if (candidateHosts == null) {
                candidateHosts = getHostCandidates();
                if (candidateHosts.isEmpty()) {
                    throw new TransportException("cluster server list is empty");
                }
            }
            if (endpointIdx >= candidateHosts.size()) {
                throw new TransportException("Cannot execute request on any known server");
            }
            // 首次 或者 上一次请求失败 会重新复制endpointIdx,连接候选列表中的下个实例
            currentEndpoint = candidateHosts.get(endpointIdx++);
            currentHttpClient = clientFactory.newClient(currentEndpoint);
        }

        try {
            EurekaHttpResponse response = requestExecutor.execute(currentHttpClient);
            // 访问成功, 返回结束,否则会继续下次循环,并将当前server放入黑名单
            if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
                delegate.set(currentHttpClient);
                return response;
            }
        } catch (Exception e) {
            logger.warn("Request execution failed with message: {}", e.getMessage());
        }
        delegate.compareAndSet(currentHttpClient, null);
        if (currentEndpoint != null) {
            quarantineSet.add(currentEndpoint);
        }
    }
    // 如果重试次数达到限制仍然失败,抛出异常
    throw new TransportException("Retry limit reached; giving up on completing the request");
}

DiscoveryClient 静态内部类 EurekaTransport 集合了所有访问 Server 的路径。

初始化代码在 DiscoveryClient # scheduleServerEndpointTask, 初始化结果是

结果是

private static final class EurekaTransport {
    private ClosableResolver bootstrapResolver;  // 即异步解析器 AsyncResolver 
    // 工厂类RestTemplateTransportClientFactory
    private TransportClientFactory transportClientFactory; 

    // 注册使用的EurekaHttpClient 与对应工厂, 实际为 SessionedEurekaHttpClient
    private EurekaHttpClient registrationClient;
    private EurekaHttpClientFactory registrationClientFactory;

    // 同上
    private EurekaHttpClient queryClient;
    private EurekaHttpClientFactory queryClientFactory;
}

最终由EurekaHttpClients#canonicalClientFactory 实现,可以看到此处返回的是 SessionedEurekaHttpClient SessionedEurekaHttpClient 持有 RetryableEurekaHttpClientFactory,

RetryableEurekaHttpClientFactory 持有 RedirectingEurekaHttpClientFactory,

RedirectingEurekaHttpClientFactory持有 TransportClientFactory(RestTemplateTransportClientFactory)

static EurekaHttpClientFactory canonicalClientFactory(final String name,
                                                      final EurekaTransportConfig transportConfig,
                                                      final ClusterResolver clusterResolver,
                                                      final TransportClientFactory transportClientFactory) {

    return new EurekaHttpClientFactory() {
        @Override
        public EurekaHttpClient newClient() {
            return new SessionedEurekaHttpClient(
                    name,
                    RetryableEurekaHttpClient.createFactory(
                            name,
                            transportConfig,
                            clusterResolver,                            RedirectingEurekaHttpClient.createFactory(transportClientFactory),
                            ServerStatusEvaluators.legacyEvaluator()),
                    transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
            );
        }

        @Override
        public void shutdown() {
            wrapClosable(clusterResolver).shutdown();
        }
    };
}

Eureka Instance

InstanceInfo 描述实例本身, 如instanceId, 应用名称,host、租约信息等。

public class InstanceInfo {
    private volatile String instanceId; // 唯一
    private volatile String appName;
    private volatile String appGroupName;
    private volatile String ipAddr;
    private volatile String sid = SID_DEFAULT;
    private volatile int port = DEFAULT_PORT;
    private volatile int securePort = DEFAULT_SECURE_PORT;
    private volatile String homePageUrl;
    private volatile String statusPageUrl;
    private volatile String healthCheckUrl;
    private volatile String secureHealthCheckUrl;
    private volatile String vipAddress;
    private volatile String secureVipAddress;
    private String statusPageRelativeUrl;
    private String statusPageExplicitUrl;
    private String healthCheckRelativeUrl;
    private String healthCheckSecureExplicitUrl;
    private String vipAddressUnresolved;
    private String secureVipAddressUnresolved;
    private String healthCheckExplicitUrl;
    private volatile int countryId = DEFAULT_COUNTRY_ID; // Defaults to US
    private volatile boolean isSecurePortEnabled = false;
    private volatile boolean isUnsecurePortEnabled = true;
    private volatile DataCenterInfo dataCenterInfo;
    private volatile String hostName;
    // starting 初始化、down健康检查失败、up正常服务、out_of_service 正常服务但是不接受请求 unkown 未知状态
    private volatile InstanceStatus status = InstanceStatus.UP;
    // 覆盖状态
    private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;
    // 标注实例是否脏(client对比server) true表示需要向服务端注册
    private volatile boolean isInstanceInfoDirty = false;
    // 租约信息
    private volatile LeaseInfo leaseInfo;
    private volatile Boolean isCoordinatingDiscoveryServer = Boolean.FALSE;
    // 元数据
    private volatile Map metadata;
    private volatile Long lastUpdatedTimestamp;
    private volatile Long lastDirtyTimestamp;
    private volatile ActionType actionType;
    private volatile String asgName;
    private String version = VERSION_UNKNOWN;
}
public class InstanceInfoFactory {
   public InstanceInfo create(EurekaInstanceConfig config) {
   。。。
   }
}

Eureka Client 实现

核心逻辑DiscoveryClient

// 初始化所有定时任务
private void initScheduledTasks() {
    // 拉取注册表
    if (clientConfig.shouldFetchRegistry()) {
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        // TimedSupervisorTask 定时任务, task 是 CacheRefreshThread
        cacheRefreshTask = new TimedSupervisorTask(
                "cacheRefresh",
                scheduler,
                cacheRefreshExecutor,
                registryFetchIntervalSeconds,
                TimeUnit.SECONDS,
                expBackOffBound,
                new CacheRefreshThread()
        );
        // scheduler 触发延时 cacheRefreshTask
        scheduler.schedule(
                cacheRefreshTask,
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

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

        // TimedSupervisorTask 定时任务, task 是 HeartbeatThread
        heartbeatTask = new TimedSupervisorTask(
                "heartbeat",
                scheduler,
                heartbeatExecutor,
                renewalIntervalInSecs,
                TimeUnit.SECONDS,
                expBackOffBound,
                new HeartbeatThread()
        );
        // scheduler 触发延时 cacheRefreshTask
        scheduler.schedule(
                heartbeatTask,
                renewalIntervalInSecs, TimeUnit.SECONDS);

        // 服务复制器
        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) {
                instanceInfoReplicator.onDemandUpdate();
            }
        };
        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }
        // 服务复制开始
        instanceInfoReplicator.start(
        clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

缓存刷新 CacheRefreshThread

定时拉取增量数据更新本地缓存

心跳任务 HeartbeatThread

定时发送续约

服务复制器 InstanceInfoReplicator

当dirty时,重新注册

状态变更监听器 ApplicationInfoManager.StatusChangeListener

执行一次 InstanceInfoReplicator#run

Eureka Server 实现

Eureka 可视化仪表盘 EurekaController

Spring Cloud 提供了可视化的页面,可以通过 EurekaDashboardProperties 配置。

image-20211007155132174.png

资源层

使用Jersey实现的Restful web 服务,类似 @RestController


image-20211013212732106.png

ApplicationsResource

ApplicationResource

InstanceResource

InstancesResource

PeerReplicationResource

响应缓存实现-二级缓存 ResponseCacheImpl

逻辑图

一级缓存 只读缓存 ConcurrentMap:ConcurrentMap readOnlyCacheMap

二级缓存 读写缓存 guaua:LoadingCache readWriteCacheMap


image-20211013212144819.png

先看构造代码

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)
    // key 删除监听,将 Key 从 regionSpecificKeys 移除
    .removalListener(new RemovalListener() {
        @Override
        public void onRemoval(RemovalNotification notification) {
            Key removedKey = notification.getKey();
            if (removedKey.hasRegions()) {
                Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
            }
        }
    })
    // CacheLoader 缓存加载
    // Key 放入 regionSpecificKeys
    // 从registry 获取 Value 放入缓存
    .build(new CacheLoader() {
        @Override
        public Value load(Key key) throws Exception {
            if (key.hasRegions()) {
                Key cloneWithNoRegions = key.cloneWithoutRegions();
                regionSpecificKeys.put(cloneWithNoRegions, key);
            }
            // 从注册表获取Value缓存
            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);
    }
}

再看获取缓存方法

Value getValue(final Key key, boolean useReadOnlyCache) {
    Value payload = null;
    try {
        // 如果使用了只读缓存 readOnlyCacheMap,则优先从只读缓存读取,如果不存在,在从读写缓存读取,并更新只读缓存
        // 如果不使用只读缓存, 则直接从 读写缓存中读取
        if (useReadOnlyCache) {
            final Value currentPayload = readOnlyCacheMap.get(key);
            if (currentPayload != null) {
                payload = currentPayload;
            } else {
                payload = readWriteCacheMap.get(key);
                readOnlyCacheMap.put(key, payload);
            }
        } else {
            payload = readWriteCacheMap.get(key);
        }
    } catch (Throwable t) {
        logger.error("Cannot get value for key : {}", key, t);
    }
    return payload;
}

注册表实现

image-20211014103612323.png

LookupService 服务发现

LeaseManager 租约行为

AbstractInstanceRegistry 接口核心逻辑实现

PeerAwareInstanceRegistryImpl 同步复制

InstanceRegistry 发布 Spring 事件

public interface LookupService {
    Application getApplication(String appName);
    Applications getApplications();
    List getInstancesById(String id);
    InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
public interface LeaseManager {
    void register(T r, int leaseDuration, boolean isReplication);
    boolean cancel(String appName, String id, boolean isReplication);
    boolean renew(String appName, String id, boolean isReplication);
    void evict();
}

注册表存储registry 本质上是一个双层的 ConcurrentHashMap,存储在内存中的。AbstractInstanceRegistry#registry

private final ConcurrentHashMap>> registry
        = new ConcurrentHashMap>>();
  • 第一层的 key 是appName,value 是第二层 ConcurrentHashMap;
  • 第二层 ConcurrentHashMap 的 key 是服务的 InstanceId,value 是 Lease 对象,Lease 对象包含了实例详情和服务治理相关的属性。

服务注册 register

image-20211105164240817.png
InstanceRegistry
public void register(InstanceInfo info, boolean isReplication) {
    // 发送事件 EurekaInstanceRegisteredEvent
    handleRegistration(info, leaseDuration, isReplication);
    super.register(info, leaseDuration, isReplication);
}

PeerAwareInstanceRegistryImpl
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    super.register(info, leaseDuration, isReplication);
    // 复制同步到其它节点
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

AbstractInstanceRegistry
// registrant 注册对象、leaseDuration 租约时间、isReplication 是否来自eureka-server复制
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    // 获取读锁,即读取操作不受阻塞,写操作会阻塞。
    read.lock();
    try {
        //gMap是一个CurrentHashMap, 注册表获取应用所有租约信息
        Map> gMap = registry.get(registrant.getAppName());
        REGISTER.increment(isReplication);
        // 如果 gMap 为null, 则新建gNewMap放入注册表registry,并赋值给 gMap
        if (gMap == null) {
            final ConcurrentHashMap> gNewMap = new ConcurrentHashMap>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        // 获取对应实例的租约信息, 
        // 如果存在且已存在的lastdirtytime > 待注册的实例lastdirtytime, 使用已存在的实例
        // 如果不存在,认为是新注册实例,
        // expectedNumberOfClientsSendingRenews+1并更新numberOfRenewsPerMinThreshold
        Lease existingLease = gMap.get(registrant.getId());
        if (existingLease != null && (existingLease.getHolder() != null)) {
            Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
            Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
            logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
            if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                registrant = existingLease.getHolder();
            }
        } else {
            synchronized (lock) {
                if (this.expectedNumberOfClientsSendingRenews > 0) {
                    this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                    updateRenewsPerMinThreshold();
                }
            }
        }
        // 用注册对象,构建Lease,并放入gMap
        Lease lease = new Lease<>(registrant, leaseDuration);
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        gMap.put(registrant.getId(), lease);
        // This is where the initial state transfer of overridden status happens
        // 如果覆盖状态不是 UNKNOWN,则需要处理覆盖状态
        if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
            // 覆盖状态overriddenInstanceStatusMap不包含注册实例,则添加
            if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
            }
        }
        // 如果 注册实例存在覆盖状态,则注册实例使用覆盖状态替换自身的覆盖状态
        InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
        if (overriddenStatusFromMap != null) {
            registrant.setOverriddenStatus(overriddenStatusFromMap);
        }

        // 根据覆盖状态规则,获取状态,并无脏更新状态
        InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
        registrant.setStatusWithoutDirty(overriddenInstanceStatus);

        // If the lease is registered with UP status, set lease service up timestamp
        // 如果注册实例状态为UP, 更新lease serviceUpTimestamp
        if (InstanceStatus.UP.equals(registrant.getStatus())) {
            lease.serviceUp();
        }
        registrant.setActionType(ActionType.ADDED);
        // 将lease 封装成最近变更对象 RecentlyChangedItem,放入最近更新队列
        recentlyChangedQueue.add(new RecentlyChangedItem(lease));
        registrant.setLastUpdatedTimestamp();
        // 使 二级缓存中的读写缓存失效
        invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    } finally {
        read.unlock();
    }
}

服务续约 renew

image-20211017210930371.png
InstanceRegistry
public boolean renew(final String appName, final String serverId, boolean isReplication) {
   // 获取字典排序的注册表
   // 遍历注册表 找到对应实例,并发布事件 EurekaInstanceRenewedEvent
   List applications = getSortedApplications();
   for (Application input : applications) {
      if (input.getName().equals(appName)) {
         InstanceInfo instance = null;
         for (InstanceInfo info : input.getInstances()) {
            if (info.getId().equals(serverId)) {
               instance = info;
               break;
            }
         }
         publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId, instance, isReplication));
         break;
      }
   }
   return super.renew(appName, serverId, isReplication);
}

PeerAwareInstanceRegistryImpl
public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
        // 续约动作成功,则复制同步到其它eureka-server节点
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

AbstractInstanceRegistry
public boolean renew(String appName, String id, boolean isReplication) {
    RENEW.increment(isReplication);
    Map> gMap = registry.get(appName);
    Lease leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
    // 如果注册表中 renew 对象的Lease 为空,则返回false
    // 如果 Lease 非空, 则继续 renew 动作
    if (leaseToRenew == null) {
        RENEW_NOT_FOUND.increment(isReplication);
        return false;
    } else {
        // 获取 renew 对应的实例
        // 
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // 获取覆盖状态,如果覆盖状态为UNKNOWN, 返回false
            // 如果覆盖状态非UNKNOWN,则更新实例状态为覆盖状态
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

            }
        }
        renewsLastMin.increment();
        // 续约动作 lastUpdateTimestamp = System.currentTimeMillis() + duration
        leaseToRenew.renew();
        return true;
    }
}

服务下线 cancel

image-20211017212221678.png
InstanceRegistry
public boolean cancel(String appName, String serverId, boolean isReplication) {
   // 发送事件 EurekaInstanceCanceledEvent
   handleCancelation(appName, serverId, isReplication);
   return super.cancel(appName, serverId, isReplication);
}

PeerAwareInstanceRegistryImpl
public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
    if (super.cancel(appName, id, isReplication)) {
        // 成功撤销服务,则复制同步其它eureka-server
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

AbstractInstanceRegistry
protected boolean internalCancel(String appName, String id, boolean isReplication) {
    // 获取读锁,即读取操作不受阻塞,写操作会阻塞。
    read.lock();
    try {
        CANCEL.increment(isReplication);
        // 从注册表找到 cancel 的Lease,并从注册表删除
        Map> gMap = registry.get(appName);
        Lease leaseToCancel = null;
        if (gMap != null) {
            leaseToCancel = gMap.remove(id);
        }
        recentCanceledQueue.add(new Pair(System.currentTimeMillis(), appName + "(" + id + ")"));
        // 从overriddenInstanceStatusMap删除cancel的id
        InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
        if (instanceStatus != null) {

        }
        if (leaseToCancel == null) {
            CANCEL_NOT_FOUND.increment(isReplication);
            return false;
        } else {
            // 下线动作 evictionTimestamp = System.currentTimeMillis()
            // 将下线的lease封装成RecentlyChangedItem,放入最近变更队列
            leaseToCancel.cancel();
            InstanceInfo instanceInfo = leaseToCancel.getHolder();
            String vip = null;
            String svip = null;
            if (instanceInfo != null) {
                instanceInfo.setActionType(ActionType.DELETED);
                recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                instanceInfo.setLastUpdatedTimestamp();
                vip = instanceInfo.getVIPAddress();
                svip = instanceInfo.getSecureVipAddress();
            }
            // 使 二级缓存中的读写缓存失效
            invalidateCache(appName, vip, svip);
        }
    } finally {
        read.unlock();
    }
    // 下线之后,
    // expectedNumberOfClientsSendingRenews-1并更新numberOfRenewsPerMinThreshold
    synchronized (lock) {
        if (this.expectedNumberOfClientsSendingRenews > 0) {
            this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
            updateRenewsPerMinThreshold();
        }
    }
    return true;
}

增量获取

最近变更队列 ConcurrentLinkedQueue, 由定时任务更新,定时任务遍历增量队列找到保留时间达到限制的元素删除。

private ConcurrentLinkedQueue recentlyChangedQueue = new ConcurrentLinkedQueue<>();
// 构造代码
this.deltaRetentionTimer.schedule(getDeltaRetentionTask(),
                serverConfig.getDeltaRetentionTimerIntervalInMs(),
                serverConfig.getDeltaRetentionTimerIntervalInMs())

private TimerTask getDeltaRetentionTask() {
    return new TimerTask() {

        @Override
        public void run() {
            Iterator it = recentlyChangedQueue.iterator();
            while (it.hasNext()) {
                // 如果最近更新时间 < 当前时间 - 增量保留时间配置,删除
                if (it.next().getLastUpdateTime() <
                        System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
                    it.remove();
                } else {
                    break;
                }
            }
        }
    };
}

获取增量, 遍历增量队列,将增量队列中实例添加到返回值中

public Applications getApplicationDeltasFromMultipleRegions(String[] remoteRegions) {
    if (null == remoteRegions) {
        remoteRegions = allKnownRemoteRegions; // null means all remote regions.
    }

    boolean includeRemoteRegion = remoteRegions.length != 0;

    if (includeRemoteRegion) {
        GET_ALL_WITH_REMOTE_REGIONS_CACHE_MISS_DELTA.increment();
    } else {
        GET_ALL_CACHE_MISS_DELTA.increment();
    }

    Applications apps = new Applications();
    apps.setVersion(responseCache.getVersionDeltaWithRegions().get());
    Map applicationInstancesMap = new HashMap();
    
    // 获取写锁,禁止其它线程读写
    write.lock();
    try {
        // 遍历增量队列,获取实例信息放入返回结果
        Iterator iter = this.recentlyChangedQueue.iterator();
        while (iter.hasNext()) {
            Lease lease = iter.next().getLeaseInfo();
            InstanceInfo instanceInfo = lease.getHolder();
            Application app = applicationInstancesMap.get(instanceInfo.getAppName());
            if (app == null) {
                app = new Application(instanceInfo.getAppName());
                applicationInstancesMap.put(instanceInfo.getAppName(), app);
                apps.addApplication(app);
            }
            app.addInstance(new InstanceInfo(decorateInstanceInfo(lease)));
        }
        // 获取远程数据
        if (includeRemoteRegion) {
            for (String remoteRegion : remoteRegions) {
                RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);
                if (null != remoteRegistry) {
                    Applications remoteAppsDelta = remoteRegistry.getApplicationDeltas();
                    if (null != remoteAppsDelta) {
                        for (Application application : remoteAppsDelta.getRegisteredApplications()) {
                            if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {
                                Application appInstanceTillNow =
                                        apps.getRegisteredApplications(application.getName());
                                if (appInstanceTillNow == null) {
                                    appInstanceTillNow = new Application(application.getName());
                                    apps.addApplication(appInstanceTillNow);
                                }
                                for (InstanceInfo instanceInfo : application.getInstances()) {
                                    appInstanceTillNow.addInstance(new InstanceInfo(instanceInfo));
                                }
                            }
                        }
                    }
                }
            }
        }

        Applications allApps = getApplicationsFromMultipleRegions(remoteRegions);
        apps.setAppsHashCode(allApps.getReconcileHashCode());
        return apps;
    } finally {
        write.unlock();
    }
}

服务剔除 evict

image-20211017212359449.png
class EvictionTask extends TimerTask {
    // 上次执行事件时间
    private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
    @Override
    public void run() {
        try {
            long compensationTimeMs = getCompensationTimeMs();
            evict(compensationTimeMs);
        } catch (Throwable e) {
        }
    }

    // 计算补偿时间(额外补偿的租约时间) (System.nanoTime() - lastExecutionNanos - evictionIntervalTimerInMs)
    // 如果补偿时间小于0,设置为0
    // 如: 上次执行时间使10s, 当前时间(这次执行时间)是80s, 服务剔除周期是60s, 则补偿时间是(80-10-60) = 10s
    long getCompensationTimeMs() {
        long currNanos = getCurrentTimeNano();
        long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
        if (lastNanos == 0l) {
            return 0l;
        }

        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
        long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
        return compensationTime <= 0l ? 0l : compensationTime;
    }
    long getCurrentTimeNano() {
        return System.nanoTime();
    }

}

public void evict(long additionalLeaseMs) {
    // !(shouldEnableSelfPreservation 配置不开启自我保护或最近renew次数大于自我保护的阈值)
    if (!isLeaseExpirationEnabled()) {
        return;
    }

    // 遍历注册表所有Lease,将满足剔除条件的Lease放入expiredLeases中
    List> expiredLeases = new ArrayList<>();
    for (Entry>> groupEntry : registry.entrySet()) {
        Map> leaseMap = groupEntry.getValue();
        if (leaseMap != null) {
            for (Entry> leaseEntry : leaseMap.entrySet()) {
                Lease lease = leaseEntry.getValue();
                // (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs))
                // 实际过期时间应该是 两倍的 duration,这是一个已知的官方bug,
                // 但是因为只影响不雅下线的服务,所以没有修复
                if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                    expiredLeases.add(lease);
                }
            }
        }
    }

    // 计算最大剔除个数:注册表个数 - 注册表个数 * 自我保护阈值比例
    int registrySize = (int) getLocalRegistrySize();
    int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
    int evictionLimit = registrySize - registrySizeThreshold;

    // 本次执行剔除个数:最大剔除个数和符合剔除条件Lease个数较小值
    int toEvict = Math.min(expiredLeases.size(), evictionLimit);
    if (toEvict > 0) {
        // 随机剔除符合剔除条件Lease列表中的toEvict个
        Random random = new Random(System.currentTimeMillis());
        for (int i = 0; i < toEvict; i++) {
            int next = i + random.nextInt(expiredLeases.size() - i);
            Collections.swap(expiredLeases, i, next);
            Lease lease = expiredLeases.get(i);
            String appName = lease.getHolder().getAppName();
            String id = lease.getHolder().getId();
            EXPIRED.increment();
            internalCancel(appName, id, false);
        }
    }
}

同步复制

image-20211108194254508.png

上文提到 PeerAwareInstanceRegistryImpl 在服务注册、服务续约和服务下线时,都会通过 replicateToPeers() 方法将数据同步到 eureka-server 的其它节点,看下代码。

private void replicateToPeers(Action action, String appName, String id,
                              InstanceInfo info /* optional */,
                              InstanceStatus newStatus /* optional */, boolean isReplication) {
    Stopwatch tracer = action.getTimer().start();
    try {
        // 来自其它server的同步复制
        if (isReplication) {
            numberOfReplicationsLastMin.increment();
        }
        // 如果来自其它server的同步复制或者对等server节点列表为空 不需要同步复制
        if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
            return;
        }
        // 遍历对等server节点列表,非当前节点,同步复制数据
        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();
    }
}
// 通过PeerEurekaNode根据不同的Action 发出不同的同步程序
private void replicateInstanceActionsToPeers(Action action, String appName,
                                             String id, InstanceInfo info, InstanceStatus newStatus,
                                             PeerEurekaNode node) {
    try {
        InstanceInfo infoFromRegistry;
        CurrentRequestVersion.set(Version.V2);
        switch (action) {
            case Cancel:
                node.cancel(appName, id);
                break;
            case Heartbeat:
                InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
                infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                break;
            case Register:
                node.register(info);
                break;
            case StatusUpdate:
                infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                break;
            case DeleteStatusOverride:
                infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                node.deleteStatusOverride(appName, id, infoFromRegistry);
                break;
        }
    } catch (Throwable t) {
    } finally {
        CurrentRequestVersion.remove();
    }
}

由上代码可知,


image-20211108143138054.png

对等节点管理器 PeerEurekaNodes

eureka 原生 PeerEurekaNodes

public class PeerEurekaNodes {

    protected final PeerAwareInstanceRegistry registry; // 注册表实现
    protected final EurekaServerConfig serverConfig; // server配置
    protected final EurekaClientConfig clientConfig; // client配置
    protected final ServerCodecs serverCodecs;
    private final ApplicationInfoManager applicationInfoManager; // 应用管理器
    
    // 管理维护的对等节点
    private volatile List peerEurekaNodes = Collections.emptyList();
    private volatile Set peerEurekaNodeUrls = Collections.emptySet();

    // 线程池,用于定时更新对等节点列表
    private ScheduledExecutorService taskExecutor;

    public void start() {
        // 构造线程池
        taskExecutor = Executors.newSingleThreadScheduledExecutor(
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r, "Eureka-PeerNodesUpdater");
                        thread.setDaemon(true);
                        return thread;
                    }
                }
        );
        
        try {
            // 更新管理对等节点列表
            // resolvePeerUrls 从配置中获取非当前节点的server配置url列表
            // updatePeerEurekaNodes 删除、创建节点,并更新缓存列表
            updatePeerEurekaNodes(resolvePeerUrls());
            
            // 构造更新对等节点列表线程Runable,并开始定时任务
            Runnable peersUpdateTask = new Runnable() {
                @Override
                public void run() {
                    try {
                        updatePeerEurekaNodes(resolvePeerUrls());
                    } catch (Throwable e) {
                        logger.error("Cannot update the replica Nodes", e);
                    }

                }
            };
            taskExecutor.scheduleWithFixedDelay(
                    peersUpdateTask,
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    TimeUnit.MILLISECONDS
            );
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}

Spring Cloud 定制 RefreshablePeerEurekaNodes,允许刷新对等节点,增加实现ApplicationListener接口,监听EnvironmentChangeEvent事件,更新对等节点列表

// 增加了ApplicationListener
static class RefreshablePeerEurekaNodes extends PeerEurekaNodes
      implements ApplicationListener {
    
   @Override
   public void onApplicationEvent(final EnvironmentChangeEvent event) {
      if (shouldUpdate(event.getKeys())) {
         updatePeerEurekaNodes(resolvePeerUrls());
      }
   }

   /*
    * 判断是否需要更新
    */
   protected boolean shouldUpdate(final Set changedKeys) {
      assert changedKeys != null;

      // dns 不需要更新
      if (this.clientConfig.shouldUseDnsForFetchingServiceUrls()) {
         return false;
      }

      // 如果配置变更的key包含“eureka.client.region”,则需要更新
      if (changedKeys.contains("eureka.client.region")) {
         return true;
      }
      // 如果配置变更的Key包含“eureka.client.service-url.”
      // 或“eureka.client.availability-zones.”前缀,在需要更新
      for (final String key : changedKeys) {
         if (key.startsWith("eureka.client.service-url.")
               || key.startsWith("eureka.client.availability-zones.")) {
            return true;
         }
      }
      return false;
   }

}

综上:对等节点管理器 PeerEurekaNodes 管理维护者对等节点列表,有两种更新方式

  • 定时任务 (PeerEurekaNodes)
  • EnvironmentChangeEvent 事件监听 (RefreshablePeerEurekaNodes)

对等节点 PeerEurekaNode

看PeerEurekaNode成员对象可知,主要是对等节点信息,还有就是批量调度器(batchingDispatcher)和非批量调度器(nonBatchingDispatcher),后文仅介绍批量调度器(batchingDispatcher)。

public class PeerEurekaNode {

    public static final String BATCH_URL_PATH = "peerreplication/batch/";
    private final String serviceUrl; // 当前节点地址
    private final EurekaServerConfig config; // server配置
    private final long maxProcessingDelayMs; // 最大延迟时间
    private final PeerAwareInstanceRegistry registry; // 注册表实现,见上文
    private final String targetHost; // 目标host
    private final HttpReplicationClient replicationClient; // http访问客户端

    /**
    * batchingDispatcher 与 nonBatchingDispatcher为批量和非批量的调度器
    **/
    private final TaskDispatcher batchingDispatcher;
    private final TaskDispatcher nonBatchingDispatcher;
    
    
    // 构造函数
    PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl,
                                 HttpReplicationClient replicationClient, EurekaServerConfig config,
                                 int batchSize, long maxBatchingDelayMs,
                                 long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
    this.registry = registry;
    this.targetHost = targetHost;
    this.replicationClient = replicationClient;

    this.serviceUrl = serviceUrl;
    this.config = config;
    this.maxProcessingDelayMs = config.getMaxTimeForReplication();

    String batcherName = getBatcherName();
    ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
    this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
            batcherName,
            config.getMaxElementsInPeerReplicationPool(),
            batchSize,
            config.getMaxThreadsForPeerReplication(),
            maxBatchingDelayMs,
            serverUnavailableSleepTimeMs,
            retrySleepTimeMs,
            taskProcessor
    );
    this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
            targetHost,
            config.getMaxElementsInStatusReplicationPool(),
            config.getMaxThreadsForStatusReplication(),
            maxBatchingDelayMs,
            serverUnavailableSleepTimeMs,
            retrySleepTimeMs,
            taskProcessor
    );
}
    
}

TaskDispatcher 任务调度器创建代码,极其简单,创建了两个对象,AcceptorExecutor和TaskExecutors。其中process方法,就是执行AcceptorExecutor的process方法。

public static  TaskDispatcher createBatchingTaskDispatcher(String id, int maxBufferSize, int workloadSize, int workerCount, long maxBatchingDelay, long congestionRetryDelayMs, long networkFailureRetryMs, TaskProcessor taskProcessor) {

    final AcceptorExecutor acceptorExecutor = new AcceptorExecutor(id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs);
    final TaskExecutors taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);
    
    return new TaskDispatcher() {
        public void process(ID id, T task, long expiryTime) {
            acceptorExecutor.process(id, task, expiryTime);
        }
        public void shutdown() {
            acceptorExecutor.shutdown();
            taskExecutor.shutdown();
        }
    };
}
接收处理器 AcceptorExecutor

数据结构

// 新任务队列,一段进一段出
BlockingQueue> acceptorQueue = new LinkedBlockingQueue<>();
// 重新处理任务队列,双端进出
BlockingDeque> reprocessQueue = new LinkedBlockingDeque<>();
// 
// 等待处理的任务,定时任务
Map> pendingTasks = new HashMap<>();
Deque processingOrder = new LinkedList<>();

// 即将处理的任务,单个
Semaphore singleItemWorkRequests = new Semaphore(0);
BlockingQueue> singleItemWorkQueue = new LinkedBlockingQueue<>();

// 即将要处理的任务,批量
Semaphore batchWorkRequests = new Semaphore(0);
BlockingQueue>> batchWorkQueue = new LinkedBlockingQueue<>();

提供的方法

// 提供给注册表实现的方法,接受任务,放入队列
void process(ID id, T task, long expiryTime) {
    acceptorQueue.add(new TaskHolder(id, task, expiryTime));
    acceptedTasks++;
}

// 提供给work线程TaskExecutors的方法,重新添加失败的任务,批量
void reprocess(List> holders, ProcessingResult processingResult) {
    reprocessQueue.addAll(holders);
    replayedTasks += holders.size();
    trafficShaper.registerFailure(processingResult);
}
// 提供给work线程TaskExecutors的方法,重新添加失败的任务,单个
void reprocess(TaskHolder taskHolder, ProcessingResult processingResult) {
    reprocessQueue.add(taskHolder);
    replayedTasks++;
    trafficShaper.registerFailure(processingResult);
}
// 提供给work线程TaskExecutors的方法,获取任务,单个
BlockingQueue> requestWorkItem() {
    singleItemWorkRequests.release(); // 释放锁
    return singleItemWorkQueue;
}
// 提供给work线程TaskExecutors的方法,获取任务,批量
BlockingQueue>> requestWorkItems() {
    batchWorkRequests.release(); // 释放锁
    return batchWorkQueue;
}

定时任务

class AcceptorRunner implements Runnable {
    @Override
    public void run() {
        long scheduleTime = 0;
        while (!isShutdown.get()) {
            try {
                // 将reprocessQueue队列中元素挪至pendingTasks和processingOrder中,
                // 并记录超时未处理的
                // 将acceptorQueue队列中元素挪着pendingTasks和processingOrder中
                // 在放置pendingTasks元素过程中,完成相同任务覆盖
                // pendingTasks 放满即停止
                // 如果放置之后acceptorQueue、reprocessQueue、pendingTasks都为空,
                // 阻塞等待获取10毫秒
                // 每次都取满pendingTasks或acceptorQueue、reprocessQueue被取完
                drainInputQueues();

                
                int totalItems = processingOrder.size();

                long now = System.currentTimeMillis();
                if (scheduleTime < now) {
                    scheduleTime = now + trafficShaper.transmissionDelay();
                }
                if (scheduleTime <= now) {
                    // 将pendingTasks中任务取出批量放入batchWorkQueue中
                    assignBatchWork();
                    // 将pendingTasks中任务取出一个放入singleItemWorkQueue中
                    assignSingleItemWork();
                }

                // 没有任务从pendingTasks移走,说明work线程阻塞或延迟,等待10毫秒
                if (totalItems == processingOrder.size()) {
                    Thread.sleep(10);
                }
            } catch (InterruptedException ex) {
                // Ignore
            } catch (Throwable e) {
                logger.warn("Discovery AcceptorThread error", e);
            }
        }
    }
任务处理器 TaskExecutors

在构造函数中开启work线程

TaskExecutors(WorkerRunnableFactory workerRunnableFactory, int workerCount, AtomicBoolean isShutdown) {
    this.isShutdown = isShutdown;
    this.workerThreads = new ArrayList<>();

    ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    for (int i = 0; i < workerCount; i++) {
        WorkerRunnable runnable = workerRunnableFactory.create(i);
        Thread workerThread = new Thread(threadGroup, runnable, runnable.getWorkerName());
        workerThreads.add(workerThread);
        workerThread.setDaemon(true);
        workerThread.start();
    }
}

work线程实现,已批量方式为例,

static class BatchWorkerRunnable extends WorkerRunnable {
    @Override
    public void run() {
        try {
            // 只要没有关闭 就一直重试
            while (!isShutdown.get()) {
                // 通过 AcceptorExecutor#requestWorkItems 获取任务,如果为空,poll 1秒
                List> holders = getWork();
                List tasks = getTasksOf(holders);
                
                // 由ReplicationTaskProcessor执行任务,
                // 如果是TransientError错误,如超时或连接失败,则放入重试队列
                ProcessingResult result = processor.process(tasks);
                switch (result) {
                    case Success:
                        break;
                    case Congestion:
                    case TransientError:
                        taskDispatcher.reprocess(holders, result);
                        break;
                    case PermanentError:
                        logger.warn();
                }
            }
        } catch (InterruptedException e) {
            // Ignore
        } catch (Throwable e) {
            logger.warn("Discovery WorkerThread error", e);
        }
    }

}

Eureka 配置

Eureka Instance 配置

public interface EurekaInstanceConfig {

    // eureka 中注册实例的id, 必须是惟一的,
    String getInstanceId();

    // eureka 中注册实例的应用名称 
    String getAppname();
    // eureka 中注册实例的应用组, 多个应用可分组,很少用,一般为null
    String getAppGroupName();

    // 这个属性用来决定应用服务是否一注册上就可以开始接收请求, 默认是false
    // 应用启动时,可以根据eureka.instance.instance-enabled-onit配置设定(默认为false),
    // 来配置初始注册到eureka server的时候,其status是UP,还是STARTING。
    // 默认初始化的时候是STARTING,之后自动注册的时候,变更为UP,表示可以开始接收请求。
    boolean isInstanceEnabledOnit();

    // 非安全端口
    int getNonSecurePort();
    // 安全端口
    int getSecurePort();

    // 非安全端口是否启用 
    boolean isNonSecurePortEnabled();
    // 安全端口是否启用 
    boolean getSecurePortEnabled();

    // 心跳续约间隔 默认30s
    int getLeaseRenewalIntervalInSeconds();

    // 租约到期时间 默认90秒
    int getLeaseExpirationDurationInSeconds();

    // 虚拟域名,其它实例可以通过虚拟域名找到这个实例,类似全限定域名访问 
    String getVirtualHostName();
    // 安全虚拟域名 
    String getSecureVirtualHostName();

    // 域名
    String getHostName(boolean refresh);

    // 元数据, K-V 结构,会被发送到 Server 端
    Map getMetadataMap();

    // ip 
    String getIpAddress();

    // 获取状态路径, /actuator/info
    String getStatusPageUrlPath();

    // 获取状态url, / ,配合 getStatusPageUrlPath 使用
    String getStatusPageUrl();

    // 获取home page路径, / 
    String getHomePageUrlPath();

    // 获取home page路径, / , 配合 getHomePageUrlPath 使用
    String getHomePageUrl();

    // 健康检查url path  /actuator/health
    // 通常用来做依据实例健康做决策
    String getHealthCheckUrlPath();

    // 健康检查url, / , 配合 getHealthCheckUrlPath 使用 
    String getHealthCheckUrl();

    // 健康检查url, / , 配合 getHealthCheckUrlPath 使用 
    String getSecureHealthCheckUrl();

    // 配置命名空间,一般是 "eureka"
    String getNamespace();

}

对应 Springboot 环境 EurekaInstanceConfig 类的实现是 EurekaInstanceConfigBean 类

@ConfigurationProperties("eureka.instance")
public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {
        
}

EurekaInstanceConfig 被 InstanceInfoFactory 用来创建 InstanceInfo 对象。

public class InstanceInfoFactory {

    public InstanceInfo create(EurekaInstanceConfig config) {
        LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
                .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
                .setDurationInSecs(config.getLeaseExpirationDurationInSeconds());

        InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();

        String namespace = config.getNamespace();
        if (!namespace.endsWith(".")) {
            namespace = namespace + ".";
        }
        builder.setNamespace(namespace).setAppName(config.getAppname())
                .setInstanceId(config.getInstanceId())
                .setAppGroupName(config.getAppGroupName())
                .setDataCenterInfo(config.getDataCenterInfo())
                .setIPAddr(config.getIpAddress())
                .setHostName(config.getHostName(false))
                .setPort(config.getNonSecurePort())
                .enablePort(InstanceInfo.PortType.UNSECURE,
                        config.isNonSecurePortEnabled())
                .setSecurePort(config.getSecurePort())
                .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
                .setVIPAddress(config.getVirtualHostName())
                .setSecureVIPAddress(config.getSecureVirtualHostName())
                .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
                .setStatusPageUrl(config.getStatusPageUrlPath(),
                        config.getStatusPageUrl())
                .setHealthCheckUrls(config.getHealthCheckUrlPath(),
                        config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
                .setASGName(config.getASGName());

        // Start off with the STARTING state to avoid traffic
        // 如果 isInstanceEnabledOnit 配置为 false, 则启动状态为 STARTING
        // 否则启动状态为 UP
        if (!config.isInstanceEnabledOnit()) {
            InstanceInfo.InstanceStatus initialStatus = InstanceInfo.InstanceStatus.STARTING;
            if (log.isInfoEnabled()) {
                log.info("Setting initial instance status as: " + initialStatus);
            }
            builder.setStatus(initialStatus);
        }
        else {
            if (log.isInfoEnabled()) {
                log.info("Setting initial instance status as: "
                        + InstanceInfo.InstanceStatus.UP
                        + ". This may be too early for the instance to advertise itself as available. "
                        + "You would instead want to control this via a healthcheck handler.");
            }
        }

        // 添加任意的元数据
        for (Map.Entry mapEntry : config.getMetadataMap().entrySet()) {
            String key = mapEntry.getKey();
            String value = mapEntry.getValue();
            // only add the metadata if the value is present
            if (value != null && !value.isEmpty()) {
                builder.add(key, value);
            }
        }

        InstanceInfo instanceInfo = builder.build();
        instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
        return instanceInfo;
    }

}

Eureka Client 配置

public interface EurekaClientConfig {

   // 获取注册列表时间间隔 默认30s
    int getRegistryFetchIntervalSeconds();

    // 更新实例信息的变化到 Server 的间隔时间,默认值30s
    int getInstanceInfoReplicationIntervalSeconds();
    // 行上面任务的初始化Delay延迟  默认值40s
    int getInitialInstanceInfoReplicationIntervalSeconds();

    // 轮询eureka服务器信息更改的频率(动态获取serviceUrl列表)
    // Server 可能会增加或减少,这个配置控制 Client 可以多块知晓变更
    // 默认是5分钟
    int getEurekaServiceUrlPollIntervalSeconds();

    // Server 的代理
    String getProxyHost();
    String getProxyPort();
    String getProxyUserName();
    String getProxyPassword();

    // 是否要对 Server 拉取的内容容进行 GZip 压缩 默认值是 true
    // true:会加过滤器 GZIPContentEncodingFilter,在请求上加请求头:`Accept-Encoding:gzip`
    boolean shouldGZipContent();

    // Client 访问 Server, 控制发送请求时的 readTimeout 值,默认是8s 
    int getEurekaServerReadTimeoutSeconds();
    // Client 访问 Server, 控制发送请求时的 connectionTimeout 值,默认是8s  
    int getEurekaServerConnectTimeoutSeconds();

    // 获取备注册中心的实现类(若你的EurekaClient链接的Server挂了,就使用它去连其它的)
    // 若你想做兜底,那么可以使用它(比如使用Nacos做备用)
    // 说明:内部并未提供任何实现,若有需要请自己提供实现类,且配置好就可生效
    String getBackupRegistryImpl();

    // Client 是可以并发发出N多个请求请求 Server 的,
    // 这里就给出了限制,避免单个 Client 实例把 Server 就给搞垮了
    // 控制 maxTotalConnections,也就是发送请求的连接池的最大容量,默认值200
    // 如果是Apache的HC,那就是控制它的连接池大小
    int getEurekaServerTotalConnections();

    //  单台host的允许的连接总数  默认值50
    int getEurekaServerTotalConnectionsPerHost();

    // ========下面这些配置均只有在eureka服务器ip地址列表是在DNS中才会用到,默认为null======= //
    // 表示eureka注册中心的路径,如果配置为eureka,则为http://x.x.x.x:x/eureka/
    // 在eureka的配置文件中加入此配置表示eureka作为客户端向注册中心注册,从而构成eureka集群
    String getEurekaServerURLContext();
    // 获取eureka服务器的端口
    String getEurekaServerPort();
    // 获取要查询的DNS名称来获得eureka服务器
    String getEurekaServerDNSName();
    // 是否使用 DNS 方式获取 Eureka-Server URL 地址  默认是false
    // 在获取到Server集群的节点后,会解析出url们。这里决定如何解析(要不要用DNS)
    boolean shouldUseDnsForFetchingServiceUrls();

    // 是否注册自己这个实例到Server上  默认是true
    // 有些情况,你不希望腻子注册到 Server, 可以选择不注册,如服务消费者
    // 如果不注册自己,很多定时任务是不需要的,因此Server端建议关闭此项
    // 当然你若想看看自己是否还“活着”,让其注册上去也无妨     
    boolean shouldRegisterWithEureka();

    // 在 Client shutdown 时,是否从 Server 上主动下线, 默认true
    // 它是在EurekaClient#shutdown方法里被调用的
    default boolean shouldUnregisterOnShutdown() {
        return true;
    }

    // 实例是否使用同一zone里的eureka服务器,默认为true
    // 理想状态下,eureka客户端与服务端是在同一zone下
    boolean shouldPreferSameZoneEureka();

    // 简单的说:是否允许Server端给你返回302重定向其它机器去处理(比如自己负载太高了不想处理)
    // 它的实现方式是发送请求时添加请求头:`X-Discovery-AllowRedirect:true`
    // 默认值是false
    boolean allowRedirects();

    // 当Client本地的实例们和Server返回的实例们出现差时(比如状态变更、元数据变更等),是否记录log日志
    // 默认值是false,不记录。毕竟这种日志意义并不大~~~没必要消耗性能
    boolean shouldLogDeltaDiff();

    // 是否禁用增量获取 true:每次全量获取,false:每次增量获取
    // 默认值是false
    boolean shouldDisableDelta();

    // Eureka-Server 的 URL 集合
    // 默认为http://XXXX:X/eureka/,但是如果采用DNS方式获取服务地址,则不需要配置此设置。
    List getEurekaServerServiceUrls(String myZone);

    // 获取实例时是否过滤,仅保留UP状态的实例  默认值是true
    boolean shouldFilterOnlyUpInstances();

    // 控制连接线程池的。最大空闲时间,默认值30s
    int getEurekaConnectionIdleTimeoutSeconds();

    // 是否从Eureka服务端获取注册信息  默认true
    boolean shouldFetchRegistry();

    // 只去只获得一个 vipAddress 对应的应用实例们的注册信息。默认值null
    // 若指定来该值:那么只会从这个单一的vipAddress里获取
    @Nullable
    String getRegistryRefreshSingleVipAddress();

    // 心跳执行程序(续约线程)线程池的大小   默认为5
    int getHeartbeatExecutorThreadPoolSize();

    // 心跳超时重试延迟时间的最大乘数值。默认值是10
    // 也就是超时了的话  5*10  50s后再去试一把
    int getHeartbeatExecutorExponentialBackOffBound();

    // 缓存刷新线程池的初始化线程数  默认值5
    int getCacheRefreshExecutorThreadPoolSize();

    // 缓存刷新重试延迟时间的最大乘数值。默认值是10
    // 也就是超时了的话  5*10  50s后再去试一把    
    int getCacheRefreshExecutorExponentialBackOffBound();

    // eureka服务器序列化/反序列化的信息中获取“$”符号的的替换字符串。默认为“_-”
    String getDollarReplacement();

    // eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“__”
    String getEscapeCharReplacement();

    // true:通过ApplicationInfoManager本地实例状态时,立即触发更新到远程server
    // false:不立即触发,依赖于心跳,默认值true
    boolean shouldOnDemandUpdateStatusChange();

    // 客户端是否应在初始化期间强制注册  默认值false
    // 毕竟初始化期间不一定全部搞定了,所以为false较好
    default boolean shouldEnforceRegistrationAtInit() {
        return false;
    }


    String getEncoderName();
    String getDecoderName();

    // 加上请求头:`X-Eureka-Accept = xxx`
    // 可选值是:full/compact
    String getClientDataAccept();

    // experimental:试验的
    // 当尝试新功能迁移过程时,为了避免配置API污染,试验的这部分配置即可通过此接口传递
    // 可见:JerseyEurekaHttpClientFactory#buildExperimental用于构建试验的配置
    // 用于灰度一些实验性的配置比较好用
    String getExperimental(String name);

    // 为了兼容性,返回传输层配置类
    // EurekaTransportConfig会放在详解传输的时候解析
    // 也是在DiscoveryClient里会使用到
    EurekaTransportConfig getTransportConfig();
}

Springboot 环境,EurekaClientConfig 的实现类是 EurekaClientConfigBean。

@ConfigurationProperties(EurekaClientConfigBean.PREFIX)
public class EurekaClientConfigBean implements EurekaClientConfig, Ordered {
}

public static final String PREFIX = "eureka.client";

Eureka Server 配置

public interface EurekaServerConfig {

    // 是否开启自我保护机制 默认开启true
    boolean shouldEnableSelfPreservation();
    // 开启自我保护模式比例,超过该比例后开启自我保护模式。默认0.85
    double getRenewalPercentThreshold();
    // 自我保护模式比例更新定时任务执行频率。
    int getRenewalThresholdUpdateIntervalMs();
    // 期望客户端发送心跳的间隔。
    // 默认值为30秒。
    // 如果客户端以不同的频率发送心跳,比如说,每15秒发送一次,那么应该相应地调整此参数,
    // 否则,自我保护将无法按预期工作。
    int getExpectedClientRenewalIntervalSeconds();

    // server 节点间相互发现周期
    int getPeerEurekaNodesUpdateIntervalMs();

    // 复制数据使用压缩,通过 Accept-Encoding 实现。
    boolean shouldEnableReplicatedRequestCompression();
    // 复制同步重试次数
    int getNumberOfReplicationRetries();
    
    // server 节点状态刷新周期
    int getPeerEurekaStatusRefreshTimeIntervalMs();

    // 设置如果Eureka Server启动时无法从临近Eureka Server节点获取注册信息,
    // 它多久不对外提供注册服务
    int getWaitTimeInMsWhenSyncEmpty();

    // server 节点间访问连接超时时间
    int getPeerNodeConnectTimeoutMs();

    // server 节点间读超时时间
    int getPeerNodeReadTimeoutMs();

    // 节点间 最大连接数
    int getPeerNodeTotalConnections();

    // 节点间对某一节点最大连接数
    int getPeerNodeTotalConnectionsPerHost();

    // 连接空闲时间
    int getPeerNodeConnectionIdleTimeoutSeconds();

    // 增量队列缓存保留时间
    long getRetentionTimeInMSInDeltaQueue();

    // 增量队列检查缓存过期时间周期
    long getDeltaRetentionTimerIntervalInMs();

    // 剔除心跳周期
    long getEvictionIntervalTimerInMs();

    // 只读缓存过期时间
    long getResponseCacheAutoExpirationInSeconds();

    // 二级缓存过期时间
    long getResponseCacheUpdateIntervalMs();

    // 使用一级缓存
    boolean shouldUseReadOnlyResponseCache();

    // 是否关闭使用增量信息
    boolean shouldDisableDelta();

    // 同步线程池 线程空闲时间
    long getMaxIdleThreadInMinutesAgeForStatusReplication();

    // 同步线程池 最小线程数
    int getMinThreadsForStatusReplication();

    // 同步线程池 最大线程数
    int getMaxThreadsForStatusReplication();

    // 同步线程池 同步队列最大值
    int getMaxElementsInStatusReplicationPool();

    // 当时间戳不同时 是否同步
    boolean shouldSyncWhenTimestampDiffers();

    // server启动时,从其他节点拉取注册表 重试次数
    int getRegistrySyncRetries();

    // server启动时,从其他节点拉取注册表 重试等待时间
    long getRegistrySyncRetryWaitMs();

    // 同步线程 批量大小
    int getMaxElementsInPeerReplicationPool();
    // 最小up状态的server节点数目
    int getHealthStatusMinNumberOfAvailablePeers();

    // 批量同步最大延迟时间,超时不处理
    int getMaxTimeForReplication();
    // 是否使用批量同步
    boolean shouldBatchReplication();
    // 当前serverUrl
    String getMyUrl();
    // 是否启动限流
    boolean isRateLimiterEnabled();
    // 总限流值
    int getRateLimiterBurstSize();
    // 拉取请求限流值
    int getRateLimiterRegistryFetchAverageRate();
    // 全量拉取限流值
    int getRateLimiterFullFetchAverageRate();
}

你可能感兴趣的:(eureka 源码阅读)