Marco's Java【Eureka篇之Eureka集羣搭建及源碼解析】

Spring Cloud Eureka 簡介

說到註冊中心,大家很容易會聯想到Zookeeper,那麼今天的主角Eureka同Zookeeper一樣都是註冊中心。

Eureka 來源於古希臘詞彙,意爲“發現了”。在軟件領域, Eureka 是 Netflix
在線影片公司開源的一個服務註冊與發現的組件,和其他Netflix 公司的服務組件(例如負載均衡、熔斷器、網關等) 一起,被Spring Cloud 社區整合爲Spring Cloud Netflix 模塊。 Eureka 是Netflix 貢獻給Spring
Cloud的一個框架!Netflix 給Spring Cloud 貢獻了很多框架。


Spring Cloud Eureka 和Zookeeper的區別

關於Eureka 和Zookeeper的區別,我們着重從分佈式特徵CAP層面來分析

Consistance : 數據的一致性 (A,B,C裏面的數據是一致的)
Zookeeper 注重數據的一致性。Eureka 不是很注重數據的一致性!
Available: 服務的可用性
在Zookeeper裏面,若主機掛了,則Zookeeper集羣整體不對外提供服務了,需要選一個新的主機出來(30-120s)才能繼續對外提供服務(選舉期間不對外提供服務)!Eureka 注重服務的可用性,當Eureka集羣只有一臺活着,它就能對外提供服務.
Partition Tolerence:分區的容錯性 (在集羣裏面的集羣,因爲網絡原因,機房的原因,可能導致數據不會里面同步),這是分佈式產品必須需要實現的特性!

當然還有一些其他的區別,比如說,Zookeeper使用比較靈活,可作爲Dubbo的註冊中心,實現分佈式服務的管理,可作爲配置管理中心,統一管理Solr集羣的配置文件,利用臨時節點剔除的原理管理ActiveMQ集羣,還有諸如分佈式鎖的應用,而Eureka作爲Spring Cloud的組件,則着重作爲服務的註冊中心,像一張網,將Cloud中的組件連在一起,從而讓各個組件發揮各自的功能。Eureka提供自我保護和自身集羣監控功能,結合緩存功能實現Eureka的高可用!

總體來說Zookeeper 注重數據的一致性(CP),而Eureka 注重服務的可用性(AP)


Eureka高可用的原理

在這裏插入圖片描述
Eureka的客戶端在向某個Eureka註冊時,或者發現連接失敗時,則會自動切換至其它節點,只要有一臺Eureka還在,就能保證註冊服務可用(保證可用性),只不過查到的信息可能不是最新的(不保證強一致性)。除此之外,Eureka還有一種自我保護機制,如果在15分鐘內超過85%的節點都沒有正常的心跳,那麼Eureka就認爲客戶端與註冊中心出現了網絡故障,此時會出現以下幾種情況:
1)Eureka不再從註冊列表中移除因爲長時間沒收到心跳而應該過期的服務
2)Eureka仍然能夠接受新服務的註冊和查詢請求,但是不會被同步到其它節點上(即保證當前節點依然可用)
3)當網絡穩定時,當前實例新的註冊信息會被同步到其它節點中

因此, Eureka可以很好的應對因網絡故障導致部分節點失去聯繫的情況,而不會像zookeeper那樣使整個註冊服務癱瘓


Spring Cloud 其他註冊中心的選擇

Eureka 修改了授權協議,之前Eureka Apache2.0 的協議,後續的Eureka ,可能使用其他的授權協議。
Spring Cloud 還有別的註冊中心 Consul,阿里巴巴提供Nacos 都能作爲註冊中心,我們的選擇還是很多。
但是我們學習還是選擇Eureka ,因爲它的成熟度相對來說很高。
在這裏插入圖片描述


Eureka相關配置

Eureka Server 服務端配置

eureka-server.jar包導入

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

application.yml配置

server:
  port: 8080
spring:
  application:
    name: eureka-client-a
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/,http://peer3:8763/eureka/
  instance: 
    instance-id: ${spring.application.name}:${server.port}
    prefer-ip-address: true

