04.Spring Cloud 之 Eureka Client 源码解析

1. 环境搭建

代码已经上传至 https://github.com/masteryourself-tutorial/tutorial-spring,工程是 tutorial-spring-cloud/tutorial-spring-cloud-eureka/tutorial-spring-cloud-eureka-single-7001

2. 源码解析

详细的源码注释可参考 https://github.com/masteryourself/eureka

2.1 服务 注册/续约/下线 处理

2.1.1 流程分析

服务注册续约下线处理

2.1.2 核心源码剖析
1. com.netflix.eureka.registry.AbstractInstanceRegistry#register
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        // 注册中心,实际上就是一个 ConcurrentHashMap
        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>>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        // 正常来说,第一次注册时候 existingLease 是空数据,走 else 流程
        // 但是也存在这种情况:第一次注册时候,由于网络原因还没有处理,心跳的请求已经先处理结束,然后这里的 existingLease 是有值的
        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();
            logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);

            // 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.
            // 存在这种情况:心跳的请求先处理了,然后再处理注册,这时候就有可能 existingLastDirtyTimestamp > registrationLastDirtyTimestamp
            if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                        " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                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();
                }
            }
            logger.debug("No previous lease information found; it is new registration");
        }
        // 创建续约对象
        Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        // 进行注册操作
        gMap.put(registrant.getId(), lease);
        synchronized (recentRegisteredQueue) {
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
        }
        
        ...
        
    } finally {
        read.unlock();
    }
}
2. com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers
private void replicateInstanceActionsToPeers(Action action, String appName,
                                             String id, InstanceInfo info, InstanceStatus newStatus,
                                             PeerEurekaNode node) {
    try {
        InstanceInfo infoFromRegistry = null;
        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);
    }
}
3. com.netflix.eureka.cluster.PeerEurekaNode#register
public void register(final InstanceInfo info) throws Exception {
    // 获取过期时间
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            taskId("register", info),
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                    // 在过期时间之前需要完成 register() 任务
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}
4. com.netflix.eureka.registry.AbstractInstanceRegistry#renew
public boolean renew(String appName, String id, boolean isReplication) {
    RENEW.increment(isReplication);
    // 获取注册中心对应的应用集合信息
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        // 获取其对应的 Lease 信息
        leaseToRenew = gMap.get(id);
    }
    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) {
        
            ...
            
            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(),
                                instanceInfo.getOverriddenStatus().name(),
                                instanceInfo.getId());
                // 设置状态信息
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

            }
        }
        // 计数器 +1
        renewsLastMin.increment();
        // 更新最后修改时间
        leaseToRenew.renew();
        return true;
    }
}
5. com.netflix.eureka.cluster.PeerEurekaNode#heartbeat
public void heartbeat(final String appName, final String id,
                      final InstanceInfo info, final InstanceStatus overriddenStatus,
                      boolean primeConnection) throws Throwable {
    // 判断是否是第一次
    if (primeConnection) {
        // We do not care about the result for priming request.
        replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
        return;
    }
    ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
        @Override
        public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
            return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
        }

        ...
       
    };
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    // 在过期时间之前需要完成 sendHeartBeat() 任务
    batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}
6. com.netflix.eureka.registry.AbstractInstanceRegistry#internalCancel
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);
        }
        synchronized (recentCanceledQueue) {
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
        }
        // 从状态 map 中删除
        InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
        if (instanceStatus != null) {
            logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
        }
        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();
            
            ...
            
            return true;
        }
    } finally {
        read.unlock();
    }
}
7. com.netflix.eureka.cluster.PeerEurekaNode#cancel
public void cancel(final String appName, final String id) throws Exception {
    long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;
    batchingDispatcher.process(
            taskId("cancel", appName, id),
            new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) {
                @Override
                public EurekaHttpResponse<Void> execute() {
                    // 在过期时间之前需要完成 cancel() 任务
                    return replicationClient.cancel(appName, id);
                }

                ...
                
            },
            expiryTime
    );
}

2.2 EurekaServerAutoConfiguration 装配流程

EurekaServerAutoConfiguration 装配流程

spring-cloud-starter-netflix-eureka-server-xxx.jar -> spring-cloud-netflix-eureka-server-xxx.jar -> 定义 spring.factories 文件 -> 向容器中导入 EurekaServerAutoConfiguration 组件,其中配置文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

EurekaServerAutoConfiguration -> EurekaServerInitializerConfiguration + EurekaServerConfigBean

EurekaServerConfigBean 其实就是配置类,自动关联 eureka.server 前缀的配置文件

EurekaServerInitializerConfiguration 实现了 SmartLifecycle 接口,Spring 在 finishRefresh() -> getLifecycleProcessor().onRefresh() 方法中会去调用 start() 方法

2.3 定时清理 client 信息

2.3.1 流程分析

定时清理 client 信息

2.3.2 核心源码剖析
1. com.netflix.eureka.registry.AbstractInstanceRegistry#postInit
protected void postInit() {
    // 启动 renewsLastMin 定时器,即更新 Renews(last min)
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    evictionTaskRef.set(new EvictionTask());
    // 启动 evictionTimer 定时器,清理注册中心信息
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),
            serverConfig.getEvictionIntervalTimerInMs());
}
2. com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask#getCompensationTimeMs
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;
}
3. com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)
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();
                // 判断依据:是否过期,当前 holder 是否为空
                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);
        // 洗牌清除客户端数量,避免每次清除的都是前面的 client 信息
        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);
            // 清理工作,即之前分析的 cancel() 服务下线操作
            internalCancel(appName, id, false);
        }
    }
}
4. com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#isLeaseExpirationEnabled
public boolean isLeaseExpirationEnabled() {
    // 如果禁用了自我保护机制,直接返回
    if (!isSelfPreservationModeEnabled()) {
        // The self preservation mode is disabled, hence allowing the instances to expire.
        return true;
    }
    // 当 renewsLastMin (实际在最后一分钟收到的 client 端发送的续约的数量) < numberOfRenewsPerMinThreshold(期待每分钟收到 client 端发送的续约总数) 时,触发自动保护机制
    return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章