Eureka Server 源码解析

Eureka Server 为了满足与 Eureka Client 的交互,它提供了以下功能:

  • 服务注册
  • 接收服务心跳
  • 服务剔除
  • 服务下线
  • 获取集群
  • 获取注册表中的服务实例

一、Eureka Server 端基本设计

在这里首先看一张类结构图:

从上面的类图中,可以看到最顶层的接口是 LookupService 和 LeaseManager,关于LookupService 接口,在前面一篇文章已有阐述,它的主要作用是做服务列表查询的(因为 Eureka Server 端也可以向自己发起注册)。而 LeaseManager 接口从其命名上,便可看出它是用于 租约管理的,所谓的租约管理就是指服务的注册、下线、剔除、续约。LookupService 和 LeaseManager 接口代码如下:

public interface LeaseManager<T> {

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


public interface LookupService<T> {

    Application getApplication(String appName);

    Applications getApplications();

    List<InstanceInfo> getInstancesById(String id);

    InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}

LeaseManager 所管理的对象是 Lease,Lease 代表着 Eureka Client 中服务实例信息的租约, 它持有对其类持有的类的时间有效性操作。 Lease 所持有的类便是 InstanceInfo(服务实例信息),在其类中定义了租约的操作类型,如:注册、更新、下线,同时定义了对租约时间属性的各项操作(租约默认有效时长是90 s)。

InstanceRegistry 接口是 Eureka Server 注册表管理的核心接口,主要是为了在内存中管理注册到 Eureka Server中的服务实例,它继承了 LookupService 和 LeaseManager,并在其基础上添加了一些其他功能,使其具有更简单的管理服务实例租约和服务实例列表信息的查询。在 AbstractInstanceRegistry 抽象类中可查看其对 InstanceRegistry 接口 的具体实现。

对于 PeerAwareInstanceRegistry 接口,它是继承了 InstanceRegistry 接口的,PeerAwareInstanceRegistry 接口主要是添加了对 Eureka Server 集群的相关操作方法,其实现类 PeerAwareInstanceRegistryImpl 继承 AbstractInstanceRegistry 的实现,这个实现类主要是在对本地注册表操作的基础上,又添加了对其节点 peer 节点对同步复制操作,可使得 Eureka Server 集群中的注册表信息保持一致。

最下层的 InstanceRegistry 类,它继承了PeerAwareInstanceRegistryImpl 类,其主要是为了适配 Spring Cloud 的使用环境。

二、服务注册

当 Eureka Client 发起服务注册的时候,会把自身的的信息包装成 InstanceInfo,然后将 InstanceInfo 发送到 Eureka Server 端。Eureka Server 在接收到 Eureka Client 端所发送的 InstanceInfo 的时候,会将其放到本地注册表中,方便后续 Eureka Client 进行服务查询。服务注册主要实现的代码在 AbstractInstanceRegistry#register 中,如下:

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        // 获取读锁
        read.lock();
        try {
            // 根据 appName 对服务实例集群进行分类
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                // 这是一个比较严谨的操作,防止在添加新的服务注实例集群租约的时候,把已有的其他线程添加的租约信息给覆盖掉。所以这里的语义是如果存在该键值
                // 的时候直接返回存在的值;否则添加该键值对,并返回 null
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            // 根据 instanceId 获取实例的租约
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
      
                // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
                // InstanceInfo instead of the server local copy.
                // 如果该实例租约已经存在,比较最后更新时间戳大小,取最大值的注册信息为有效
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    registrant = existingLease.getHolder();
                }
            } else {
                // The lease does not exist and hence it is a new registration
                // 如果这个租约不存在,则表示它是一个新的注册实例
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        // Since the client wants to register it, increase the number of clients sending renews
                        // 自我保护机制
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
            }
            // 创建新的租约
            Lease<InstanceInfo> lease = new Lease<>(registrant, leaseDuration);
            if (existingLease != null) {
                // 如果租约存在,继承租约在服务上线的初试时间
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            // 保存租约
            gMap.put(registrant.getId(), lease);
            // 添加到队列
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            // 根据实例 Id 获得覆盖实例状态的集合,如果存在的话,则需要更新服务实例的覆盖状态            
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            // 根据覆盖状态规则得到服务实例的最红状态,并设置服务实例的当前状态
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            // 如果服务实例状态为 UP 设置租约的上线时间,只有第一次设置有效
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            // 添加最近租约变更记录队列,标识其状态为 ADDED
            // 这将用于 Eureka Client 增量式获取注册表信息
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            // 设置服务实例的更新时间
            registrant.setLastUpdatedTimestamp();
            // 设置 response 缓存过期,着将用于 Eureka Client 全量获取注册表信息
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            // 释放锁
            read.unlock();
        }
    }