這裏我說明下,爲什麼Eureka-Server需要配置eureka.client.serviceUrl,因爲Eureka-Server不僅提供讓別人註冊的功能,它也能註冊到別人裏面。
在這裏插入圖片描述
在這裏插入圖片描述
並且根據源碼分析,Eureka-Server在啓動時,還會自己註冊自己。通過這種相互註冊,相互發現的機制,可以輕易的完成服務端集羣的搭建。

補充

# 響應的緩存配置(爲了eureka-server能快速的響應client)
response-cache-update-interval-ms: 3000
responseCacheAutoExpirationInSeconds: 180
# 定期刪除沒有需要的instance(lastupdatetime)
evictionIntervalTimerInMs: 3000
# server的自我保護模式
enableSelfPreservation: true # 本地調試時可fasle關閉。但生產建議打開,可防止因網絡不穩定等原因導致誤剔除服務。
renewalPercentThreshold: 0.85 # 默認85%
Eureka Client 客戶端配置

eureka-client.jar包導入

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml配置

server:
  port: 8761
spring:
  application:
    name: eureka-server
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    serviceUrl:
      defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/,http://peer3:8763/eureka/
    preferIpAddress: true # 默認false。應該始終設置爲true。如果基於Docker等容器的部署,容器會生成一個隨機的主機名,此時DNS不存在該名,無法解析 - John Carnell

當Eureka-Serve兩兩之間相互註冊之後,Client端通過註冊整個集羣的Server的方式,形成一個高可用的集羣迴路
在這裏插入圖片描述
運行成功之後顯示如下,三個Eureka Server搭建成一個集羣,當Client註冊任意一個Server之後,其他的Server都會複製該數據,實現數據的共享。
在這裏插入圖片描述
補充

instance:
    lease-renewal-interval-in-seconds: 30 # 30s client 向server發一個續簽請求,代表我還活着 
    lease-expiration-duration-in-seconds: 90 # client 多久沒有向server發請求,server 會清除它
    instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}
# 詳見EurekaClientConfigBean(實現EurekaClientConfig)
client:
  # 是否啓用eureka客戶端。默認true
  enabled: true # 本地調試時,若不想啓動eureka,可配置false即可,而不需要註釋掉@EnableDiscoveryClient這麼麻煩。
  registerWithEureka: true # 默認true,因此也可省略。
  fetchRegistry: true # 默認true,此處可不配置。
  registry-fetch-interval-seconds: 30 # 如果想eureka server剔除服務後儘快在client體現,我覺得可縮短此時間。

Eureka概念的理解

服務的註冊
當項目一啓動,向eureka-server 發送自己的元數據,(運行的ip,port,健康的監控數據)eureka-server在自己內部保存這些元數據。
服務的續約
Client項目啓動成功了,也向eureka-server 註冊成功。 項目還會定時的去eureka-server 彙報自己。代表我還活着。
服務的下線
當項目關閉時,項目會給eureka-server 報告自己,表示自己要下線了!
服務的剔除
當服務沒有向 eureka-server 彙報自己的狀態超過一段時間,eureka-server則認爲它掛了,並會把它剔除掉!

接下來我們會圍繞上面的幾個特徵,對Eureka的源碼進行分析。


Eureka源碼解析

Eureka-Server對外會提供Restful服務(http服務 + 特定的請求方式url + 特定的url地址),只要利用這些restful服務,就可以實現項目中的服務的註冊和發現。因爲http是跨平臺的,不管客戶端是什麼語言,只要能發起http請求,就可以自己實現服務的註冊和發現!

服務註冊源碼解析

首先,在eureka-client-1.9.13.jar源碼包中找到DiscoveryClient類
在這裏插入圖片描述
在DiscoveryClient有個註冊方法register(instanceInfo)
在這裏插入圖片描述
instanceInfo 爲實例信息,底層使用AbstractJerseyEurekaHttpClient類中的register()方法發起一個post請求,並返回響應的結果httpResponse,如果響應的結果的狀態爲NO_CONTENT無內容,則表示成功!

