服務治理:
服務註冊:
在服務治理框架中,通常都會構建一個註冊中心,每個服務單元向註冊中心登記自己提供的服務,將主機與端口號、版本號、通信協議等一些附加信息告知註冊中心,註冊中心按服務名分類組織服務清單。比如:有兩個提供服務A的進程分別運行於192.168.0.100:8000 和192.168.0.101:8000 位置上,還有三個提供服務B的進程分別運行於192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000位置上。當這些進程都啓動,並向註冊中心註冊自己的服務之後,註冊中心就會維護類似下面的一個服務清單。另外,註冊中心還需要以心跳的方式去監測清單中的服務是否可用,若不可用需要從服務清單中剔除,達到排除故障服務的效果。
服務發現:
Netflix Eureka
搭建服務註冊中心
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion><groupId>com.example</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-server</name> <description>Demo project <span style="color:rgb(0,0,255);line-height:1.5;">for</span> Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope><span style="color:rgb(0,0,255);line-height:1.5;">import</span></scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
通過@EnableEurekaServer 註解啓動一個服務註冊中心提供給其他應用進行對話
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
</span><span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">static</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> main(String[] args) {
SpringApplication.run(EurekaServerApplication.</span><span style="color:rgb(0,0,255);line-height:1.5;">class</span><span style="line-height:1.5;">, args);
}
}
server.port=8082
eureka.instance.hostname=localhost
向註冊中心註冊服務
eureka.client.register-with-eureka=false
檢索服務
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://{server.port}/eureka/
註冊服務提供者
<?xml version=“1.0” encoding=“UTF-8”?>
<project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
<modelVersion>4.0.0</modelVersion><groupId>com.example</groupId> <artifactId>eureka-client</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-client</name> <description>Demo project <span style="color:rgb(0,0,255);line-height:1.5;">for</span> Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope><span style="color:rgb(0,0,255);line-height:1.5;">import</span></scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
接着,新建RESTful API,通過注入DiscoveryClient對象,在日誌中打印出服務的相關內容。
package com.example.demo.web;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
- @author lxx
- @version V1.0.0
- @date 2017-8-9
*/
@RestController
public class HelloController {
</span><span style="color:rgb(0,0,255);line-height:1.5;">private</span> <span style="color:rgb(0,0,255);line-height:1.5;">final</span> Logger logger =<span style="line-height:1.5;"> Logger.getLogger(getClass());
@Autowired
</span><span style="color:rgb(0,0,255);line-height:1.5;">private</span><span style="line-height:1.5;"> DiscoveryClient client;
@RequestMapping(value </span>= "/index"<span style="line-height:1.5;">)
</span><span style="color:rgb(0,0,255);line-height:1.5;">public</span><span style="line-height:1.5;"> String index(){
ServiceInstance instance </span>=<span style="line-height:1.5;"> client.getLocalServiceInstance();
logger.info(</span>"/hello:host:"+instance.getHost()+" port:"+<span style="line-height:1.5;">instance.getPort()
</span>+" service_id:"+<span style="line-height:1.5;">instance.getServiceId());
</span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> "hello world!"<span style="line-height:1.5;">;
}
}
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {
</span><span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">static</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> main(String[] args) {
SpringApplication.run(EurekaClientApplication.</span><span style="color:rgb(0,0,255);line-height:1.5;">class</span><span style="line-height:1.5;">, args);
}
}
最後修改application.properties文件,通過spring.application.name屬性爲服務命名,再通過eureka.client.service-url.defaultZone 屬性來指定服務註冊中心的地址,地址和註冊中心設置的地址一致:
server.port=2222
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
2017-08-09 17:17:27.635 INFO 8716 — [ main] c.example.demo.EurekaClientApplication : Started EurekaClientApplication in 9.844 seconds (JVM running for 10.772)
2017-08-09 17:17:27.797 INFO 8716 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_HELLO-SERVICE/chanpin-PC:hello-service:2222 - registration status: 204
2017-08-09 17:17:27.786 INFO 10396 — [nio-8082-exec-1] c.n.e.registry.AbstractInstanceRegistry : Registered instance HELLO-SERVICE/chanpin-PC:hello-service:2222 with status UP (replication=false)
2017-08-09 17:17:47.792 INFO 10396 — [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Running the evict task with compensationTime 0ms
高可用註冊中心
- 創建 application-peer1.properties,作爲peer1 服務中心的配置,並將serviceUrl指向peer2:
spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.service-url.defaultZone=http://peer2:1112/eureka/
- 創建 application-peer2.properties,作爲peer2 服務中心的配置,並將serviceUrl指向peer1:
spring.application.name=eureka-server
server.port=1112
eureka.instance.hostname=peer2
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/
- 在C:\Windows\System32\drivers\etc\hosts 文件中添加對peer1 和 peer2 中的轉換,讓上面配置的host形式的serviceURL能在本地正確訪問到;
127.0.0.1 peer1
127.0.0.1 peer2 - 通過spring.profiles.active 屬性來分別啓動peer1 和 peer2(打開兩個terminal進行啓動,在一個terminal中先啓動的peer1 會報錯,但不影響,是因爲它所註冊的服務peer2 還未啓動,在另外個terminal中把peer2 啓動即可,不用啓動主類) :
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
- 在設置了多節點的服務註冊中心之後,服務提供方還需要做一些簡單的配置才能將服務註冊到Eureka Server 集羣中。以hello-service爲例,修改配置文件如下:
server.port=2222
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
如果不想使用主機名來定義註冊中心的地址,也可以使用IP地址的形式,但是需要在配置文件中增加配置參數 eureka.instance.prefer-ip-address=true,該值默認爲false。
- 準備工作:啓動之前實現的服務註冊中心eureka-server以及hello-service服務,爲了實驗Ribbon的客戶端負載均衡功能,我們通過java -jar 命令行的方式來啓動兩個端口不同的hello-service,具體如下:
- 修改配置文件:
server.port=2222
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
- 再將hello-service應用打包:mvn clean package
- 通過下列命令啓動應用程序:
java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8011
java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8012
- 成功啓動兩個服務後,可以在註冊中心看到名爲HELLO-SERVICE的服務中出現兩個實例單元:
- 創建一個Spring boot項目來實現服務消費者,取名爲ribbon-consumer,並在pom.xml中引入如下的依賴內容。
<?xml version=“1.0” encoding=“UTF-8”?>
<project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
<modelVersion>4.0.0</modelVersion><groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo</name> <description>Demo project <span style="color:rgb(0,0,255);line-height:1.5;">for</span> Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope><span style="color:rgb(0,0,255);line-height:1.5;">import</span></scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
- 在主類中通過@EnableDiscoveryClient註解讓該應用註冊爲Eureka客戶端應用,以獲取服務發現的能力,同時,在該主類中創建RestTemplate的Spring Bean實例,並通過@LoadBalanced 註解開啓客戶端負載均衡。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
</span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> <span style="color:rgb(0,0,255);line-height:1.5;">new</span><span style="line-height:1.5;"> RestTemplate();
}
</span><span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">static</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> main(String[] args) {
SpringApplication.run(DemoApplication.</span><span style="color:rgb(0,0,255);line-height:1.5;">class</span><span style="line-height:1.5;">, args);
}
}
- 創建ConsumerController類並實現/ribbon-consumer接口。在該接口中,通過上面創建的RestTemplate 來實現對HELLO-SERVICE 服務提供的 /hello 接口進行調用。此處的訪問地址是服務名 HELLO-SERVICE ,而不是一個具體的地址,在服務治理框架中,這是一個重要特性。
package com.example.demo.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
- @author lxx
- @version V1.0.0
- @date 2017-8-9
*/
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value </span>= "ribbon-consumer", method =<span style="line-height:1.5;"> RequestMethod.GET)
</span><span style="color:rgb(0,0,255);line-height:1.5;">public</span><span style="line-height:1.5;"> String helloConsumer(){
</span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> restTemplate.getForEntity("http://HELLO-SERVICE/index"<span style="line-height:1.5;">,
String.</span><span style="color:rgb(0,0,255);line-height:1.5;">class</span><span style="line-height:1.5;">).getBody();
}
}
- 在application.properties中配置Eureka服務註冊中心的位置,需要與之前的HELLO-SERVICE一樣,同時設置該消費者的端口爲3333,不與之前啓動的應用端口衝突即可。
server.port=3333
spring.application.name=ribbon-consumer
eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
- 啓動ribbon-consumer應用後,可以在Eureka信息面板中看到,除了HELLO-SERVICE外,還多了實現的RIBBON-CONSUMER服務。
- 通過向 http://localhost:3333/ribbon-consumer 發起訪問, 成功返回字符串 “hello world”。在消費者控制檯中打印出服務列表情況。
- 多發送幾次請求,可以在服務提供方hello-service的控制檯中看到一些打印信息,可以看出兩個控制檯基本是交替訪問,實現了客戶端的負載均衡。
Eureka詳解
基礎架構(核心三要素)
- 服務註冊中心:Eureka提供的服務端,提供服務註冊與發現的功能,即之前的eureka-server。
- 服務提供者:提供服務的應用,可以是spring boot應用,也可以是其他技術平臺且遵循Eureka通信機制的應用。它將自己提供的服務註冊到Eureka,以供其他應用發現。即之前的HELLO-SERVICE.
- 服務消費者:消費者從服務註冊中心獲取服務列表,從而使消費者可以知道去何處調用其所需要的服務,在上一節中使用了Ribbon來實現服務消費,後續還會介紹使用Feign的消費方式
服務治理機制
體驗了Spring cloud Eureka 通過簡單的註解配置就能實現強大的服務治理功能之後,進一步瞭解一下Eureka基礎架構中各個元素的一些通信行爲,以此來理解基於Eureka實現的服務治理體系是如何運作起來的。以上圖爲例,其中有幾個重要元素:
- “服務註冊中心-1” 和 “服務註冊中心-2”,他們互相註冊組成高可用集羣。
- “服務提供者” 啓動了兩個實例,一個註冊到“服務註冊中心-1” 上,另外一個註冊到 “服務註冊中心-2” 上。
- 還有兩個 “服務消費者” ,它們也都分別指向了一個註冊中心。
根據上面的結構,可以詳細瞭解從服務註冊開始到服務調用,及各個元素所涉及的一些重要通信行爲。
服務提供者
服務註冊
“服務提供者” 在啓動的時候會通過發送REST請求的方式將自己註冊到Eureka Server 上,同時帶上了自身服務的一些元數據信息。Eureka Server 接收到這個REST請求後,將元數據信息存儲在一個雙層結構Map中,其中第一層的key是服務名,第二層的key 是具體服務的實例名。
在服務註冊時,需要確認eureka.client.register-with-eureka=true參數是否正確,若爲false,將不會啓動註冊操作。
服務同步
如圖所示,這裏的兩個服務提供者分別註冊到了兩個不同的服務註冊中心上,即它們的信息分別被兩個服務註冊中心維護。由於服務註冊中心之間爲互相註冊,當服務提供者發送註冊請求到一個服務註冊中心時,它會將請求轉發給集羣中相連的其他註冊中心,從而實現註冊中心之間的服務同步。通過服務同步,兩個服務提供者的服務信息就可以通過這兩個服務註冊中心中的任意一臺獲取到。
服務續約
在註冊完服務之後,服務提供者會維護一個心跳用來持續告訴 Eureka Server :“我還活着”,以防止 Eureka Server 的 “剔除任務” 將該服務實例從服務列表中排除出去,我們稱該操作爲服務續約。
服務消費者
獲取服務
到這裏,在服務註冊中心已經註冊了一個服務,並且該服務有兩個實例。當我們啓動服務消費者時,它會發送一個REST請求給服務註冊中心,來獲取上面註冊的服務清單。爲了性能考慮,Eureka Server 會維護一份只讀的服務清單來返回給客戶端,同時該緩存清單會每隔30秒更新一次。
獲取服務是服務消費者的基礎,所以要確保 eureka-client-fetch-registery=true 參數沒有被修改成false,該值默認爲 true。若想修改緩存清單的更新時間,可以通過 eureka-client.registry-fetch-interval-seconds=30 參數來進行修改,該值默認爲30,單位爲秒。
服務調用
服務消費者在獲取服務清單後,通過服務名可以獲得具體提供服務的實例名和該實例的元數據信息。因爲有這些服務實例的詳細信息,所以客戶端可以根據自己的需要決定具體需要調用的實例,在Ribbon中會默認採用輪詢的方式進行調用,從而實現客戶端的負載均衡。
服務下線
在系統運行過程中必然會面臨關閉或重啓服務的某個實例的情況,在服務關閉期間,我們自然不希望客戶端會繼續調用關閉了的實例。所以在客戶端程序中,當服務實例進行正常的關閉操作時,它會觸發一個服務下線的REST請求給 Eureka Server,告訴服務註冊中心:“我要下線了”。服務端在接收到請求之後,將該服務狀態設置爲下線(DOWN),並把該下線事件傳播出去。
服務註冊中心
失效剔除
當一些外部原因如內存溢出、網絡故障等導致服務實例非正常下線,而服務註冊中心並未收到“服務下線”的請求。爲了從服務列表中將這些無法提供服務的實例剔除,Eureka Server 在啓動的時候會創建一個定時任務,默認每隔一段時間(默認60秒)將當前清單中超時(默認90秒)沒有續約的服務剔除出去。
自我保護
當我們在本地調試基於 Eureka 的程序時,基本上都會在服務註冊中心的信息面板上出現類似下面的紅色警告信息:
實際上,該警告就是觸發了Eureka Server的自我保護機制。之前介紹過,服務註冊到Eureka Server之後,會維護一個心跳連接,告訴Eureka Server 自己還活着。Eureka Server 在運行期間,會統計心跳失敗的比例在15分鐘之內低於85%,如果出現低於的情況,Eureka Server 會將當前的實例信息保護起來,讓這些實例不會過期,儘可能保護這些註冊信息。但是,在保護期間內實例若出現問題,那麼客戶端很容易拿到實際已經不存在的服務實例,會出現調用失敗的情況,所以客戶端必須要有容錯機制,比如可以使用請求重試、斷路器等機制。
由於在本地調試很容易觸發註冊中心的保護機制,使得註冊中心維護的服務實例不那麼準確。可以在本地進行開發時,使用 eureka-server.enable-self-preservation=false 參數來關閉保護機制,確保註冊中心將不可用的實例正確剔除。
源碼分析
上面,我們對Eureka中各個核心元素的通信行爲做了詳細的介紹,爲了更深入的理解它的運作和配置,下面我們結合源碼來分別看看各個通信行爲是如何實現的。
在看具體源碼之前,先回顧一下之前所實現的內容,從而找到一個合適的切入口去分析。首先,對於服務註冊中心、服務提供者、服務消費者這三個主要元素來說,後兩者(Eureka客戶端)在整個運行機制中是大部分通信行爲的主動發動着,而註冊中心主要是處理請求的接受者。所以,我們從Eureka客戶端作爲入口看看它是如何完成這些主動通信行爲的。
我們將一個普通的spring boot 應用註冊到Eureka Server 或是從 Eureka Server 中獲取服務列表時,主要就做了兩件事:
- 在應用類中配置了 @EnableDiscoveryClient 註解。
- 在application.properties 中用 eureka-client.service-url.defaultZone 參數指定了註冊中心的位置。
順着上面的線索,我們看看 @EnableDiscoveryClient 的源碼,具體如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.discovery;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.discovery.EnableDiscoveryClientImportSelector;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}
從該註解的註釋中我們可以知道,它主要用來開啓 DiscoveryClient 的實例。通過搜索 DiscoveryClient ,我們可以發現有一個類和一個接口。通過梳理可以得到如下圖所示的關係:
其中,1 是 Spring Cloud 的接口,它定義了用來發現服務的常用抽象方法,通過該接口可以有效的屏蔽服務治理的實現細節,所以使用 Spring Cloud 構建的微服務應用可以方便的切換不同服務治理框架,而不改動程序代碼,只需要另外添加一些針對服務治理框架的配置即可。2 是對 1 接口的實現,從命名判斷。它實現的是對 Eureka 發現服務的封裝。所以 EurekaDiscoveryClient 依賴了 Netflix Eureka 的 EurekaClient 接口,EurekaClient 接口繼承了 LookupService 接口,它們都是 Netflix 開源包中的內容,主要定義了針對 Eureka 的發現服務的抽象發放,而真正實現發現服務的則Netflix包中的 DiscoveryClient (5)類。
接下來,我們就詳細看看DiscoveryClient類。先看下該類的頭部註釋,大致內容如下:
在具體研究Eureka Client 負責完成的任務之前,我們先看看在哪裏對Eureka Server 的URL列表進行配置。根據配置的屬性名 eureka.client.service-url.defaultZone,通過 ServiceURL 可以找到該屬性相關的加載屬性,但是在SR5 版本中它們都被 @Deprecated 標註爲不再建議使用,並 @link 到了替代類 EndpointUtils,所以可以在該類中找到下面這個函數:
public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
LinkedHashMap orderedUrls = new LinkedHashMap();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if(availZones == null || availZones.length == 0) {
availZones = new String[]{“default”};
}logger.debug(</span>"The availability zone for the given region {} are {}"<span style="line-height:1.5;">, region, Arrays.toString(availZones)); </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span> myZoneOffset =<span style="line-height:1.5;"> getZoneOffset(instanceZone, preferSameZone, availZones); String zone </span>=<span style="line-height:1.5;"> availZones[myZoneOffset]; List serviceUrls </span>=<span style="line-height:1.5;"> clientConfig.getEurekaServerServiceUrls(zone); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(serviceUrls != <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { orderedUrls.put(zone, serviceUrls); } </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span> currentOffset = myZoneOffset == availZones.length - 1?0:myZoneOffset + 1<span style="line-height:1.5;">; </span><span style="color:rgb(0,0,255);line-height:1.5;">while</span>(currentOffset !=<span style="line-height:1.5;"> myZoneOffset) { zone </span>=<span style="line-height:1.5;"> availZones[currentOffset]; serviceUrls </span>=<span style="line-height:1.5;"> clientConfig.getEurekaServerServiceUrls(zone); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(serviceUrls != <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { orderedUrls.put(zone, serviceUrls); } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(currentOffset == availZones.length - 1<span style="line-height:1.5;">) { currentOffset </span>= 0<span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { </span>++<span style="line-height:1.5;">currentOffset; } } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(orderedUrls.size() < 1<span style="line-height:1.5;">) { </span><span style="color:rgb(0,0,255);line-height:1.5;">throw</span> <span style="color:rgb(0,0,255);line-height:1.5;">new</span> IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"<span style="line-height:1.5;">); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span><span style="line-height:1.5;"> orderedUrls; } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> </p><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> Region、Zone</h4><p style="margin:10px auto;"> 從上面的函數中可以發現,客戶端依次加載了兩個內容,第一個是Region,第二個是Zone,從其加載邏輯上可以判斷它們之間的關係:</p><ul style="margin-left:30px;"><li>通過 getRegion 函數,我們可以看到他從配置中讀取了一個Region返回,所以一個微服務應用只可以屬於一個Region,如果不特別配置,默認爲default。若要自己配置,可以通過 eureka.client.region屬性來定義。</li></ul><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"> <span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">static</span><span style="line-height:1.5;"> String getRegion(EurekaClientConfig clientConfig) { String region </span>=<span style="line-height:1.5;"> clientConfig.getRegion(); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(region == <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { region </span>= "default"<span style="line-height:1.5;">; } region </span>=<span style="line-height:1.5;"> region.trim().toLowerCase(); </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span><span style="line-height:1.5;"> region; }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><ul style="margin-left:30px;"><li>通過 getAvailabilityZones 函數,可以知道當我們沒有特別爲 Region 配置 Zone 的時候,默認採用defaultZone , 這纔是我們之前配置參數 eureka.client.service-url.defaultZone 的由來。若要爲應用指定Zone,可以通過eureka.client.availability-zones 屬性來設置。從該函數的 return 內容,可以知道 Zone 能夠設置多個,並且通過逗號分隔來配置。由此,我們可以判斷Region與Zone 是一對多的關係。</li></ul><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"> <span style="color:rgb(0,0,255);line-height:1.5;">public</span><span style="line-height:1.5;"> String[] getAvailabilityZones(String region) { String value </span>= (String)<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.availabilityZones.get(region); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(value == <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { value </span>= "defaultZone"<span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> value.split(","<span style="line-height:1.5;">); }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> serviceUrls</h4><p style="margin:10px auto;"> 在獲取了Region 和 Zone 的信息之後,纔開始真正加載 Eureka Server 的具體地址。它根據傳入的參數按一定算法確定加載位於哪一個Zone配置的serviceUrls。</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">int</span> myZoneOffset =<span style="line-height:1.5;"> getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
具體獲取serviceUrls 的實現,可以詳細查看getEurekaServerServiceUrls 函數的具體實現類 EurekaClientConfigBean,用來加載配置文件中的內容,通過搜索defaultZone,我們可以很容易找到下面這個函數,它具體實現瞭如何解析該參數的過程,通過此內容,我們可以知道,eureka.client.service-url.defaultZone 屬性可以配置多個,並且需要通過逗號分隔。
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = (String)this.serviceUrl.get(myZone);
if(serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = (String)this.serviceUrl.get(“defaultZone”);
}</span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(!<span style="line-height:1.5;">StringUtils.isEmpty(serviceUrls)) { String[] serviceUrlsSplit </span>=<span style="line-height:1.5;"> StringUtils.commaDelimitedListToStringArray(serviceUrls); ArrayList eurekaServiceUrls </span>= <span style="color:rgb(0,0,255);line-height:1.5;">new</span><span style="line-height:1.5;"> ArrayList(serviceUrlsSplit.length); String[] var5 </span>=<span style="line-height:1.5;"> serviceUrlsSplit; </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span> var6 =<span style="line-height:1.5;"> serviceUrlsSplit.length; </span><span style="color:rgb(0,0,255);line-height:1.5;">for</span>(<span style="color:rgb(0,0,255);line-height:1.5;">int</span> var7 = 0; var7 < var6; ++<span style="line-height:1.5;">var7) { String eurekaServiceUrl </span>=<span style="line-height:1.5;"> var5[var7]; </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(!<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.endsWithSlash(eurekaServiceUrl)) { eurekaServiceUrl </span>= eurekaServiceUrl + "/"<span style="line-height:1.5;">; } eurekaServiceUrls.add(eurekaServiceUrl); } </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span><span style="line-height:1.5;"> eurekaServiceUrls; } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> <span style="color:rgb(0,0,255);line-height:1.5;">new</span><span style="line-height:1.5;"> ArrayList(); } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 當我們在微服務應用中使用Ribbon來實現服務調用時,對於Zone的設置可以在負載均衡時實現區域親和特性:Ribbon的默認策略會優先訪問同客戶端處於一個Zone中的服務端實例,只有當同一個Zone 中沒有可用服務端實例的時候纔會訪問其他Zone中的實例。所以通過Zone屬性的定義,配合實際部署的物理結構,我們就可以有效地設計出對區域性故障的容錯集羣。</p><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> 服務註冊</h4><p style="margin:10px auto;"> 在理解了多個服務註冊中心信息的加載後,我們再回頭看看DiscoveryClient類是如何實現“服務註冊”行爲的,通過查看它的構造類,可以找到調用了下面這個函數:</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">private</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> initScheduledTasks() { </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span><span style="line-height:1.5;"> renewalIntervalInSecs; </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span><span style="line-height:1.5;"> expBackOffBound; </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.shouldFetchRegistry()) { renewalIntervalInSecs </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getRegistryFetchIntervalSeconds(); expBackOffBound </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">new</span> TimedSupervisorTask("cacheRefresh", <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, <span style="color:rgb(0,0,255);line-height:1.5;">new</span> DiscoveryClient.CacheRefreshThread()), (<span style="color:rgb(0,0,255);line-height:1.5;">long</span><span style="line-height:1.5;">)renewalIntervalInSecs, TimeUnit.SECONDS); } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.shouldRegisterWithEureka()) { renewalIntervalInSecs </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); expBackOffBound </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info(</span>"Starting heartbeat executor: renew interval is: " +<span style="line-height:1.5;"> renewalIntervalInSecs); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">new</span> TimedSupervisorTask("heartbeat", <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, <span style="color:rgb(0,0,255);line-height:1.5;">new</span> DiscoveryClient.HeartbeatThread(<span style="color:rgb(0,0,255);line-height:1.5;">null</span>)), (<span style="color:rgb(0,0,255);line-height:1.5;">long</span><span style="line-height:1.5;">)renewalIntervalInSecs, TimeUnit.SECONDS); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfoReplicator = <span style="color:rgb(0,0,255);line-height:1.5;">new</span> InstanceInfoReplicator(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfo, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2<span style="line-height:1.5;">); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.statusChangeListener = <span style="color:rgb(0,0,255);line-height:1.5;">new</span><span style="line-height:1.5;"> StatusChangeListener() { </span><span style="color:rgb(0,0,255);line-height:1.5;">public</span><span style="line-height:1.5;"> String getId() { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> "statusChangeListener"<span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> notify(StatusChangeEvent statusChangeEvent) { </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN !=<span style="line-height:1.5;"> statusChangeEvent.getPreviousStatus()) { DiscoveryClient.logger.info(</span>"Saw local status change event {}"<span style="line-height:1.5;">, statusChangeEvent); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { DiscoveryClient.logger.warn(</span>"Saw local status change event {}"<span style="line-height:1.5;">, statusChangeEvent); } DiscoveryClient.</span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfoReplicator.onDemandUpdate(); } }; </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.shouldOnDemandUpdateStatusChange()) { </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.applicationInfoManager.registerStatusChangeListener(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.statusChangeListener); } </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfoReplicator.start(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { logger.info(</span>"Not registering with Eureka server per configuration"<span style="line-height:1.5;">); } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 在上面的函數中,可以看到一個與服務註冊相關的判斷語句 if(this.clientConfig.shouldRegisterWithEureka())。在該分支內,創建了一個 InstanceInfoReplicator 類的實例,他會執行一個定時任務,而這個定時任務的具體工作可以查看該類的run() 函數,具體如下所示:</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> run() { </span><span style="color:rgb(0,0,255);line-height:1.5;">boolean</span> var6 = <span style="color:rgb(0,0,255);line-height:1.5;">false</span><span style="line-height:1.5;">; ScheduledFuture next2; label53: { </span><span style="color:rgb(0,0,255);line-height:1.5;">try</span><span style="line-height:1.5;"> { var6 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">true</span><span style="line-height:1.5;">; </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.discoveryClient.refreshInstanceInfo(); Long next </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo.isDirtyWithTime(); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(next != <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.discoveryClient.register(); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo.unsetIsDirty(next.longValue()); var6 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">false</span><span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { var6 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">false</span><span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">break</span><span style="line-height:1.5;"> label53; } </span><span style="color:rgb(0,0,255);line-height:1.5;">catch</span><span style="line-height:1.5;"> (Throwable var7) { logger.warn(</span>"There was a problem with the instance info replicator"<span style="line-height:1.5;">, var7); var6 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">false</span><span style="line-height:1.5;">; } </span><span style="color:rgb(0,0,255);line-height:1.5;">finally</span><span style="line-height:1.5;"> { </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span><span style="line-height:1.5;">(var6) { ScheduledFuture next1 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>, (<span style="color:rgb(0,0,255);line-height:1.5;">long</span>)<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.replicationIntervalSeconds, TimeUnit.SECONDS); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.scheduledPeriodicRef.set(next1); } } next2 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>, (<span style="color:rgb(0,0,255);line-height:1.5;">long</span>)<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.replicationIntervalSeconds, TimeUnit.SECONDS); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.scheduledPeriodicRef.set(next2); </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span><span style="line-height:1.5;">; } next2 </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>, (<span style="color:rgb(0,0,255);line-height:1.5;">long</span>)<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.replicationIntervalSeconds, TimeUnit.SECONDS); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.scheduledPeriodicRef.set(next2); }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 這裏有個 this.discoveryClient.register(); 這一行,真正觸發調用註冊的地方就在這裏,繼續查看register() 的實現內容,如下:</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"> <span style="color:rgb(0,0,255);line-height:1.5;">boolean</span> register() <span style="color:rgb(0,0,255);line-height:1.5;">throws</span><span style="line-height:1.5;"> Throwable { logger.info(</span>"DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.appPathIdentifier + ": registering service..."<span style="line-height:1.5;">); EurekaHttpResponse httpResponse; </span><span style="color:rgb(0,0,255);line-height:1.5;">try</span><span style="line-height:1.5;"> { httpResponse </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.eurekaTransport.registrationClient.register(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo); } </span><span style="color:rgb(0,0,255);line-height:1.5;">catch</span><span style="line-height:1.5;"> (Exception var3) { logger.warn(</span>"{} - registration failed {}", <span style="color:rgb(0,0,255);line-height:1.5;">new</span> Object[]{"DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.appPathIdentifier, var3.getMessage(), var3}); </span><span style="color:rgb(0,0,255);line-height:1.5;">throw</span><span style="line-height:1.5;"> var3; } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span><span style="line-height:1.5;">(logger.isInfoEnabled()) { logger.info(</span>"{} - registration status: {}", "DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.appPathIdentifier, Integer.valueOf(httpResponse.getStatusCode())); } </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> httpResponse.getStatusCode() == 204<span style="line-height:1.5;">; }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 可以看出,註冊操作也是通過REST請求的方式進行的。同時,我們能看到發起註冊請求的時候,傳入了一個 instanceInfo 對象,該對象就是註冊時客戶端給服務端的服務的元數據。</p><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> 服務獲取與服務續約</h4><p style="margin:10px auto;"> 順着上面的思路,繼續看 DiscoveryClient 的 initScheduledTasks 函數,不難發現在其中還有兩個定時任務,分別是 “服務獲取” 和 “服務續約” :</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">private</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span><span style="line-height:1.5;"> initScheduledTasks() { </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span><span style="line-height:1.5;"> renewalIntervalInSecs; </span><span style="color:rgb(0,0,255);line-height:1.5;">int</span><span style="line-height:1.5;"> expBackOffBound; </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.shouldFetchRegistry()) { renewalIntervalInSecs </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getRegistryFetchIntervalSeconds(); expBackOffBound </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">new</span> TimedSupervisorTask("cacheRefresh", <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, <span style="color:rgb(0,0,255);line-height:1.5;">new</span> DiscoveryClient.CacheRefreshThread()), (<span style="color:rgb(0,0,255);line-height:1.5;">long</span><span style="line-height:1.5;">)renewalIntervalInSecs, TimeUnit.SECONDS); } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.shouldRegisterWithEureka()) { renewalIntervalInSecs </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); expBackOffBound </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info(</span>"Starting heartbeat executor: renew interval is: " +<span style="line-height:1.5;"> renewalIntervalInSecs); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler.schedule(<span style="color:rgb(0,0,255);line-height:1.5;">new</span> TimedSupervisorTask("heartbeat", <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.scheduler, <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, <span style="color:rgb(0,0,255);line-height:1.5;">new</span> DiscoveryClient.HeartbeatThread(<span style="color:rgb(0,0,255);line-height:1.5;">null</span>)), (<span style="color:rgb(0,0,255);line-height:1.5;">long</span><span style="line-height:1.5;">)renewalIntervalInSecs, TimeUnit.SECONDS); …………</span><span style="line-height:1.5;"> } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { logger.info(</span>"Not registering with Eureka server per configuration"<span style="line-height:1.5;">); } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 從源碼中可以看出,“服務獲取” 任務相對於 “服務續約” 和 “服務註冊” 任務更爲獨立。“服務續約” 與 “服務註冊” 在同一個 if 邏輯中,這個不難理解,服務註冊到Eureka Server 後,需要一個心跳去續約,防止被剔除,所以它們肯定是成對出現的。</p><p style="margin:10px auto;"> 而 “服務獲取” 的邏輯在一個獨立的 if 判斷中,而且是由eureka.client.fetch-registry=true 參數控制,它默認爲true,大部分情況下不需關心。</p><p style="margin:10px auto;"> 繼續往下可以發現 “服務獲取” 和 “服務續約” 的具體方法,其中 “服務續約” 的實現比較簡單,直接以REST請求的方式進行續約:</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">boolean</span><span style="line-height:1.5;"> renew() { </span><span style="color:rgb(0,0,255);line-height:1.5;">try</span><span style="line-height:1.5;"> { EurekaHttpResponse httpResponse </span>= <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.eurekaTransport.registrationClient.sendHeartBeat(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfo.getAppName(), <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfo.getId(), <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.instanceInfo, (InstanceStatus)<span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">); logger.debug(</span>"{} - Heartbeat status: {}", "DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.appPathIdentifier, Integer.valueOf(httpResponse.getStatusCode())); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(httpResponse.getStatusCode() == 404<span style="line-height:1.5;">) { </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.REREGISTER_COUNTER.increment(); logger.info(</span>"{} - Re-registering apps/{}", "DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.appPathIdentifier, <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.instanceInfo.getAppName()); </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.register(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> httpResponse.getStatusCode() == 200<span style="line-height:1.5;">; } } </span><span style="color:rgb(0,0,255);line-height:1.5;">catch</span><span style="line-height:1.5;"> (Throwable var3) { logger.error(</span>"{} - was unable to send heartbeat!", "DiscoveryClient_" + <span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.appPathIdentifier, var3); </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> <span style="color:rgb(0,0,255);line-height:1.5;">false</span><span style="line-height:1.5;">; } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 而 “服務獲取” 則複雜一些,會根據是否是第一次獲取發起不同的 REST 請求和相應的處理。</p><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> 服務註冊中心處理</h4><p style="margin:10px auto;"> 通過上面的源碼分析,可以看到所有的交互都是通過 REST 請求發起的。下面看看服務註冊中心對這些請求的處理。Eureka Server 對於各類 REST 請求的定義都位於 com.netflix.eureka.resources 包下。</p><p style="margin:10px auto;"> 以 “服務註冊” 請求爲例(在ApplicationResource類中):</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="line-height:1.5;">@POST @Consumes({</span>"application/json", "application/xml"<span style="line-height:1.5;">}) </span><span style="color:rgb(0,0,255);line-height:1.5;">public</span> Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication"<span style="line-height:1.5;">) String isReplication) { logger.debug(</span>"Registering instance {} (replication={})"<span style="line-height:1.5;">, info.getId(), isReplication); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.isBlank(info.getId())) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Missing instanceId"<span style="line-height:1.5;">).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span> <span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.isBlank(info.getHostName())) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Missing hostname"<span style="line-height:1.5;">).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span> <span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.isBlank(info.getAppName())) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Missing appName"<span style="line-height:1.5;">).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span> <span style="color:rgb(0,0,255);line-height:1.5;">if</span>(!<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.appName.equals(info.getAppName())) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Mismatched appName, expecting " + <span style="color:rgb(0,0,255);line-height:1.5;">this</span>.appName + " but was " +<span style="line-height:1.5;"> info.getAppName()).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span> <span style="color:rgb(0,0,255);line-height:1.5;">if</span>(info.getDataCenterInfo() == <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Missing dataCenterInfo"<span style="line-height:1.5;">).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span> <span style="color:rgb(0,0,255);line-height:1.5;">if</span>(info.getDataCenterInfo().getName() == <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400).entity("Missing dataCenterInfo Name"<span style="line-height:1.5;">).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { DataCenterInfo dataCenterInfo </span>=<span style="line-height:1.5;"> info.getDataCenterInfo(); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(dataCenterInfo <span style="color:rgb(0,0,255);line-height:1.5;">instanceof</span><span style="line-height:1.5;"> UniqueIdentifier) { String dataCenterInfoId </span>=<span style="line-height:1.5;"> ((UniqueIdentifier)dataCenterInfo).getId(); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.isBlank(dataCenterInfoId)) { </span><span style="color:rgb(0,0,255);line-height:1.5;">boolean</span> experimental = "true".equalsIgnoreCase(<span style="color:rgb(0,0,255);line-height:1.5;">this</span>.serverConfig.getExperimental("registration.validation.dataCenterInfoId"<span style="line-height:1.5;">)); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span><span style="line-height:1.5;">(experimental) { String amazonInfo1 </span>= "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id"<span style="line-height:1.5;">; </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(400<span style="line-height:1.5;">).entity(amazonInfo1).build(); } </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(dataCenterInfo <span style="color:rgb(0,0,255);line-height:1.5;">instanceof</span><span style="line-height:1.5;"> AmazonInfo) { AmazonInfo amazonInfo </span>=<span style="line-height:1.5;"> (AmazonInfo)dataCenterInfo; String effectiveId </span>=<span style="line-height:1.5;"> amazonInfo.get(MetaDataKey.instanceId); </span><span style="color:rgb(0,0,255);line-height:1.5;">if</span>(effectiveId == <span style="color:rgb(0,0,255);line-height:1.5;">null</span><span style="line-height:1.5;">) { amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId()); } } </span><span style="color:rgb(0,0,255);line-height:1.5;">else</span><span style="line-height:1.5;"> { logger.warn(</span>"Registering DataCenterInfo of type {} without an appropriate id"<span style="line-height:1.5;">, dataCenterInfo.getClass()); } } } </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.registry.register(info, "true"<span style="line-height:1.5;">.equals(isReplication)); </span><span style="color:rgb(0,0,255);line-height:1.5;">return</span> Response.status(204<span style="line-height:1.5;">).build(); } }</span></pre><div class="cnblogs_code_toolbar" style="margin-top:5px;"><span class="cnblogs_code_copy" style="padding-right:5px;line-height:1.5;"><a title="複製代碼" style="color:rgb(7,93,179);text-decoration:underline;border:none;"><img src="https://common.cnblogs.com/images/copycode.gif" alt="複製代碼" style="max-width:900px;border:none;"></a></span></div></div><p style="margin:10px auto;"> 在對註冊信息進行了一堆校驗之後,會調用 org.springframework.cloud.netflix.eureka.server.InstanceRegister 對象中的 register( InstanceInfo info, int leaseDuration, boolean isReplication) 函數來進行服務註冊:</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"> <span style="color:rgb(0,0,255);line-height:1.5;">public</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span> register(InstanceInfo info, <span style="color:rgb(0,0,255);line-height:1.5;">int</span> leaseDuration, <span style="color:rgb(0,0,255);line-height:1.5;">boolean</span><span style="line-height:1.5;"> isReplication) { </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">.handleRegistration(info, leaseDuration, isReplication); </span><span style="color:rgb(0,0,255);line-height:1.5;">super</span><span style="line-height:1.5;">.register(info, leaseDuration, isReplication); }</span></pre></div><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"> <span style="color:rgb(0,0,255);line-height:1.5;">private</span> <span style="color:rgb(0,0,255);line-height:1.5;">void</span> handleRegistration(InstanceInfo info, <span style="color:rgb(0,0,255);line-height:1.5;">int</span> leaseDuration, <span style="color:rgb(0,0,255);line-height:1.5;">boolean</span><span style="line-height:1.5;"> isReplication) { </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.log("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication " +<span style="line-height:1.5;"> isReplication); </span><span style="color:rgb(0,0,255);line-height:1.5;">this</span>.publishEvent(<span style="color:rgb(0,0,255);line-height:1.5;">new</span> EurekaInstanceRegisteredEvent(<span style="color:rgb(0,0,255);line-height:1.5;">this</span><span style="line-height:1.5;">, info, leaseDuration, isReplication)); }</span></pre></div><p style="margin:10px auto;"> 在註冊函數中,先調用publishEvent 函數,將該新服務註冊的事件傳播出去,然後調用 com.netflix.eureka.registry.AbstractInstanceRegistry 父類中的註冊實現,將 InstanceInfo 中的元數據信息存儲在一個 ConcurrentHashMap 對象中。正如之前所說,註冊中心存儲了兩層 Map 結構,第一層的key 存儲服務名: InstanceInfo 中的APPName 屬性,第二層的 key 存儲實例名:InstanceInfo中的 instanceId 屬性。</p><h3 style="font-size:16px;line-height:1.5;margin-top:10px;margin-bottom:10px;"><a name="t15"></a>配置詳解</h3><p style="margin:10px auto;"> 在 Eureka 的服務治理體系中,主要分爲服務端和客戶端兩個不同的角色,服務端爲服務註冊中心,而客戶端爲各個提供接口的微服務應用。當我們構建了高可用的註冊中心之後,該集羣中所有的微服務應用和後續將要介紹的一些基礎類應用(如配置中心、API網關等)都可以視爲該體系下的一個微服務(Eureka客戶端)。服務註冊中心也一樣,只是高可用環境下的服務註冊中心除了服務端之外,還爲集羣中的其他客戶端提供了服務註冊的特殊功能。所以,Eureka 客戶端的配置對象存在於所有 Eureka 服務治理體系下的應用實例中。在使用使用 Spring cloud Eureka 的過程中, 我們所做的配置內容幾乎都是對 Eureka 客戶端配置進行的操作,所以瞭解這部分的配置內容,對於用好 Eureka 非常有幫助。</p><p style="margin:10px auto;"> Eureka 客戶端的配置主要分爲以下兩個方面:</p><ul style="margin-left:30px;"><li>服務註冊相關的配置信息,包括服務註冊中心的地址、服務獲取的間隔時間、可用區域等。</li><li>服務實例相關的配置信息,包括服務實例的名稱、IP地址、端口號、健康檢查路徑等。</li></ul><p style="margin:10px auto;"> </p><h3 style="font-size:16px;line-height:1.5;margin-top:10px;margin-bottom:10px;"><a name="t16"></a>服務註冊類配置</h3><p style="margin:10px auto;"> 關於服務註冊類的配置信息,我們可以通過查看 org.springframework.cloud.netflix.eureka.EurekaClientConfigBean 的源碼來獲得比官方文檔中更爲詳盡的內容,這些配置信息都已 eureka.client 爲前綴。下面針對一些常用的配置信息做進一步的介紹和說明。</p><h4 style="font-size:14px;margin-top:10px;margin-bottom:10px;"> 指定註冊中心</h4><p style="margin:10px auto;"> 在配置文件中通過 eureka.client.service-url 實現。該參數定義如下所示,它的配置值存儲在HashMap類型中,並且設置有一組默認值,默認值的key爲 defaultZone、value 爲 http://localhost:8761/eureka/,類名爲 EurekaClientConfigBean。</p><div class="cnblogs_code" style="background-color:rgb(245,245,245);border:1px solid rgb(204,204,204);padding:5px;margin:5px 0px;font-family:'Courier New';font-size:12px;"><pre style="margin-bottom:0px;white-space:pre-wrap;font-family:'Courier New';"><span style="color:rgb(0,0,255);line-height:1.5;">private</span> Map<String, String> serviceUrl = <span style="color:rgb(0,0,255);line-height:1.5;">new</span><span style="line-height:1.5;"> HashMap();
this.serviceUrl.put(“defaultZone”, “http://localhost:8761/eureka/”);
public static final String DEFAULT_URL = “http://localhost:8761/eureka/”;
public static final String DEFAULT_ZONE = “defaultZone”;
由於之前的服務註冊中心使用了 8082 端口,所以我們做了如下配置,來講應用註冊到對應的 Eureka 服務端中。
eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
當構建了高可用的服務註冊中心集羣時,可以爲參數的value 值配置多個註冊中心的地址(逗號分隔):
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
另外,爲了服務註冊中心的安全考慮,很多時候會爲服務註冊中心加入安全校驗。這個時候,在配置serviceUrl時,需要在value 值的 URL 中加入響應的安全校驗信息,比如: http://<username>:<password>@localhost:1111/eureka。其中<username>爲安全校驗信息的用戶名,<password>爲該用戶的密碼。
其他配置
這些參數均以 eureka.client 爲前綴。
服務實例類配置
關於服務實例類的配置信息,可以通過查看 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 的源碼來獲取詳細內容,這些配置均以 eureka.instance 爲前綴。
元數據
在 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 的配置信息中,有一大部分內容都是對服務實例元數據的配置,元數據是 Eureka 客戶端在向註冊中心發送註冊請求時,用來描述自身服務信息的對象,其中包含了一些標準化的元數據,比如服務名稱、實例名稱、實例IP、實例端口等用於服務治理的重要信息;以及一些用於負載均衡策略或是其他特殊用途的自定義元數據信息。
在使用 Spring Cloud Eureka 的時候,所有的配置信息都通過 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 進行加載,但在真正進行服務註冊時,還是會包裝成 com.netflix.appinfo.InstanceInfo 對象發送給 Eureka 客戶端。這兩個類的定義非常相似,可以直接查看 com.netflix.appinfo.InstanceInfo 類中的詳細定義來了解原聲的 Eureka 對元數據的定義。其中,Map<String, String> metaData = new ConcurrentHashMap<String, String>(); 是自定義的元數據信息,而其他成員變量則是標準化的元數據信息。Spring Cloud 的EurekaInstanceConfigBean 對原生元數據對象做了一些配置優化處理,在後續的介紹中會提到這些內容。
我們可以通過 eureka.instance.<properties>=<value> 的格式對標準化元數據直接進行配置,<properties> 就是 EurekaInstanceConfigBean 對象中的成員變量名。對於自定義元數據,可以通過 eureka.instance.metadataMap.<key>=<value> 的格式來進行配置。
接着,針對一些常用的元數據配置做進一步的介紹和說明。
實例名配置
實例名,即 InstanceInfo 中的 instanceId 參數,它是區分同一服務中不同實例的唯一標識。在NetflixEureka 的原生實現中,實例名採用主機名作爲默認值,這樣的設置使得在同一主機上無法啓動多個相同的服務實例。所以,在 Spring Cloud Eureka 的配置中,針對同一主機中啓動多實例的情況,對實例名的默認命名做了更爲合理的擴展,它採用瞭如下默認規則:
{spring.application.name}:{server.port}
對於實例名的命名規則,可以通過eureka.instance.instanceId 參數來進行配置。比如,在本地進行客戶端負載均衡調試時,需要啓動同一服務的多個實例,如果我們直接啓動同一個應用必然會發生端口衝突。雖然可以在命令行中指定不同的server.port 來啓動,但這樣略顯麻煩。可以直接通過設置 server.port=0 或者使用隨機數 server.port={spring.application.name}😒{random.int}
通過上面的配置,利用應用名+隨機數的方式來區分不同的實例,從而實現在同一個主機上,不指定端就能輕鬆啓動多個實例的效果。