Spring Cloud Eureka 源碼分析(一) 服務端啓動過程

一. 前言

    咱們在使用Spring Cloud Eureka服務發現功能的時候,簡單的引入maven依賴,且在項目入口類根據服務端和客戶端加上不一樣的註解就能夠了;html

可是,這些功能是如何實現的呢?java

    咱們在下面進行一下分析,服務發現分爲客戶端和服務端,咱們分開來看,根據Spring Cloud 的版本不一樣,類名略有不一樣,可是實現邏輯是一致的,因此請讀者注意這一點;node

    當前使用版本: <version>Dalston.RC1</version>spring

    水平有限,異議之處請留言討論,相互學習;json

二. Eureka註冊中心

2.1.啓動過程分析

2.1.1 @EnableEurekaServer 

            使用 @EnableEurekaServer  來標記啓動註冊中心功能;bootstrap

            @Enable*******, 這種格式的註解有不少,是Spring Boot約定的開啓某些功能的方式,從而避免一些配置的繁瑣,可點擊查看用法;tomcat

            查看該註解源碼:網絡

@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {

}

            這裏面有2個重要的操做:app

             1. 註解@EnableDiscoveryClient:dom

                        這個註解是開啓SpringCloud服務發現客戶端的註解,之因此這裏沒有說是開啓Eureka客戶端,是由於開啓Eureka客戶端的註解是

                @EnableEurekaClient,因爲SpringCloud在服務發現組件上不僅支持Eureka,還支持例如Alibaba的Dubbo等,而前者纔是       

                SpringCloud開啓服務發現的註解;假若SpringCloud集成Dubbo,也許針對此的註解就是@EnableDubboClient了;

            2. @Import(EurekaServerMarkerConfiguration.class)

                 導入了配置類EurekaServerMarkerConfiguration,該配置類中實例化了一個Marker的bean,這個bean在此處還不知道有何做用

                打開  EurekaServerAutoConfiguration 這個類的實例化條件就豁然開朗了;

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {...代碼省略先...}

            如今咱們展開來講這個Eureka服務端的自動配置類;

            1. 這個配置類實例化的前提條件是上下文中存在 EurekaServerMarkerConfiguration.Marker 這個bean,解釋了上面的問題;      

            2. 經過@EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class })導入了兩個配置類;

            1) EurekaDashboardProperties.class

            這個配置類用於控制eureka面板的啓動與否及打開的路徑;

屬性參數 釋義   default value
eureka.dashboard.path eureka面板的路徑

The path to the Eureka dashboard (relative to the servlet path). Defaults to "/".

/
eureka.dashboard.enabled 是否開啓eureka面板

Flag to enable the Eureka dashboard. Default true.

  true

             2)  InstanceRegistryProperties.class

屬性參數 釋義   default value
eureka.server.expectedNumberOfRenewsPerMin 每分鐘指望續約的數量      1
eureka.server.defaultOpenForTrafficCount 默認打開的通訊數量       1

                4. @Import(EurekaServerInitializerConfiguration.class) 

                      這裏有導入了一個配置類,// TODO

                5.@PropertySource("classpath:/eureka/server.properties")

                      咱們打開這個配置文件看一下:spring.http.encoding.force=false,應該是控制字符集編碼的;

2.1.2  配置類EurekaServerAutoConfiguration

           1. 首先實例化一個bean,目前沒研究    TODO

@Bean
	public HasFeatures eurekaServerFeature() {
		return HasFeatures.namedFeature("Eureka Server",
				EurekaServerAutoConfiguration.class);
	}

          2. 在靜態內部類中有條件的實例化了eureka服務端配置,配置類爲 EurekaServerConfig,詳細參數說明在另外一篇文章中;

@Configuration
	protected static class EurekaServerConfigBeanConfiguration {
		@Bean
		@ConditionalOnMissingBean
		public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
			EurekaServerConfigBean server = new EurekaServerConfigBean();
			if (clientConfig.shouldRegisterWithEureka()) {
				// Set a sensible default if we are supposed to replicate
				server.setRegistrySyncRetries(5);
			}
			return server;
		}
	}

         3. 實例化了進入eureka控制面板的Controller類:EurekaController:

@Bean
	@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
	public EurekaController eurekaController() {
		return new EurekaController(this.applicationInfoManager);
	}

            打開EurekaController這個類咱們能夠看到此類就是一個普通的請求入口類,用於展現eureka面板;

          4.實例化了eureka多個服務維持節點同步的bean;

@Bean
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		this.eurekaClient.getApplications(); // force initialization
		return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
				serverCodecs, this.eurekaClient,
				this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
				this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
	}

         5. 每一個eureka服務節點的生命週期管理

