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的修改的數據(註冊新的服務,上線服務)