上面代码 registry.get(registrant.getAppName()) 中的 registry 集合是线程安全的 ConcurrentHashMap,它的key 所存的是 appName ,而其 value 所存的是 Map<String, Lease<InstanceInfo>>,由此可见 value 是map 集合,map 集合的 value 所存的便是 Lease(租约),从代码中租约对象中所管理的是 InstanceInfo(服务实例)。在服务注册的时候,会先获取一把锁,为的是防止其他线程对 registry 的数据进行再次操作,以避免数据的出现不一致。然后从集合中根据 appName查询对应的注册表,如果不存在,则进行相关操作(看代码注释)。当存在的时候,便根据服务实例 Id 获取对应的服务实例信息,。如果租约存在的话,便会比较两个租约中的 InstanceInfo 最后更新时间 LastDirtyTimestamp ,保存时间戳大的服务实例的信息,而如果不存在的话,则说明是一次全新的服务注册,便会进行自我保护的统计操作,然后创建新的租约,创建成果后再将其存入 registry 中。

之后还会进行一系列的缓存操作,并根据覆盖状态规则来设置服务实例的状态。这里的缓存操作包括 将 InstanceInfo 加入用于统计 Eureka Client 增量式获取注册表信息的 recentlyChangedQueue 队列和 responseCache 中。最后便是设置服务实例租约的上线时间,来用于计算租约的有效时间,然后释放读锁完成服务注册的操作。

三、服务续约

当服务注册完成后,还需要定时向 Eureka Server 发送心跳请求(默认为 30 s),以此维持自己在 Eureka Server 中租约的有效性。

关于 Eureka Server 处理心跳的核心代码在 AbstractInstanceRegistry#renew方法中,这个方法的入参有服务名称、服务实例 Id,代码如下:

public boolean renew(String appName, String id, boolean isReplication) {
    RENEW.increment(isReplication);
    // 根据 appName 获取 服务实例的集合信息
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
     // 如果租约不存在,直接返回 false
    if (leaseToRenew == null) {
        RENEW_NOT_FOUND.increment(isReplication);
        logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
        return false;
    } else {
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // touchASGCache(instanceInfo.getASGName());
            // 根据覆盖状态规则,得到服务实例信息的最终状态
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
                        + "; re-register required", instanceInfo.getId());
                // 如果所得到的服务实例的状态是 UNKNOWN,便取消租约
                RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }
            // 当服务实例的状态信息不包含覆盖实例的状态信息,需设置其服务实例的状态信息
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                logger.info(
                        "The instance status {} is different from overridden instance status {} for instance {}. "
                                + "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
                                overriddenInstanceStatus.name(),
                                instanceInfo.getId());
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

            }
        }
        // 统计每分钟租约续租的次数,用于自我保护机制
        renewsLastMin.increment();
        // 更新租约中的有效时间
        leaseToRenew.renew();
        return true;
    }
}

四、服务剔除

在 Eureka Client 服务注册后,既没有续约,也没有下线(服务崩溃或网络异常原因导致),那么服务便状态便处于不可知状态,这时这个服务实例的数据便不具有实用性,因此需要对其进行清理,而这样剔除服务操作的代码在 AbstractInstanceRegistry#evict 中,该方法还会批量处理所有过期的租约,代码如下:

@Override
public void evict() {
    evict(0l);
}

public void evict(long additionalLeaseMs) {
    logger.debug("Running the evict task");
    // 自我保护相关, 如果出现该状态,则不需剔除
    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }

    // We collect first all expired items, to evict them in random order. For large eviction sets,
    // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
    // the impact should be evenly distributed across all applications.
    // 遍历注册表集合,一次性获取所有过期的租约
    List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
    for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
        Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
        if (leaseMap != null) {
            for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                Lease<InstanceInfo> lease = leaseEntry.getValue();
                if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                    expiredLeases.add(lease);
                }
            }
        }
    }

    // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
    // triggering self-preservation. Without that we would wipe out full registry.
    // 计算最大允许剔除租约的数量,获取注册表租约总数
    int registrySize = (int) getLocalRegistrySize();
    // 计算注册表租约的阈值,和自我保护相关
    int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
    int evictionLimit = registrySize - registrySizeThreshold;

    // 计算剔除租约的数量
    int toEvict = Math.min(expiredLeases.size(), evictionLimit);
    if (toEvict > 0) {
        logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
        // 逐个随机剔除
        Random random = new Random(System.currentTimeMillis());
        for (int i = 0; i < toEvict; i++) {
            // Pick a random item (Knuth shuffle algorithm)
            int next = i + random.nextInt(expiredLeases.size() - i);
            Collections.swap(expiredLeases, i, next);
            Lease<InstanceInfo> lease = expiredLeases.get(i);

            String appName = lease.getHolder().getAppName();
            String id = lease.getHolder().getId();
            EXPIRED.increment();
            logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
            // 逐个剔除
            internalCancel(appName, id, false);
        }
    }
}