/**
 * Register with the eureka service by making the appropriate REST call.
 */
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    try {
    	// 註冊實例信息,實際上是向server端發送一個http註冊請求
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

先經過EurekaHttpClientDecorator,並執行register(final InstanceInfo info),當然這個方法也是一個跳板,實際上還是執行delegate.register(info),delegate是一個EurekaHttpClient代理對象,本質上是調用AbstractJerseyEurekaHttpClient中的register方法

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

          @Override
          public RequestType getRequestType() {
              return RequestType.Register;
          }
      });
  }

我們再接着跟進AbstractJerseyEurekaHttpClient中的註冊方法,通過urlPath結構不難看出這是一個restful風格的url地址,Eureka通過構建模式構建一個Builder用於發送http請求,底層實際上是在此處發起post請求。
然後Server端會返回一個結果對象response,在Eureka中默認狀態碼204爲註冊成功!

@Override
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);
        // 底層實際上是在此處發起http請求(post)
        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 {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                    response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

接下來我們來看服務端的InstanceRegistry類,它是負責接收客戶端發過來的http請求,並返回響應結果的。
在這裏插入圖片描述

@Override
public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
	handleRegistration(info, leaseDuration, isReplication);
	super.register(info, leaseDuration, isReplication);
}

@Override
public void register(final InstanceInfo info, final boolean isReplication) {
	handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
	super.register(info, isReplication);
}

register(info, leaseDuration, isReplication)意爲註冊自己,replicateToPeers()是將實例的info註冊到集羣中。在Server中使用一個雙層的ConcurrentMap集合ConcurrentMap<String,Map<String,Instance>>專門用來存儲註冊的服務的信息,第一個String爲服務的名稱,第二個String爲instanceId,Instance則包含實例的ip:port和狀態。(由於HashMap是線程不安全的,因此需要使用ConcurrentMap,保證線程的安全)
在這裏插入圖片描述

/**
 * Registers a new instance with a given duration.
 *
 * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
 */
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        // 通過服務的名稱在註冊中心registry中獲取一個實例的列表
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
        REGISTER.increment(isReplication);
        // 若實例的列表爲nul(第一次註冊)
        if (gMap == null) {
        	// 新建一個實例的列表,通過map集合封裝實例的信息
            final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
            // 將新建的gNewMap放在註冊中心中
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        // gMap就是該服務的實例,registrant.getId()爲實例的id
        Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
        // Retain the last dirty timestamp without overwriting it, if there is already a lease
        // 如果existingLease 不爲null,代表有註冊
        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.
            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");
        }
        // 如果服務沒有註冊,則執行註冊,將該實例放入到gMap中
        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() + ")"));
        }
        // This is where the initial state transfer of overridden status happens
        if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
            logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                            + "overrides", registrant.getOverriddenStatus(), registrant.getId());
            if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
            }
        }
        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
        if (InstanceStatus.UP.equals(registrant.getStatus())) {
            lease.serviceUp();
        }
        registrant.setActionType(ActionType.ADDED);
        recentlyChangedQueue.add(new RecentlyChangedItem(lease));
        registrant.setLastUpdatedTimestamp();
        invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
        logger.info("Registered instance {}/{} with status {} (replication={})",
                registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
    } finally {
        read.unlock();
    }
}
關於服務註冊的總結

1)通過DiscoveryClient(eureka-client)中的register方法完成服務的註冊
2)Eureka Client 本質上是使用AbstractJerseyEurekaHttpClient中的register方法,調用http的post請求,向Server發起註冊的請求。
3)Eureka Server 中的InstanceRegistry中的register方法用於實際註冊Eureka Client 客戶端,客戶端的信息都被保存在一個ConcurrentMap中。


服務的續約

由Eureka Client 向Eureka Server 發起續約請求(renew),切入點還是DiscoveryClient,其本質上是通過定時任務每隔30s向服務端發送心跳續約。

/**
 * Renew with the eureka service by making the appropriate REST call
 */
boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    try {
    	// 發送心跳到服務端,包含客戶端基本信息
        httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
        logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            REREGISTER_COUNTER.increment();
            logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
            long timestamp = instanceInfo.setIsDirtyWithTime();
            boolean success = register();
            if (success) {
                instanceInfo.unsetIsDirty(timestamp);
            }
            return success;
        }
        return httpResponse.getStatusCode() == Status.OK.getStatusCode();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
        return false;
    }
}

發送心跳的代碼如下,將需要續約的客戶端的信息以http的形式發送給服務端

@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;
    try {
        WebResource webResource = jerseyClient.resource(serviceUrl)
                .path(urlPath)
                .queryParam("status", info.getStatus().toString())
                .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
        if (overriddenStatus != null) {
            webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
        }
        Builder requestBuilder = webResource.getRequestBuilder();
        addExtraHeaders(requestBuilder);
        response = requestBuilder.put(ClientResponse.class);
        EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
        if (response.hasEntity()) {
            eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
        }
        return eurekaResponseBuilder.build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

同樣在服務端也有一個renew()方法,切入點依然是InstanceRegistry
在這裏插入圖片描述
該renew方法本質上是調用InstanceRegistry的父類PeerAwareInstanceRegistryImpl的方法,而PeerAwareInstanceRegistryImpl中的renew又是調用AbstractInstanceRegistry中得renew方法

public boolean renew(String appName, String id, boolean isReplication) {
	RENEW.increment(isReplication);
	// 獲取該服務的實例列表(Map)
	Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
	Lease<InstanceInfo> leaseToRenew = null;
	if (gMap != null) {
		// 通過實例的id 得到要續約的實例
		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) {
			// 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)) {
				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);

			}
		}
		renewsLastMin.increment();
		// 正常的發生了續約
		leaseToRenew.renew();
		return true;
	}
}

續約的本質是修改最後的修改時間
在這裏插入圖片描述
duration爲服務器"忍耐"時間,也就是說當超過30s,Server端不會馬上剔除過期的Client,而是在30s+duration之後剔除。

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

注意,lastUpdateTimestamp是用volatile修飾的,目的是讓此變量在線程中可見,當我這個線程中的lastUpdateTimestamp被修改時,其他的線程也都能夠看到。
在這裏插入圖片描述


服務的剔除

當eureka-server發現有的實例長期沒有發送續約(沒有心跳),則剔除該Client服務。在AbstractInstanceRegistry中通過evict方法實現剔除功能。
在這裏插入圖片描述

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<>();
     // 開始循環registry註冊中心,來完成對過期實例的檢測工作
     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);
             // 交換元素位置,保證剔除的公平性(元素被剔除的先後順序和放入到list的順序無關)
             Collections.swap(expiredLeases, i, next);
             // 使用Knuth shuffle 算法,在expiredLeases中
             // 從expiredLeases中獲取一個過期的實例
             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);
             // 實際上元素(client)被剔除的方法
             internalCancel(appName, id, false);
         }
     }
 }

internalCancel(String appName, String id, boolean isReplication)方法中,最終是執行leaseToCancel = gMap.remove(id),從存儲客戶端信息的Map中remove掉這個instance。判斷實例是否過期的方法如下。

public boolean isExpired(long additionalLeaseMs) {
    return (evictionTimestamp > 0 
    || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}

其實追根溯源,可以發現Eureka Server本質上是使用一個定期刪除的策略來完成對Eureka Client的剔除。
在這裏插入圖片描述
發現該定時任務默認定期檢查時間爲60s
在這裏插入圖片描述


服務的下線

當eureka-client下線時,項目不會立馬被關閉,而是做好"善後"工作才關閉。
首先eureka-client會發送請求給eureka-server,表示客戶端要下線了。在DiscoveryClient中通過unregister()方法發出服務下線的請求。
在這裏插入圖片描述
接着跟進cancle方法(AbstractJerseyEurekaHttpClient),可以看到,通過請求下線服務的名稱拼接得到urlPath,並構建一個resourceBuilder對象,最終調用resourceBuilder.delete(ClientResponse.class)向Server發出http請求下線。
在這裏插入圖片描述
那麼eureka-server怎麼處理該請求呢?我們在AbstractInstanceRegistry中找到對應得cancel方法
在這裏插入圖片描述
我們可以看見服務的下線在eureka-server 裏面直接調用

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        CANCEL.increment(isReplication);
        // 從註冊中心中通過請求的應用名稱獲取對飲的gMap
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToCancel = null;
        // 
        if (gMap != null) {
        	// 從gMap中移除實例
            leaseToCancel = gMap.remove(id);
        }
        ....
    }   
}

