Eureka 服务实现细节


一、Eureka sever 实现细节
来源:《深度剖析服务发现组件Netflix Eureka》https://zhuanlan.zhihu.com/p/24829766

一、Eureka sever 实现细节

注册的服务列表保存在一个嵌套的hash map中:

  • 第一层hash map的key是app name,也就是应用名字

  • 第二层hash map的key是instance name,也就是实例名字

    Hash map定义如下:

在这里插入图片描述

以此为例:
在这里插入图片描述
在这里插入图片描述

app name为Consumer-client-dev,instance name 为172.20.10.2:consumer-client-dev:8202

一、服务注册Register

  1. ApplicationResource类接收Http服务请求,调用PeerAwareInstanceRegistryImpl的register方法
  2. PeerAwareInstanceRegistryImpl完成服务注册后,调用replicateToPeers向其它Eureka Server节点(Peer)做状态同步(异步操作)
preview

a、ApplicationResource.java

//注册实例
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
  .......
       registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

b、PeerAwareInstanceRegistryImpl.java

/**
  向eureka server 注册实例
  isReplication:若来自其他副本eureka server节点复制注册,则为true,否则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);
   //把当前eureka操作,复制到其他eureka server副本节点
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
.........

    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 (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
              	//排除自己eureka server,防止向自己复制
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

二、服务续约 Renew

Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。主要是用来告诉Eureka Server Service Provider还活着,避免服务被剔除掉。首先更新自身状态,再同步到其它Peer。

当客户端进行服务注册之后,会有一个定时任务,默认是每30s 向Eureka Server端进行服务续约,也就是告诉Eureka Server 我这个客户端当前还活着,别给我从注册表中剔除了,服务续约其实就是心跳,如果是一段时间没有向Eureka Server 发送续约请求,就会被认为服务发生故障,故而从注册表中剔除。

preview

a、InstanceResource.java

/**
接收eureka client 客户端发过来的,服务续约请求
**/
@PUT
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
   //调用服务续约方法
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
    // Not found in the registry, immediately ask for a register
    //若未在注册表中找到,请立即要求注册
    if (!isSuccess) {
        logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
        return Response.status(Status.NOT_FOUND).build();
    }
    ....
}

b、PeerAwareInstanceRegistryImpl.java

/*
 * (non-Javadoc)
 *
 * @see com.netflix.eureka.registry.InstanceRegistry#renew(java.lang.String,
 * java.lang.String, long, boolean)
 */
public boolean renew(final String appName, final String id, final boolean isReplication) {
    //调用父类方法,进行续约,如果
  if (super.renew(appName, id, isReplication)) {
      	//把当前eureka操作,复制到其他eureka server副本节点
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

c、AbstractInstanceRegistry.java

/**
 * Marks the given instance of the given app name as renewed, and also marks whether it originated from
 * replication.
 */
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) {
      	//根据instance name 获取指定续租实例(id=“172.20.10.2:provider-client-dev:8102”)
        leaseToRenew = gMap.get(id);
    }
    if (leaseToRenew == null) {
        .....
    } else {
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // touchASGCache(instanceInfo.getASGName());
          	//获取覆盖状态
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
           .......
          	//若续租实例的状态与获得的获取覆盖状态不一致,则将实例状态设为覆盖状态
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
               ......
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
            }
        }
      // renew计数,自我保护机制会用到
        renewsLastMin.increment();
        leaseToRenew.renew();
      	/**
      	其实就是更新lastUpdateTimestamp 这个字段。
				将lastUpdateTimestamp 设置成 当前时间+ 过期间隔, 如果说一旦lastUpdateTimestamp 小于了当前时间,就说明这个实例							租约过期了,这个实例可能会被Eureka Server 后台线程从注册表中剔除。
      	public void renew() {
   					lastUpdateTimestamp = System.currentTimeMillis() + duration;
				}
      	**/
        return true;
    }
}

三、服务下线Cancel

Cancel(服务下线)一般在Service Provider shut down的时候调用,用来把自身的服务从Eureka Server中删除,以防客户端调用不存在的服务。

preview

a、InstanceResource.java#cancelLease()

//接收服务下线请求并处理
@DELETE
public Response cancelLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    try {
        boolean isSuccess = registry.cancel(app.getName(), id,
            "true".equals(isReplication));
			........
    } catch (Throwable e) {
        return Response.serverError().build();
    }

}

b、PeerAwareInstanceRegistryImpl.java