@Bean
	@ConditionalOnMissingBean
	public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
			ServerCodecs serverCodecs) {
		return new PeerEurekaNodes(registry, this.eurekaServerConfig,
				this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
	}

        6. eureka服務的Context維護,具體這裏先不解釋了;

@Bean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}

       7.  經過tomcat管理eureka的生命週期;

@Bean
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}

2.1.3 Eureka服務的啓動

             1. 在EurekaServerBootstrap類中咱們看到了初始化方法:   

public void contextInitialized(ServletContext context) {
		try {

            //看源碼可知這裏主要初始化服務環境,配置信息;
			initEurekaEnvironment();
           
            //初始化了eureka服務端的上下文
			initEurekaServerContext();

			context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
		}
		catch (Throwable e) {
			log.error("Cannot bootstrap eureka server :", e);
			throw new RuntimeException("Cannot bootstrap eureka server :", e);
		}
	}

         在這個方法中咱們看到了初始化eureka-server環境配置及eureka-server上下文的操做,那麼這個方法應該在一個地方有調用,經過查找調用發現:

@Configuration
@CommonsLog
public class EurekaServerInitializerConfiguration
		implements ServletContextAware, SmartLifecycle, Ordered {

   @Override
	public void start() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					//TODO: is this class even needed now?
					 
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
					log.info("Started Eureka Server");

					publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
					EurekaServerInitializerConfiguration.this.running = true;
					publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
				}
				catch (Exception ex) {
					// Help!
					log.error("Could not initialize Eureka servlet context", ex);
				}
			}
		}).start();
	}


    @Override
	public void stop() {
		this.running = false;
		eurekaServerBootstrap.contextDestroyed(this.servletContext);
	}

 ..... 部分代碼省略.....

}

           這個類在頂層實現了 org.springframework.context.Lifecycle 接口,經過tomcat管理生命週期;

          2. 經過分析上面實例化bean,咱們能夠看到eureka服務是經過tomcat調用其聲明週期方法來啓動的;

              那麼在啓動eureka服務有哪些操做呢?咱們來深刻跟進下源碼.

protected void initEurekaServerContext() throws Exception {
		// For backward compatibility
		JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);
		XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);

		if (isAws(this.applicationInfoManager.getInfo())) {
			this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
					this.eurekaClientConfig, this.registry, this.applicationInfoManager);
			this.awsBinder.start();
		}

		EurekaServerContextHolder.initialize(this.serverContext);

		log.info("Initialized server context");

		// Copy registry from neighboring eureka node
		int registryCount = this.registry.syncUp();
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

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

           在初始化eureka服務端initEurekaServerContext()方法中,主要作了初始化server上下文,同步了其餘節點的信息,啓動了剔除不可用eureka客戶端的定時任務;

 

 

三. 接收Eureka客戶端請求        

       咱們都知道,Eureka註冊中心經過接收客戶端的註冊、續約等Http請求來維持服務實例在註冊中心的狀態,那麼註冊中心確定有一個端口供客戶端訪問,他們在哪裏呢?

3.1 接收註冊信息

3.1.1. 註冊入口

        咱們經過重啓註冊中心查看輸出日誌,在跟蹤代碼調用,找到了註冊的入口;

2018-08-23 11:11:36.029  INFO 202760 --- [nio-8761-exec-6] c.n.e.registry.AbstractInstanceRegistry  : Registered instance APPLICATIONCLIENT/PC-HEPENGFEI.ppmoney.com:applicationClient:9001 with status UP

       找到該處日誌打印,跟蹤到了 com.netflix.eureka.resources.ApplicationResource#addInstance,

/**
     * Registers information about a particular instance for an
     * {@link com.netflix.discovery.shared.Application}.
     *
     * @param info
     *            {@link InstanceInfo} information of the instance.
     * @param isReplication
     *            a header parameter containing information whether this is
     *            replicated from other nodes.
     */
    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);

       ....省略部分代碼....

        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

            首先看一下方法入參,由isReplication解釋可知,該方法還接收其餘節點同步註冊列表,所以這個入口有兩個做用;

             1. 接收eureka客戶端的註冊請求,完成服務實例向註冊中心的註冊;

             2. 接收其餘註冊中心節點的同步信息.完成節點間服務列表的同步工做;