服务剔除的操作会便利注册表,找到其中需要剔除的租约,然后根据配置文件中续租百分比的阈值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中的租约总数量减去当前组侧标租约的阈值),然后分批剔除过期的服务实例租约,这个操作在 AbstractInstanceRegistry#internalCancel 方法中。

在服务剔除的 AbstractInstanceRegistry#evict 方法中,有很多的限制,其都是为了保证 Eureka Server 端可用性:

  • 自我保护时期不能进行服务剔除操作
  • 过期操作进行分批执行
  • 服务剔除是逐个随机剔除的,剔除均匀分布在所有的应用中,为的是防止同一时间内同一服务集群中的服务的全部过期的都没同时剔除,以致大量剔除发生时,在未进行自我保护前,促使了程序的崩溃

服务剔除其实也是一个定时任务,在 AbstractInstanceRegistry 类中定义了一个 EvictionTask ,来用于定时执行服务剔除(默认是 60S 一次),而这个定时任务一般是在 AbstractInstanceRegistry 类初始化结束后进行的,其执行频率那是根据 EvictionIntervalTimerInMs 设定的,来定时清除过期的服务实例租约。

自我保护机制主要是作用在 Eureka Server 与Eureka Client 存在网络分区的情况下,且在服务端和客户端皆有实现。加入在某种特定的情况下(网络出现错误),Eureka Client 和 Eureka Server 无法进行通信,那么Eureka Client便无法向 Eureka Server 发起服务注册和续约的请求,这样 Eureka Server 中便会出现服务实例租约大量过期而被剔除的危险,但是 Eureka Client 可能却还是处于健康的状态,这样直接将服务实例租约直接剔除是不合理的。所以,Eureka 设计了自我保护机制,在Eureka Server 端,如果出现大量的服务剔除时间,那么便会触发自我保护机制,以保护注册表中的服务实例不被剔除,在通信稳定后,再退出此模式。而在 Eureka Client 向 Eureka Server 注册失败,将快速超时并尝试与其他 Eureka Server 进行通信,因此 “自我保护机制”设计大大提高了 Eureka 的可用性。

五、服务下线

Eureka Client 在应用销毁的时候,会向 Eureka Server 发送服务下线请求,Eureka Server 便会清楚服务实例注册表中对应的应用租约,以避免无效的服务调用。在服务剔除过程中,也是通过服务下线来完成对单个服务实例过期租约的清除工作。

服务下线的主要代码在 AbstractInstanceRegistry#cancel 方法中,这里所传的是服务实例名称和服务实例 Id等值,以此完成服务下线需求,代码如下:

@Override
public boolean cancel(String appName, String id, boolean isReplication) {
    return internalCancel(appName, id, isReplication);
}

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    // 获取读锁,防止其他线程修改
    read.lock();
    try {
        CANCEL.increment(isReplication);
        // 根据 appName 获取对应的服务实例集群
        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 + ")"));
        InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
        if (instanceStatus != null) {
            logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
        }
        // 如果租约不存在,直接返回 false
        if (leaseToCancel == null) {
            CANCEL_NOT_FOUND.increment(isReplication);
            logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
            return false;
        } else {
            // 设置租约的下线时间
            leaseToCancel.cancel();
            InstanceInfo instanceInfo = leaseToCancel.getHolder();
            String vip = null;
            String svip = null;
            if (instanceInfo != null) {
                instanceInfo.setActionType(ActionType.DELETED);
                // 添加最近租约变更的记录队列,标识其服务实例状态为 DELETED
                // 将应用于 Eureka Client 增量式获取注册表信息
                recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                instanceInfo.setLastUpdatedTimestamp();
                vip = instanceInfo.getVIPAddress();
                svip = instanceInfo.getSecureVipAddress();
            }
            // 设置 response 缓存过期
            invalidateCache(appName, vip, svip);
            logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
        }
    } finally {
        read.unlock();
    }

    synchronized (lock) {
        // 判断期望发送更新的数量是否大于 0
        if (this.expectedNumberOfClientsSendingRenews > 0) {
            // Since the client wants to cancel it, reduce the number of clients to send renews.
            // 计算期望发送更新的数量
            this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
            // 计算每分钟的阈值
            updateRenewsPerMinThreshold();
        }
    }
    
    // 下线成功
    return true;
}

六、集群同步

当 Eureka Server 是通过集群的方式部署的,那么为了维护 Eureka Server 注册表数据的一致性,必然需要一个同步机制,来同步 Eureka Server 集群中注册表信息的一致性。Eureka Server 同步包括两部分:

  • Eureka Server 在启动的过程中,从它的 peer 接待你拉取注册表信息,并将这些服务实例信息同步到本地注册表中
  • Eureka Server 在每次操作本地的注册表的时候,同时会将本地的注册表信息同步到它的 peer 节点中