public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
  	//调用父类cancel(),处理服务下线
    if (super.cancel(appName, id, isReplication)) {
      //把当前eureka操作,复制到其他eureka server副本节点
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

c、AbstractInstanceRegistry.java

 @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) {
              // 根据实例id 移除对应的实例租约信息
                leaseToCancel = gMap.remove(id);
            }
          // 将变更信息 扔到最近删除队列中
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
          //从实例状态map中移除该实例的状态信息。
            InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
            ......
            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);
                  // 放入最近变更队列中
                    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                  //更新实例剔除时间  
                  	instanceInfo.setLastUpdatedTimestamp();
                    vip = instanceInfo.getVIPAddress();
                    svip = instanceInfo.getSecureVipAddress();
                }
              // 删除实例对应的缓存数据
                invalidateCache(appName, vip, svip);
                logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
            }
        } finally {
            read.unlock();
        }
			// 更新 需要发送心跳的客户端数量,只要为了更新自我保护机制触发阈值,这块涉及到Eureka Server 自我保护机制实现原理
        synchronized (lock) {
          //这个是与自我保护机制有关的
            if (this.expectedNumberOfClientsSendingRenews > 0) {
               // 每有一个的客户端下线,就会-1,表示未来要发送心跳的客户端-1
                // Since the client wants to cancel it, reduce the number of clients to send renews.
                this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
              // 更新下 自己能容忍的最少心跳数量  
              updateRenewsPerMinThreshold();
            }
        }

        return true;
    }

四、获取注册表 Fetch Registries

Fetch Registries由客户端调用,用来获取Eureka Server上注册的服务。为了提高性能,服务列表在Eureka Server会缓存一份,同时每30秒更新一次。

全量更新
ApplicationsResource#getContainers
增量更新
ApplicationsResource#getContainerDifferential
preview

a、ApplicationsResource.java

public Response getContainers(@PathParam("version") String version,
                              @HeaderParam(HEADER_ACCEPT) String acceptHeader,
                              @HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
                              @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
                              @Context UriInfo uriInfo,
                              @Nullable @QueryParam("regions") String regionsStr) {
    ......
      // 构建缓存键
    Key cacheKey = new Key(Key.EntityType.Application,
            ResponseCacheImpl.ALL_APPS,
            keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), regions
    );
    Response response;
  	//这里从responseCache当中获取了Applications的序列号结果直接返回了
  	// 返回不同的编码类型的数据,去缓存中取数据的方法基本一致
    if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {
        response = Response.ok(responseCache.getGZIP(cacheKey))
                .header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE)
                .header(HEADER_CONTENT_TYPE, returnMediaType)
                .build();
    } else {
        response = Response.ok(responseCache.get(cacheKey))
                .build();
    }
    CurrentRequestVersion.remove();
    return response;
}

responseCache来源是,在ApplicationsResource构造的时候从Registry中获取的

@Inject
ApplicationsResource(EurekaServerContext eurekaServer) {
    this.serverConfig = eurekaServer.getServerConfig();
    this.registry = eurekaServer.getRegistry();
    this.responseCache = registry.getResponseCache();
}

b、ResponseCacheImpl.java

//从缓存中读取数据。   
public byte[] getGZIP(Key key) {
        Value payload = getValue(key, shouldUseReadOnlyResponseCache);
        if (payload == null) {
            return null;
        }
        return payload.getGzipped();
    }
@VisibleForTesting
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            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;
    }

c、Key.java

这个对象中包含了缓存键,这个hashKey最后的结果就是类似于这样的:ApplicationALL_APPSJSONV2full

public Key(EntityType entityType, String entityName, KeyType type, Version v, EurekaAccept eurekaAccept, @Nullable String[] regions) {
    this.regions = regions;
    this.entityType = entityType;
    this.entityName = entityName;
    this.requestType = type;
    this.requestVersion = v;
    this.eurekaAccept = eurekaAccept;
    hashKey = this.entityType + this.entityName + (null != this.regions ? Arrays.toString(this.regions) : "")
            + requestType.name() + requestVersion.name() + this.eurekaAccept.name();
}

五、服务剔除Eviction

Eviction(失效服务剔除)用来定期(默认为每60秒)在Eureka Server检测失效的服务,检测标准就是超过一定时间没有Renew的服务。默认失效时间为90秒,也就是如果有服务超过90秒没有向Eureka Server发起Renew请求的话,就会被当做失效服务剔除掉。

失效时间可以通过eureka.instance.leaseExpirationDurationInSeconds进行配置,定期扫描时间可以通过eureka.server.evictionIntervalTimerInMs进行配置。

//注意:
// Eureka的服务剔除会因为Eureka的自我保护机制而受到影响,导致不会剔除掉已经认为下线的服务
preview img

