【SpringCloud】Eureka源碼解析 上

e246a1dda09849a5a89535a62441565d.png

Eureka是一個服務發現與注冊組件,它包含服務端和客戶端,服務端管理服務的注冊信息,客戶端簡化服務實例與服務端的交互。我們結合源碼來分析下eureka組件的實現原理,內容分為上下兩章,第一章分析eureka的服務注冊,第二章分析eureka的心跳機制,本章節是第一章。

參考源碼:<spring-cloud.version>Hoxton.SR9</spring-cloud.version>

往期系列:

【SpringBoot】SpringBoot源碼解析第一章 SpringBoot的構造方法-CSDN博客

【SpringBoot】SpringBoot源碼解析第二章 SpringBoot的run方法-CSDN博客

【SpringBoot】SpringBoot源碼解析第三章 SpringBoot的自動化配置-CSDN博客

【SpringBoot】SpringBoot源碼解析第四章 SpringBoot的bean接口-CSDN博客

【SpringBoot】SpringBoot源碼解析第五章 SpringBoot的beanDefinition收集過程-CSDN博客

【SpringBoot】SpringBoot源碼解析第六章 SpringBoot的getBean方法-CSDN博客

【SpringBoot】SpringBoot源碼解析第七章 SpringBoot的感悟-CSDN博客

1、注冊服務

1.1 服務端接收注冊信息

spring-cloud-netflix-eureka-server依賴包下有一個spring.factories文件,文件內容如下

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

根據springboot自動配置的原理可知,EurekaServerAutoConfiguration會被標記成了一個自動配置類。EurekaServerAutoConfiguration配置類中有一個jerseyApplication方法,這個方法會收集指定包下被Path或Provider注解標記的類的beanDefinition,這些類可以看作是Controller

// 掃描包路徑
private static final String[] EUREKA_PACKAGES = new String[]{"com.netflix.discovery", "com.netflix.eureka"};    // 收集包下指定類的beanDefinition,放入application對象
@Bean
public Application jerseyApplication(Environment environment, ResourceLoader resourceLoader) {ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, environment);// 收集的對象要求被Path或Provider注解標記provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));String[] var5 = EUREKA_PACKAGES;int var6 = var5.length;for(int var7 = 0; var7 < var6; ++var7) {String basePackage = var5[var7];// 掃描包路徑,收集beanDefinitionSet<BeanDefinition> beans = provider.findCandidateComponents(basePackage);Iterator var10 = beans.iterator();while(var10.hasNext()) {BeanDefinition bd = (BeanDefinition)var10.next();Class<?> cls = ClassUtils.resolveClassName(bd.getBeanClassName(), resourceLoader.getClassLoader());classes.add(cls);}}...return rc;
}// 獲取到application,將beanDefinition置入servlet容器
@Bean
public FilterRegistrationBean<?> jerseyFilterRegistration(Application eurekaJerseyApp) {FilterRegistrationBean<Filter> bean = new FilterRegistrationBean();bean.setFilter(new ServletContainer(eurekaJerseyApp));bean.setOrder(Integer.MAX_VALUE);bean.setUrlPatterns(Collections.singletonList("/eureka/*"));return bean;
}

收集的beanDefinition會通過jerseyFilterRegistration方法放入servlet容器,這樣接收請求時就能通過url映射給指定的bean來處理請求

com.netflix.eureka包下被掃描的類如下:

ApplicationResource類是Controller中的一員,它有一個addInstance方法,這個方法就是服務端響應服務注冊的方法

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {...// 執行注冊this.registry.register(info, "true".equals(isReplication));return Response.status(204).build();
}
調用鏈:
-> ApplicationResource.addInstance
-> InstanceRegistry.register
-> PeerAwareInstanceRegistryImpl.register
-> AbstractInstanceRegistry.register

服務端使用currentHashMap來存儲服務的信息,服務端響應注冊的過程較為簡單

// 用currentHashMap存儲服務信息 
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry= new ConcurrentHashMap();    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication)   
{...Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);if (existingLease != null) {lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());}// 將服務信息放入map中((Map)gMap).put(registrant.getId(), lease);...
}