1、Eureka Server 初始化本地注册表信息

在 Eureka Server 启动的过程中(见 EurekaServerBootstrap#initEurekaServerContext 方法),它会从其 peer 节点拉取注册表信息,并同步到本地注册表中,其主要实现在PeerAwareInstanceRegistryImpl#syncUp 方法中,代码如下:

@Override
public int syncUp() {
    // Copy entire entry from neighboring DS node
    // 从临近节点复制整个注册表
    int count = 0;
    // 如果获取不到,线程等待
    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
        if (i > 0) {
            try {
                Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
            } catch (InterruptedException e) {
                logger.warn("Interrupted during registry transfer..");
                break;
            }
        }
        // 获取所有的服务实例
        Applications apps = eurekaClient.getApplications();
        for (Application app : apps.getRegisteredApplications()) {
            for (InstanceInfo instance : app.getInstances()) {
                try {
                    // 判断是否可注册,主要用于 AWS 环境下,若部署在其他环境,直接返回 true
                    if (isRegisterable(instance)) {
                        // 注册到自身注册表中
                        register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        count++;
                    }
                } catch (Throwable t) {
                    logger.error("During DS init copy", t);
                }
            }
        }
    }
    return count;
}

Eureka Server 本身也是一个 Eureka Client ,在启动的时候同样也会进行 DiscoveryClient 的初始化,同时将其对应的 Eureka Server 注册表信息进行全量拉取。当 Eureka Server 集群部署的时候,Eureka Server 还会从它的 peer 节点拉取注册表信息,然后遍历整个 Applications ,将所有的服务实例通过 AbstractInstanceRegistry#register 方法注册到自身的注册表中。

在初始化本地注册表是时候,Eureka Server 不会接受来自 Eureka Client 的所有请求(如注册、获取注册表信息)。在信息同步结束后会通过 InstanceRegistry#openForTraffic 方法允许该 Server 接受请求,代码如下:

@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
    // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
    // 初始化自我保护机制统计参数
    this.expectedNumberOfClientsSendingRenews = count;
    updateRenewsPerMinThreshold();
    logger.info("Got {} instances from neighboring DS node", count);
    logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
    this.startupTime = System.currentTimeMillis();
    // 如果同步的实例数量为 0,将会在一段时间内,拒绝 Client 获取注册表信息
    if (count > 0) {
        this.peerInstancesTransferEmptyOnStartup = false;
    }
    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
    boolean isAws = Name.Amazon == selfName;
    // 判断是否在 AWS 环境运行,慈湖忽略
    if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
        logger.info("Priming AWS connections for all replicas..");
        primeAwsReplicas(applicationInfoManager);
    }
    logger.info("Changing status to UP");
    // 修改服务实例状态为健康上线,接受请求
    applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
    super.postInit();
}

2、Eureka Server 之间注册表信息的同步复制

为保证 Eureka 集群运行时注册表数据的一致性,每个 Eureka Server 在对自身注册表进行管理的时候,还需要同步到其 peer 节点中。在 PeerAwareInstanceRegistryImpl 中的注册、续约、下线等方法中,都添加了对 peer 节点的操作,以保持 Server 集群中注册表信息的一致性。代码如下:

@Override
public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
    if (super.cancel(appName, id, isReplication)) {
        // 同步下线状态
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);

        return true;
    }
    return false;
}

@Override
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);
}

public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
        // 同步续约状态
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

由此可以看到我们只需关注 PeerAwareInstanceRegistryImpl#replicateToPeers 方法,这里面会遍历 Eureka Server 的 peer 节点中的所有信息,然后向每个节点发送同步请求,代码如下:

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
        // 如果 peer 集群为空,或者本来就不需复制,那么就不再复制,防止造成死循环
        if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
            return;
        }
        // 向 peer 集群中的每一个 peer 发送同步请求
        for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
            // If the url represents this host, do not replicate to yourself.
            // 如果是自己,则无需同步复制
            if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                continue;
            }
            // 根据 action 调用不同的同步请求
            replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
        }
    } finally {
        tracer.stop();
    }
}

这里的 PeerEurekaNode 代表着一个可同步的共享数据的 Eureka Server。在 PeerEurekaNode 里面具有 register、cancel、heartbeat 和 status Update 等诸多用于向 peer 节点同步注册信息的操作。PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers 具体实现如下:

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) {
        logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
    } finally {
        CurrentRequestVersion.remove();
    }
}

PeerEurekaNode 中的每一个同步复制操作都是通过任务流的方式进行操作的,同一时间段内相同服务实例的相同操作将都使用相同的任务编号,在进行同步复制操作的时候,将根据任务编号合并操作,减少同步操作的数量和网路消耗,但是同时也造成了同步复制的延时性,不满足 CAP 中的C(强一致性)。

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/121433307