a、EurekaBootStrap.java

Eureka Server端启用的时执行的EurekaBootStrap类中initEurekaServerContext方法找到了服务剔除任务的初始化。

protected void initEurekaServerContext() throws Exception {
   ..........
    // Copy registry from neighboring eureka node
    int registryCount = registry.syncUp();
  	//初始化我们的服务剔除任务
    registry.openForTraffic(applicationInfoManager, registryCount);

    // Register all monitoring statistics.
    EurekaMonitors.registerAllStats();
}

b.PeerAwareInstanceRegistryImpl.java

@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();
    if (count > 0) {
        this.peerInstancesTransferEmptyOnStartup = false;
    }
    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
    boolean isAws = Name.Amazon == selfName;
    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();
}

c.AbstractInstanceRegistry.java

protected void postInit() {
    renewsLastMin.start();
  	// 初始化 清理租约过期任务
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
  	//添加清理租约过期任务
    evictionTaskRef.set(new EvictionTask());
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),
            //配置 eureka.evictionIntervalTimerInMs ,清理租约过期任务执行频率,单位:毫秒。默认,60000 毫秒。
            serverConfig.getEvictionIntervalTimerInMs());
}
//过期逻辑
public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");
				//是否启用租约到期
        if (!isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
            return;
        }
				//获得所有过期租约集
        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);
                    }
                }
            }
        }
		  // 为了补偿GC暂停或本地时间漂移,我们需要使用当前注册表大小作为触发自我保护的基础。没有它,我们就会把整个注册表都抹掉。
        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);
            }
        }
    }

d.内部类EvictionTask.java

/* visible for testing */ //清理租约过期任务
	class EvictionTask extends TimerTask {

    private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
    @Override
    public void run() {
        try {
          //调用 #compensationTimeMs() 方法,获得补偿时间毫秒数。计算公式 = 当前时间 - 最后任务执行时间 - 任务执行频率。
            long compensationTimeMs = getCompensationTimeMs();
            logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
          // 清理过期租约逻辑 
          evict(compensationTimeMs);
        } catch (Throwable e) {
            logger.error("Could not run the evict task", e);
        }
    }
  	.......
  }

e.lease.java

/**
 *检查租约是否已过期
 * @param additionalLeaseMs 获得补偿时间毫秒数。计算公式 = 当前时间 - 最后任务执行时间 - 任务执行频率。
 	Eureka是通过lastUpdateTimestamp这个上次更新时间来判断我们的服务是否可用,lastUpdateTimestamp每次服务续约时会更新
 */