服務的發現

之前在手寫Dubbo RPC框架的時候,有講到過服務的發現,其實理解起來很簡單,無非就是通過服務的名稱發現服務的實例。比如說A服務要調用B服務的話,那麼B服務必須提前註冊,格式爲{serviceName,{instanceid:instanceObject}}(Map中套Map的結構,上面有提到),那麼通過服務的名稱,就可以發現這個服務,進而獲取這個服務的實例信息。
在這裏插入圖片描述
在DiscoveryClient的子類CompositeDiscoveryClient中找到getInstance方法
在這裏插入圖片描述
再進到EurekaDiscoveryClient中,發現本上還是調用DiscoveryClient的getInstancesByVipAddress(serviceId, false)方法

 @Override
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure,
                                                   @Nullable String region) {
    if (vipAddress == null) {
        throw new IllegalArgumentException(
                "Supplied VIP Address cannot be null");
    }
    // 存儲的服務列表
    Applications applications;
    if (instanceRegionChecker.isLocalRegion(region)) {
        applications = this.localRegionApps.get();
    } else {
        applications = remoteRegionVsApps.get(region);
        if (null == applications) {
            logger.debug("No applications are defined for region {}, so returning an empty instance list for vip "
                    + "address {}.", region, vipAddress);
            return Collections.emptyList();
        }
    }

    if (!secure) {
    	// 真正的完成服務的發現的方法
        return applications.getInstancesByVirtualHostName(vipAddress);
    } else {
        return applications.getInstancesBySecureVirtualHostName(vipAddress);

    }
}

以上代碼中比較關鍵的就是applications對象,那麼我們點開看看Applications中究竟是什麼結構。不難發現Applications中有個專門存放InstanceInfo(服務實例信息)的List集合,只不過被AtomicReference包裝了一下,目的是使得List集合在高併發的情況下是線程安全的。
在這裏插入圖片描述
回到上面的代碼,applications.getInstancesByVirtualHostName(vipAddress)是真正的完成服務的發現的方法,那麼點開這個方法(這個方法比較難懂,是一串lambda表達式),本質上就是通過virtualHostName(服務應用名稱) 獲取服務註冊列表
在這裏插入圖片描述
在Applcaitions中存在以下四個Map集合,存儲的數據都相同,只不過應用的場景不同,這些數據全部來自Server端,由Server發送給Client。
在這裏插入圖片描述
並且這四個Map的key都是大寫的服務名稱
在這裏插入圖片描述
此時我們還沒有做服務的發現,但是該集合裏面已經有值了,說明我們的項目啓動後,會自動去拉取服務,並且將拉取的服務緩存起來。那麼具體什麼時候被放進去的呢?
在DiscoveryClient的構造器裏面,有個任務的調度線程池,該線程池將用來在服務列表的拉取。

cacheRefreshExecutor = new ThreadPoolExecutor(
                   1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 
                   0, TimeUnit.SECONDS,
                   new SynchronousQueue<Runnable>(),
                   new ThreadFactoryBuilder()
                           .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                           .setDaemon(true)
                           .build()
           );  // use direct handoff

該線程的調度 ,調度的是下面的定時任務,在項目啓動的時候就去拉取服務列表
在這裏插入圖片描述
在這裏插入圖片描述
我們點開看看這個刷新註冊列表的方法refreshRegistry()