3.1.2 註冊過程

         咱們跟蹤方法執行:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register

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

        在這裏更新了最後一次續約間隔多久剔除的參數;在註冊以後,有一個向其餘節點同步的操做replicateToPeers,這個咱們在3.1.3講解;

        再繼續跟蹤:com.netflix.eureka.registry.AbstractInstanceRegistry#register:

        在這個方法第一步是先上了讀鎖:ReentrantReadWriteLock,接着是註冊操做:

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

    從這段代碼,咱們提取出一個變量registry,這是一個map,保存了服務實例信息,也就是說註冊信息所有保存在這個map中,他的聲明:

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

   很容易知道key爲服務實例的appName,也是們eureka客戶端配置的spring.application.name參數;

         經過debug咱們發現: 

                registry的value也是一個map結構,其key是spring.application.name的值與host和端口信息組合,value爲Lease<InstanceInfo>,其包含了服務實例信息及註冊續約等時間戳信息,用於配合維護狀態;

               這樣存儲咱們能夠很容易理解,外層key是對外暴露服務的,value爲服務的集羣,內層map的key爲集羣單個實例的信息,value爲實例;

         在這裏我發現一個問題,就是當一個新的服務註冊實例的時候:

當咱們向下debug一行的時候發現問題來了:

      這個問題先拋出來,咱們先接着分析,以後再討論這個問題;

     如上的操做,咱們就算註冊了"一個服務實例"了,但實際上,這裏應該只是註冊了一個服務集羣而已,key爲集羣的同一應用名,value爲集羣map;

    再看接下來的操做: 

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

               //若是本次註冊時間小於註冊中心保存的該實例註冊最近一次註冊時間,說明以前已經註冊成功了,那麼就用已存在的替換請求進來的;
                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();
                }

 再接着看com.netflix.eureka.registry.AbstractInstanceRegistry#register

Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease);

       這裏保存了服務實例註冊的時間ServiceUpTimestamp,而且將實例塞入對應集羣的map中,在這裏纔是註冊服務的終極地方,實現了一個服務實例的註冊;

3.1.3 同步到註冊中心其餘節點

       在3.1.2咱們提到了註冊以後有個向替他節點同步的方法replicateToPeers,在這裏咱們深刻一下;

private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this will create a poison replication
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

在這個方法中,第二個foreach中的if判斷,若是目標節點和本機的hostName一致則不會同步,所以咱們在應用的時候Eureka的集羣高可用---SpringCloud(二)配置了不一樣的hostname;

      以下圖:

     具體方法在這裏:

/**
     * Replicates all instance changes to peer eureka nodes except for
     * replication traffic to this node.
     *
     */
    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);
        }
    }

能夠看到,Action指定了要同步到其餘節點信息的類型,如實例的註冊,續約等,再根據不一樣動做執行不一樣的方法,全部的動做都經過這裏分發同步到其餘節點;

咱們查看該方法(com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers)調用也得出此結論;

最後由batchingDispatcher.process()執行任務;expiryTime爲過時時間,與實例最後一次續約後剔除時長相同;

public interface TaskDispatcher<ID, T> {
    
    void process(ID id, T task, long expiryTime);

    void shutdown();
}

咱們再來看一下是如何得知要同步的節點,就是如何過去存在的其餘節點的;

在com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers方法中有peerEurekaNodes變量,咱們看一下這個變量是如何賦值的;咱們查看調用:

initialize方法的入參來自於成員變量,成員變量賦值銅鼓構造方法,再看構造方法調用:

@Bean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}

在這裏bean初始化時賦值的;咱們在依次根據該bean實例化所依賴的bean,最終能夠看到來自於配置文件;

@Bean
	public ServerCodecs serverCodecs() {
		return new CloudServerCodecs(this.eurekaServerConfig);
	}

所以咱們能夠得出一個結論,當註冊中心集羣時,每一個節點只會向其配置文件所配置的其餘節點同步信息,且是單向的;

3.2 續約

3.2.1 續約入口

       單純的直接找這個入口比較很差找,咱們經過方法調用,還記得同步服務實例信息到其餘節點的方法麼com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers,還有這個調用,從這裏能夠看到5個動做;

咱們經過跟蹤方法調用找到了續約的接收客戶端請求的入口:com.netflix.eureka.resources.InstanceResource#renewLease

/**
     * A put request for renewing lease from a client instance.
     *
     * @param isReplication
     *            a header parameter containing information whether this is
     *            replicated from other nodes.
     * @param overriddenStatus
     *            overridden status if any.
     * @param status
     *            the {@link InstanceStatus} of the instance.
     * @param lastDirtyTimestamp
     *            last timestamp when this instance information was updated.
     * @return response indicating whether the operation was a success or
     *         failure.
     */
    @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();
        }
        // Check if we need to sync based on dirty time stamp, the client
        // instance might have changed some value
        Response response = null;
        if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
            response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
            // Store the overridden status since the validation found out the node that replicates wins
            if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
                    && (overriddenStatus != null)
                    && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
                    && isFromReplicaNode) {
                registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
            }
        } else {
            response = Response.ok().build();
        }
        logger.debug("Found (Renew): {} - {}; reply status={}" + app.getName(), id, response.getStatus());
        return response;
    }