public boolean isExpired(long additionalLeaseMs) {
    return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
public void renew() {
       lastUpdateTimestamp = System.currentTimeMillis() + duration;

   }

六、服务发现

Eureka Server在启动后会调用EurekaClientConfig.getEurekaServerServiceUrls来获取所有的Peer节点,并且会定期更新。定期更新频率可以通过eureka.server.peerEurekaNodesUpdateIntervalMs配置。

preview

a.PeerEurekaNodes.java

PeerEurekaNodes创建一个定时任务, 定时执行updateP eerEurekaNodes()方法,更新peerEurekaNodes

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 {
          	// 初始化 集群节点信息
            updatePeerEurekaNodes(resolvePeerUrls());
          	//创建节点信息更新任务
            Runnable peersUpdateTask = new Runnable() {
                @Override
                public void run() {
                    try {
                      //更新对等Eureka节点
                        updatePeerEurekaNodes(resolvePeerUrls());
                    } catch (Throwable e) {
                        logger.error("Cannot update the replica Nodes", e);
                    }

                }
            };
          	//将节点信息更新任务,放入定时任务中,默认10分钟执行一次
            taskExecutor.scheduleWithFixedDelay(
                    peersUpdateTask,
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    TimeUnit.MILLISECONDS
            );
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        for (PeerEurekaNode node : peerEurekaNodes) {
            logger.info("Replica node URL:  {}", node.getServiceUrl());
        }
    }
.....
//解析对等节点urls  获取Server集群的所有serviceUrl,不包括自身
protected List<String> resolvePeerUrls() {
   // 获得 Eureka-Server 集群服务地址数组
        InstanceInfo myInfo = applicationInfoManager.getInfo();
        String zone = InstanceInfo.getZone(clientConfig.getAvailabilityZones(clientConfig.getRegion()), myInfo);
   		// 获取相同Region下的所有serviceUrl      
  		List<String> replicaUrls = EndpointUtils
                .getDiscoveryServiceUrls(clientConfig, zone, new EndpointUtils.InstanceInfoBasedUrlRandomizer(myInfo));

        int idx = 0;
        while (idx < replicaUrls.size()) {
            if (isThisMyUrl(replicaUrls.get(idx))) {
                replicaUrls.remove(idx);
            } else {
                idx++;
            }
        }
        return replicaUrls;
    }
....

b.EndpointUtils.java

/**

   * 获取所有eureka服务URL的列表
          */
         public static List<String> getDiscoveryServiceUrls(EurekaClientConfig clientConfig, String zone, ServiceUrlRandomizer randomizer) {
     .....
     return getServiceUrlsFromConfig(clientConfig, zone, clientConfig.shouldPreferSameZoneEureka());
         }
/**
描述一下整个过程:
    获取Region,若没有配置或找不到对应的Region,则使用默认值,一个微服务只能找到一个Region
    通过获取的Region获取可用的Zone数组,一Region可对应多个Zone,若获取Zone失败,则使用默认值
    在可用数组中查找当前配置的Zone实例,若找到则返回第一个匹配的下标,若没有找到则返回0表示默认值
    将与Zone匹配的已经配置好的可用的serviceUrls加入到orderedUrls中
**/
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
    List<String> orderedUrls = new ArrayList();
    //从配置寻找Region
    String region = getRegion(clientConfig);
    //根据Region寻找可用的Zone
    String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
    if (availZones == null || availZones.length == 0) {
    	//若是Zone为空则使用默认值
        availZones = new String[]{"default"};
    }
    logger.debug("The availability zone for the given region {} are {}", region, availZones);
    //根据配置的Zone来匹配获取的可用Zone数组,有则返回对应下标,无则返回0
    int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
    //根据客户端配置且已匹配了的Zone来查找Eureka服务端已经存在的defaultZone
    List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
    if (serviceUrls != null) {
        orderedUrls.addAll(serviceUrls);
    }
    //循环变量,这样是为了循环错开循环量myZoneOffset
    int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;
    //循环,因为可能客户端配置了多个Zone,同样的步骤
    while(currentOffset != myZoneOffset) {
        serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
        if (serviceUrls != null) {
            orderedUrls.addAll(serviceUrls);
        }
       if (currentOffset == availZones.length - 1) {
            currentOffset = 0;
        } else {
            ++currentOffset;
        }
    }

    if (orderedUrls.size() < 1) {
        throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
    } else {
        //返回最中可用的同Zone的服务端serviceUrls,即获取对应服务端的eureka.client.serviceUrls.defaultZone
        return orderedUrls;
    }
}

c.EurekaClientConfigBean.java

	@Override
	public List<String> getEurekaServerServiceUrls(String myZone) {
    //根据zoneName,获取
		String serviceUrls = this.serviceUrl.get(myZone);
		if (serviceUrls == null || serviceUrls.isEmpty()) {
			serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
		}
		if (!StringUtils.isEmpty(serviceUrls)) {
      //多个注册的serviceURL,使用逗号分割为数组
			final String[] serviceUrlsSplit = StringUtils
					.commaDelimitedListToStringArray(serviceUrls);
			List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length);
			for (String eurekaServiceUrl : serviceUrlsSplit) {
				if (!endsWithSlash(eurekaServiceUrl)) {
					eurekaServiceUrl += "/";
				}
				eurekaServiceUrls.add(eurekaServiceUrl.trim());
			}
			return eurekaServiceUrls;
		}

		return new ArrayList<>();
	}

a.PeerEurekaNodes.java

//更新对等Eureka节点
protected void updatePeerEurekaNodes(List<String> newPeerUrls) {
    if (newPeerUrls.isEmpty()) {
        logger.warn("The replica size seems to be empty. Check the route 53 DNS Registry");
        return;
    }

  //Eureka-Server 计算 删除的集群节点地址
      Set<String> toShutdown = new HashSet<>(peerEurekaNodeUrls);
    toShutdown.removeAll(newPeerUrls);
  // 计算 新增的集群节点地址
    Set<String> toAdd = new HashSet<>(newPeerUrls);
    toAdd.removeAll(peerEurekaNodeUrls);
if (toShutdown.isEmpty() && toAdd.isEmpty()) { // No change
    return;
}
	//关闭删除的集群节点
// Remove peers no long available
List<PeerEurekaNode> newNodeList = new ArrayList<>(peerEurekaNodes);
if (!toShutdown.isEmpty()) {
    logger.info("Removing no longer available peer nodes {}", toShutdown);
    int i = 0;
    while (i < newNodeList.size()) {
        PeerEurekaNode eurekaNode = newNodeList.get(i);
        if (toShutdown.contains(eurekaNode.getServiceUrl())) {
            newNodeList.remove(i);
            eurekaNode.shutDown();
        } else {
            i++;
        }
    }
}
	//新增创建集群节点
// Add new peers
if (!toAdd.isEmpty()) {
    logger.info("Adding new peer nodes {}", toAdd);
    for (String peerUrl : toAdd) {
        newNodeList.add(createPeerEurekaNode(peerUrl));
    }
}
this.peerEurekaNodes = newNodeList;
this.peerEurekaNodeUrls = new HashSet<>(newPeerUrls);
}