1.2 客戶端發送注冊信息
1.2.1 client客戶端

spring-cloud-netflix-eureka-client依賴包下也有一個spring.factories文件,文件內容如下

...
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration
...

EurekaClientAutoConfiguration被標記成自動配置類,它里面有一個創建EurekaClient類對象的bean方法,看類的名稱我們知道這是一個客戶端

@Bean(destroyMethod = "shutdown"
)
@ConditionalOnMissingBean(value = {EurekaClient.class},search = SearchStrategy.CURRENT
)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance, @Autowired(required = false) HealthCheckHandler healthCheckHandler) {ApplicationInfoManager appManager;if (AopUtils.isAopProxy(manager)) {appManager = (ApplicationInfoManager)ProxyUtils.getTargetObject(manager);} else {appManager = manager;}// 創建客戶端CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);cloudEurekaClient.registerHealthCheck(healthCheckHandler);return cloudEurekaClient;
}
調用鏈:
-> EurekaAutoServiceRegistration.eurekaClient
-> new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
-> CloudEurekaClient.super(applicationInfoManager, config, args);
-> DiscoveryClient.DiscoveryClient... 構造方法重載-> DiscoveryClient.DiscoveryClient
-> initScheduledTasks

跟蹤EurekaClient類的構造方法找到DiscoveryClient類,DiscoveryClient類的構造方法調用了initScheduledTasks方法,初始化了一個定時任務