3.2.2 續約renew到底作了什麼

       咱們跟蹤代碼到核心方法:com.netflix.eureka.registry.AbstractInstanceRegistry#renew

Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            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;
        }

        先看這段代碼,從註冊列表中獲取對應實例信息,準確的說是Lease對象,它包含實例信息及時間戳等,若是獲取爲null,則返回false,這是客戶端因爲接收到的返回碼404,從新發起註冊;

        接下來執行正常續約else分支:

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());
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                    Object[] args = {
                            instanceInfo.getStatus().name(),
                            instanceInfo.getOverriddenStatus().name(),
                            instanceInfo.getId()
                    };
                    logger.info(
                            "The instance status {} is different from overridden instance status {} for instance {}. "
                                    + "Hence setting the status to overridden status", args);
                    instanceInfo.setStatus(overriddenInstanceStatus);
                }
            }
            renewsLastMin.increment();
            leaseToRenew.renew();
            return true;
        }

在這個分支中首先判斷了實例的狀態,若狀態UNKNOW則返回false,從新發起註冊;

重點看一下執行續約的方法leaseToRenew.renew();究竟是如何實現續約的呢?咱們展開這個方法:

/**
     * Renew the lease, use renewal duration if it was specified by the
     * associated {@link T} during registration, otherwise default duration is
     * {@link #DEFAULT_DURATION_IN_SECS}.
     */
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;

    }

很簡單的操做,更新了上一次更新的時間戳字段爲當前時間+持續時間(duration,就是剔除時最後一次續約所間隔時間)

3.3 剔除,服務下線(EvictionTask)

      經過以前咱們分析的方法,很容易的找到剔除不可用實例的任務類EvictionTask,那麼咱們直接進入核心方法:com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)

3.3.1 註冊中心的自我保護

首先咱們看到這個方法,

if (!isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
            return;
        }

         看日誌能夠看出當前租戶過時不可用,也就是說不會由於有實例續約過時而被剔除,那麼咱們看一下是取決於哪些條件,點進去看一下:

@Override
    public boolean isSelfPreservationModeEnabled() {
        return serverConfig.shouldEnableSelfPreservation();
    }

        一是來自於配置文件 eureka.server.enableSelfPreservation自我保護模式是否開啓,自我保護模式,當出現出現網絡分區、eureka在短期內丟失過多客戶端時,會進入自我保護模式,即一個服務長時間沒有發送心跳,eureka也不會將其刪除,默認爲true.

       若是該值配置爲false,則永遠不會進入保護模式,那麼一旦遇到網絡波動,會有大量的服務實例被剔除,可是他們卻都是可用的,這是很危險的;若是是內網則另當別論了;

       二是經過閾值控制;

       若是Eureka Server最近1分鐘收到renew的次數小於閾值(即預期的最小值),則會觸發自我保護模式,此時Eureka Server此時會認爲這是網絡問題,它不會註銷任何過時的實例。等到最近收到renew的次數大於閾值後,則Eureka Server退出自我保護模式。

自我保護模式閾值計算:

  • 每一個instance的預期心跳數目 = 60/每一個instance的心跳間隔秒數
  • 閾值 = 全部註冊到服務的instance的數量的預期心跳之和 *自我保護係數

以上的參數均可配置的:

  • instance的心跳間隔秒數:eureka.instance.lease-renewal-interval-in-seconds
  • 自我保護係數:eureka.server.renewal-percent-threshold

3.3.2 隨機剔除服務實例

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

           這段代碼遍歷服務列表,並判斷是否過時,若過時將其add到expiredLeases中;

           接下來註冊中心有執行了一個保護的操做:根據本地服務的數量從新計算了續約閾值,而後與註冊的服務數量作差做爲本次剔除服務數量的最大值,再對比放在過時待剔除服務列表中的數量,取最小值做爲本次剔除過時服務的數量.該計算過程爲了不某些緣由使得該註冊中心節點服務實例被所有剔除;

          若計算後最終要剔除的服務數量小於待剔除服務列表中的數量,則採起隨機方式剔除;

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

           剔除後通知到其餘註冊中心節點;

 

四.結語

       講到這裏呢,基本上註冊中心的主要內容就差很少了,若是有什麼疑問或者每講到的,歡迎留言,我會及時補充;

相關文章
相關標籤/搜索