七、新增Eureka Server节点

简而言之就是启动时把自己当做是Service Consumer从其它Peer Eureka获取所有服务的注册信息。然后对每个服务,在自己这里执行Register,isReplication=true,从而完成初始化。

preview

a .PeerAwareInstanceRegistryImpl.java

public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;

    .......
      //获取其它Peer Eureka获取所有服务的注册信息
        Applications apps = eurekaClient.getApplications();
        for (Application app : apps.getRegisteredApplications()) {
            for (InstanceInfo instance : app.getInstances()) {
                try {
                    if (isRegisterable(instance)) {
                        register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        count++;
                    }
                } catch (Throwable t) {
                    logger.error("During DS init copy", t);
                }
            }
        }
    }
    return count;
}

二、Eureka Client (Service Provider) 实现细节

主要Service Provider的实现细节,主要就是Register、Renew、Cancel这3个操作。

img

一、Register

只需要在启动时和实例状态变化时调用Eureka Server的接口注册即可。需要注意的是,需要确保配置eureka.client.registerWithEureka=true。

preview

InstanceInfoReplicator.java

public void run() {
    try {
      //刷新当前本地instanceInfo
        discoveryClient.refreshInstanceInfo();

        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
          //Register with the eureka service by making the appropriate REST call.
          //通过rest 向eureka server 注册
            discoveryClient.register();
            instanceInfo.unsetIsDirty(dirtyTimestamp);
        }
    } catch (Throwable t) {
        logger.warn("There was a problem with the instance info replicator", t);
    } finally {
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

DiscoveryClient.java

/**
 * Register with the eureka service by making the appropriate REST call.
 	(eurekaTransport.registrationClient.register(instanceInfo);)
 */
boolean register() throws Throwable {
    EurekaHttpResponse<Void> httpResponse;
    try {
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        .......
    }
  .....
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

EurekaHttpClientDecorator.java

@Override
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
    return execute(new RequestExecutor<Void>() {
        @Override
        public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
            return delegate.register(info);
        }
		....
    });
}

AbstractJerseyEurekaHttpClient.java

//向eureka server服务端发送注册请求
public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;
    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        response = resourceBuilder
                .header("Accept-Encoding", "gzip")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON)
                .post(ClientResponse.class, info);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
       ......
    }
}

二、Renew

Renew操作会在Service Provider端定期发起,用来通知Eureka Server自己还活着。 这里有两个比较重要的配置需要注意一下:

  1. instance.leaseRenewalIntervalInSeconds

Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。

  1. instance.leaseExpirationDurationInSeconds

服务失效时间。默认是90秒,也就是如果Eureka Server在90秒内没有接收到来自Service Provider的Renew操作,就会把Service Provider剔除。

preview

三、Cancel

在Service Provider服务shut down的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务。

逻辑本身比较简单,通过对方法标记@PreDestroy,从而在服务shut down的时候会被触发。

img

四、Discovered

这里大家疑问又来了,Service Provider是怎么知道Eureka Server的地址呢?

其实这部分的主体逻辑和3.3.7 How Peer Nodes are Discovered几乎是一样的。

也是默认从配置文件读取,如果需要更灵活的控制,可以通过override getEurekaServerServiceUrls方法来提供自己的实现。定期更新频率可以通过eureka.client.eurekaServiceUrlPollIntervalSeconds配置。

img

三、Eureka Client (Service Consumer) 实现细节

Service Consumer这块的实现相对就简单一些,因为它只涉及到从Eureka Server获取服务列表和更新服务列表。

一、获取注册表

Service Consumer在启动时会从Eureka Server获取所有服务列表,并在本地缓存。需要注意的是,需要确保配置eureka.client.shouldFetchRegistry=true。

img

二、更新注册表

由于在本地有一份缓存,所以需要定期更新,定期更新频率可以通过eureka.client.registryFetchIntervalSeconds配置。

img
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值