private void initScheduledTasks() {...// 添加狀態變更監聽器this.statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {public String getId() {return "statusChangeListener";}public void notify(StatusChangeEvent statusChangeEvent) {if (statusChangeEvent.getStatus() == InstanceStatus.DOWN) {DiscoveryClient.logger.error("Saw local status change event {}", statusChangeEvent);} else {DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);}// 監聽器被通知后調用onDemandUpdate方法DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();}};...}

定時任務內添加了一個狀態修改監聽器,監聽器調用notify方法時會回調onDemandUpdate方法,追蹤這個回調方法

調用鏈:
-> InstanceInfoReplicator.onDemandUpdate
-> InstanceInfoReplicator.this.run
-> this.discoveryClient.register
-> this.eurekaTransport.registrationClient.register(this.instanceInfo)
-> AbstractJerseyEurekaHttpClient.register

進入到AbstractJerseyEurekaHttpClient類的register方法

public EurekaHttpResponse<Void> register(InstanceInfo info) {String urlPath = "apps/" + info.getAppName();ClientResponse response = null;EurekaHttpResponse var5;try {// 向注冊中心發送http請求WebResource.Builder resourceBuilder = this.jerseyClient.resource(this.serviceUrl).path(urlPath).getRequestBuilder();this.addExtraHeaders(resourceBuilder);response = (ClientResponse)((WebResource.Builder)((WebResource.Builder)((WebResource.Builder)resourceBuilder.header("Accept-Encoding", "gzip")).type(MediaType.APPLICATION_JSON_TYPE)).accept(new String[]{"application/json"})).post(ClientResponse.class, info);var5 = EurekaHttpResponse.anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();} finally {if (logger.isDebugEnabled()) {logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", new Object[]{this.serviceUrl, urlPath, info.getId(), response == null ? "N/A" : response.getStatus()});}if (response != null) {response.close();}}return var5;
}

注冊方法寫得很直白了:客戶端拿到注冊中心地址,然后攜帶服務元數據,發送請求完成注冊。不過還有一個問題,之前我們提到定時任務內初始化了一個監聽器,這個監聽器只有被通知了才會執行后續的注冊方法,那么監聽器是如何被通知的?它的觸發時機又在何時?

1.2.2?監聽器

EurekaClientAutoConfiguration配置類還有一個創建EurekaAutoServiceRegistration類的bean方法

// 創建服務注冊客戶端 
@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
@ConditionalOnProperty(value = {"spring.cloud.service-registry.auto-registration.enabled"},matchIfMissing = true
)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {return new EurekaAutoServiceRegistration(context, registry, registration);
}

EurekaAutoServiceRegistration類實現了SmartLifecycle接口。當spring容器加載完所有bean后會調用SmartLifeCycle接口實現類的start方法,start方法調用EurekaServiceRegistry類的regiser方法

public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener 
{public void start() {if (this.port.get() != 0) {if (this.registration.getNonSecurePort() == 0) {this.registration.setNonSecurePort(this.port.get());}if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {this.registration.setSecurePort(this.port.get());}}if (!this.running.get() && this.registration.getNonSecurePort() > 0) {// 調用EurekaServiceRegistry的regiserthis.serviceRegistry.register(this.registration);this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));this.running.set(true);}}
}

EurekaServiceRegistry類的regiser方法會設置實例的狀態。進入ApplicationInfoManager類的setInstanceStatus方法

// 設置實例狀態        
reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

setInstanceStatus方法觸發了一個狀態修改事件,并且通知了監聽器

public synchronized void setInstanceStatus(InstanceInfo.InstanceStatus status) {InstanceInfo.InstanceStatus next = this.instanceStatusMapper.map(status);if (next != null) {InstanceInfo.InstanceStatus prev = this.instanceInfo.setStatus(next);if (prev != null) {Iterator var4 = this.listeners.values().iterator();while(var4.hasNext()) {StatusChangeListener listener = (StatusChangeListener)var4.next();try {// 通知監聽器listener.notify(new StatusChangeEvent(prev, next));} catch (Exception var7) {logger.warn("failed to notify listener: {}", listener.getId(), var7);}}}}
}

這里的監聽器和上面提到的狀態修改監聽器其實是同一個監聽器,在調用EurekaAutoServiceRegistration對象的start方法后,監聽器會收到通知然后調用客戶端的register方法,這就是發送注冊服務請求的執行時機

2、拉取服務

2.1 初次拉取

客戶端第一次拉取服務和DiscoveryClient類的構造方法有關,詳情如下:

@Inject
DiscoveryClient(...){...// 調用fetchRegistry方法,拉取服務boolean primaryFetchRegistryResult = this.fetchRegistry(false);if (!primaryFetchRegistryResult) {logger.info("Initial registry fetch from primary servers failed");}...
}  private boolean fetchRegistry(boolean forceFullRegistryFetch) {...// 調用getAndStoreFullRegistry方法,拉取全部服務this.getAndStoreFullRegistry();...
}private void getAndStoreFullRegistry() throws Throwable {...long currentUpdateGeneration = this.fetchRegistryGeneration.get();// 啟動時會打印這行日志logger.info("Getting all instance registry info from the eureka server");Applications apps = null;// 發送http請求EurekaHttpResponse<Applications> httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get());if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {apps = (Applications)httpResponse.getEntity();}...
}
2.2 定時拉取

為了保證服務信息真實可信,客戶端會定時拉取遠程注冊列表更新本地數據。提到到定時任務,自然的聯想到DiscoveryClient類的initScheduledTasks方法(1.2.1的內容)

private void initScheduledTasks() {int renewalIntervalInSecs;int expBackOffBound;if (this.clientConfig.shouldFetchRegistry()) {renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();// 定時刷新本地服務列表任務,具體任務在CacheRefreshThread內this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new // 執行任務的線程CacheRefreshThread());this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);}
}class CacheRefreshThread implements Runnable {CacheRefreshThread() {}public void run() {// 刷新服務列表DiscoveryClient.this.refreshRegistry();}
}@VisibleForTesting
void refreshRegistry() {...// 獲取服務列表boolean success = this.fetchRegistry(remoteRegionsModified);...
}

3、總結

eureka服務端啟動后通過自動配置加載com.netflix.eureka包下的處理器,處理器會響應注冊、拉取、剔除服務等http請求

eureka客戶端啟動后會發送注冊請求,并定時更新服務列表

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/38487.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/38487.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/38487.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ubuntu apt命令 出現紅色彈框 Daemons using outdated libraries

1. 彈框沒截圖&#xff0c;是因為ubuntu22.04一個新特性導致的&#xff0c;由 needrestart 命令觸發&#xff0c;默認情況是交互性質的&#xff0c;也就是會中斷在這里需要手動要處理提示。 2. 修改/etc/needrestart/needrestart.conf 文件&#xff0c;將 #$nrconf{restart} …

【區分vue2和vue3下的element UI PageHeader 頁頭組件,分別詳細介紹屬性,事件,方法如何使用,并舉例】

在 Vue 2 中&#xff0c;Element UI 并沒有一個直接命名為 PageHeader 的組件。然而&#xff0c;你可能是在尋找一種方式來創建自定義的頁頭&#xff08;Page Header&#xff09;&#xff0c;這通常是通過組合 Element UI 的其他組件&#xff08;如 el-header、el-row、el-col、…

大數據開發中如何計算用戶留存及SQL示例

在大數據開發領域&#xff0c;用戶留存是一個關鍵指標&#xff0c;它反映了產品吸引并保留用戶的能力。 留存率的計算不僅有助于評估產品的健康狀況&#xff0c;還能為產品優化和市場策略提供重要依據。 本文將詳細介紹如何在大數據開發中計算用戶留存&#xff0c;并附帶具體…

gpu是什么?

GPU&#xff08;Graphics Processing Unit&#xff0c;圖形處理單元&#xff09;是一種專門在個人電腦、工作站、游戲機以及一些移動設備&#xff08;如平板電腦、智能手機等&#xff09;上進行圖像和圖形相關運算工作的微處理器。以下是關于GPU的詳細解釋&#xff1a; 1. **定…

精密空氣加熱器負載組

小型便攜式 &#xff1a;精密空氣加熱器&#xff08;負載組&#xff09;能夠對數據中心熱通道/冷通道冷卻系統進行全面測試。EAK 是一款 19 英寸機架式設備&#xff08;10U 高&#xff09;&#xff0c;可輕松安裝到各種標準服務器機架中。通過集成可調節的熱量水平&#xff08;…

決策樹算法介紹:原理與案例實現以及Python、R、Java、 MATLAB中使用

決策樹&#xff08;Decision Tree&#xff09;是一種常用的機器學習算法&#xff0c;適用于分類和回歸任務。它通過一系列的二分決策將數據逐步劃分成不同的子集&#xff0c;直到每個子集中的數據點具有較高的同質性。下面介紹決策樹的基本原理&#xff0c;并通過Python實現一個…

C++ :lambda表達式

目錄 lambda表達式書寫格式&#xff1a; lambda表達式各部分說明&#xff1a; lambda的使用示范&#xff1a; 注意事項&#xff1a; 返回值類型可以省略&#xff0c;參數也可也省略&#xff1a; sort內部也可以直接寫lambda表達式&#xff1a; 排序時利用lambda進行排序…

STM32MP135裸機編程:使用軟件觸發硬件復位

0 參考資料 STM32MP13xx參考手冊.pdf 1 使用寄存器實現軟件復位 1.1 復位電路概述 重點關注下面標紅的路線&#xff1a; 通過這條路線可以清楚看到&#xff0c;我們可以通過設置RCC_MP_GRSTCSETR寄存器讓RPCTL&#xff08;復位脈沖控制器&#xff09;給NRST&#xff08;硬件復…

蘋果可能與谷歌合作推AI訂閱服務;全國首個司法審判大模型在深圳上線

&#x1f989; AI新聞 &#x1f680; 蘋果可能與谷歌合作推AI訂閱服務 摘要&#xff1a;蘋果宣布將與OpenAI合作推出Apple Intelligence&#xff0c;并有望在今年秋季與谷歌達成合作&#xff0c;接入Gemini。Meta的Llama因質量不佳被拒。蘋果計劃推出訂閱模式的智能功能服務&…

Oracle PL / SQL 存儲過程

PL / SQL存儲過程不返回值。他們執行他們的指示并返回。您不能在賦值語句&#xff08;如函數&#xff09;的右側使用存儲過程。 創建存儲過程 以下代碼是一個非常簡單的存儲過程示例。 它基于SYS.DBMS_LOCK包的程序sleep&#xff08;數量&#xff09;。 此存儲過程將停止執行…

在 C++的跨平臺開發中,如何處理不同操作系統和編譯器之間的細微差異,以確保程序能夠穩定且高效地運行?

在 C 的跨平臺開發中&#xff0c;處理不同操作系統和編譯器之間的細微差異是非常重要的。以下是一些處理差異的技巧&#xff1a; 使用條件編譯&#xff1a;使用預處理指令&#xff0c;根據不同的操作系統和編譯器來編寫不同的代碼。 #if defined(_WIN32)// Windows 特定代碼 …

kafka的工作原理與常見問題

定義 kafka是一個分布式的基于發布/訂閱模式的消息隊列&#xff08;message queue&#xff09;&#xff0c;主要應用于大數據的實時處理領域 消息隊列工作原理 kafka的組成結構 kafka的基礎架構主要有broker、生產者、消費者組構成&#xff0c;還包括zookeeper. 生產者負責發送…

算法09 日期相關模擬算法【C++實現】

這是《C算法寶典》算法篇的第09節文章啦~ 如果你之前沒有太多C基礎&#xff0c;請點擊&#x1f449;專欄&#xff1a;C語法入門&#xff0c;如果你C語法基礎已經爐火純青&#xff0c;則可以進階算法&#x1f449;專欄&#xff1a;算法知識和數據結構&#x1f449;專欄&#xff…

計算斜率,判斷斜率

#include <stdio.h> #include <stdlib.h> #include <math.h> #include <stdbool.h>// 定義常量 #define LOW_COOK_WINDOW_SIZE 20 // 滑動窗口大小&#xff0c;10個樣本點&#xff08;10秒&#xff09; #define LOW_COOK_SLOPE…

Java代碼生成器(開源版本)

一、在線地址 Java在線代碼生成器&#xff1a;在線訪問 二、頁面截圖 三、核心功能 支持Mybatis、MybatisPlus、Jpa代碼生成使用 antlr4 解析SQL語句&#xff0c;保證了SQL解析的成功率支持自定義包名、作者名信息支持自定義方法名、接口地址支持自定義選擇是否生成某個方法…

16-Python Pandas聚合函數

Python Pandas聚合函數 窗口函數可以與聚合函數一起使用&#xff0c;聚合函數指的是對一組數據求總和、最大值、最小值以及平均值的操作。 應用聚合函數 首先讓我們創建一個 DataFrame 對象&#xff0c;然后對聚合函數進行應用。 import pandas as pd import numpy as np d…

SQL中的子查詢和CTE(with ....as..)

第一次看到with as 這種類似于python中讀文件的寫法還是挺疑惑的&#xff0c;其實它是CTE&#xff0c;功能和子查詢很類似但又有不同點&#xff0c;在實際應用場景中具有著獨特作用。 子查詢 子查詢是在主查詢中的嵌套查詢&#xff0c;可以出現在SELECT、FROM、WHERE等子句中…

ai除安卓手機版APP軟件一鍵操作自動渲染去擦消稀缺資源下載

安卓手機版&#xff1a;點擊下載 蘋果手機版&#xff1a;點擊下載 電腦版&#xff08;支持Mac和Windows&#xff09;&#xff1a;點擊下載 一款全新的AI除安卓手機版APP&#xff0c;一鍵操作&#xff0c;輕松實現自動渲染和去擦消效果&#xff0c;稀缺資源下載 1、一鍵操作&…

數學建模(1):期末大亂燉

1 概述&#xff01;&#xff01; 1.1 原型和模型 原型&#xff1a;客觀存在的研究對象稱為原型&#xff0c;也稱為“系統”、“過程”。 機械系統、電力系統、化學反應過程、生產銷售過程等都是原型&#xff1b; 研究原型的結構和原理&#xff0c; 從而進行優化、預測、評價…

Perl編程藝術:深入探索Tie機制的魔力

&#x1f31f; Perl編程藝術&#xff1a;深入探索Tie機制的魔力 在Perl的世界里&#xff0c;tie功能是一種極其強大的特性&#xff0c;它允許程序員將變量綁定到一個對象上&#xff0c;從而改變這個變量的默認行為。這種機制為變量提供了一種代理訪問方式&#xff0c;使得變量…