@VisibleForTesting
void refreshRegistry() {
      boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

      boolean remoteRegionsModified = false;
      // This makes sure that a dynamic change to remote regions to fetch is honored.
      String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();
      if (null != latestRemoteRegions) {
          String currentRemoteRegions = remoteRegionsToFetch.get();
          if (!latestRemoteRegions.equals(currentRemoteRegions)) {
              // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync
              synchronized (instanceRegionChecker.getAzToRegionMapper()) {
                  if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {
                      String[] remoteRegions = latestRemoteRegions.split(",");
                      remoteRegionsRef.set(remoteRegions);
                      instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);
                      remoteRegionsModified = true;
                  } else {
                      logger.info("Remote regions to fetch modified concurrently," +
                              " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);
                  }
              }
          } else {
              // Just refresh mapping to reflect any DNS/Property change
              instanceRegionChecker.getAzToRegionMapper().refreshMapping();
          }
      }
	// 在在裏面真正的執行服務列表的獲取
      boolean success = fetchRegistry(remoteRegionsModified);
      if (success) {
          registrySize = localRegionApps.get().size();
          lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
      }
}

找到fetchRegistry拉取註冊列表方法

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
	Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
	try {
	    // If the delta is disabled or if it is the first time, get all
	    // applications
	    Applications applications = getApplications();
	
	    if (clientConfig.shouldDisableDelta()
	            || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
	            || forceFullRegistryFetch
	            || (applications == null)
	            || (applications.getRegisteredApplications().size() == 0)
	            || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
	    {
	        logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
	        logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
	        logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
	        logger.info("Application is null : {}", (applications == null));
	        logger.info("Registered Applications size is zero : {}",
	                (applications.getRegisteredApplications().size() == 0));
	        logger.info("Application version is -1: {}", (applications.getVersion() == -1));
		     // 全量的拉取(代表拉取所有的註冊中心)
		     // 當緩存爲null 或者它裏面的數據爲empty時,進行全量的拉取
			getAndStoreFullRegistry();
	    } else {
	       // 增量(只拉取修改的註冊中心)
	        getAndUpdateDelta(applications);
	    }
	    applications.setAppsHashCode(applications.getReconcileHashCode());
	    logTotalInstances();
	} catch (Throwable e) {
	    logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
	    return false;
	} finally {
	    if (tracer != null) {
	        tracer.stop();
	    }
}

通過上面的代碼不難發現,Eureka提供了兩種拉取服務列表的方案,分別是全量拉取,和增量拉取

全量的拉取:

在這裏插入圖片描述
全量拉取之後將applications對象放入localRegionApps中

增量的拉取
private void getAndUpdateDelta(Applications applications) throws Throwable {
  long currentUpdateGeneration = fetchRegistryGeneration.get();

    Applications delta = null;
 	// 發起Http 請求來完成一個增量的拉取
    EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
       // 在eureka-server 裏面修改或新增的集合
		delta = httpResponse.getEntity();
    }
     // 若拉取的集合爲null
    if (delta == null) {
        logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                + "Hence got the full registry.");
       	// 進行全量拉取
        getAndStoreFullRegistry();
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
        String reconcileHashCode = "";
        if (fetchRegistryUpdateLock.tryLock()) {
            try {
               	// eureka-server 裏面變化的集合,通過它裏面的值更新本地的緩存
                updateDelta(delta);
             	// 一致性的hashcode值
             	// 一致性hash 用來校驗遠程的Eureka-server 集合和本地的eureka-server 集合是否一樣
                reconcileHashCode = getReconcileHashCode(applications);
            } finally {
                fetchRegistryUpdateLock.unlock();
            }
        } else {
            logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
        }
		//若hashcode值相等,則不拉取,否則,在拉取一次
        // There is a diff in number of instances for some reason
        if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
            reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
        }
    } else {
        logger.warn("Not updating application delta as another thread is updating it already");
        logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
    }
}
Eureka服務發現總結

1)服務列表的拉取並不是在服務調用的時候拉取,而是在項目啓動時就有定時任務去拉取了,在DiscoveryClient的構造器裏面有體現
在這裏插入圖片描述
2)我們的服務的實例並不是實時的 eureka-server 裏面的數據,而是一個本地的(內存)緩存數據
在這裏插入圖片描述
3)緩存的髒讀和更新的解決
全量拉取發生在:當服務列表爲 null的情況
增量拉取發生在當列表不爲 null ,只拉取eureka-server的修改的數據(註冊新的服務,上線服務)

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章