Spring Cloud 服務註冊與發現 Eureka
一、Eureka簡介
二、例項
一、Eureka簡介
主要用於服務的註冊發現。Eureka由兩個元件組成:Eureka伺服器和Eureka客戶端。
二、核心概念
1.Register:服務註冊
當Eureka客戶端向Eureka Server註冊時,它提供自身的元資料,比如IP地址、埠,執行狀況指示符URL,主頁等。
2.Renew:服務續約
Eureka客戶會每隔30秒傳送一次心跳來續約。 通過續約來告知Eureka Server該Eureka客戶仍然存在,沒有出現問題。 正常情況下,如果Eureka Server在90秒沒有收到Eureka客戶的續約,它會將例項從其登錄檔中刪除。 建議不要更改續約間隔。
3.Fetch Registries:獲取註冊列表資訊
Eureka客戶端從伺服器獲取登錄檔資訊,並將其快取在本地。客戶端會使用該資訊查詢其他服務,從而進行遠端呼叫。該註冊列表資訊定期(每30秒鐘)更新一次。每次返回註冊列表資訊可能與Eureka客戶端的快取資訊不同, Eureka客戶端自動處理。如果由於某種原因導致註冊列表資訊不能及時匹配,Eureka客戶端則會重新獲取整個登錄檔資訊。 Eureka伺服器快取註冊列表資訊,整個登錄檔以及每個應用程式的資訊進行了壓縮,壓縮內容和沒有壓縮的內容完全相同。Eureka客戶端和Eureka 伺服器可以使用JSON / XML格式進行通訊。在預設的情況下Eureka客戶端使用壓縮JSON格式來獲取註冊列表的資訊。
4.Cancel:服務下線
Eureka客戶端在程式關閉時向Eureka伺服器傳送取消請求。 傳送請求後,該客戶端例項資訊將從伺服器的例項登錄檔中刪除。該下線請求不會自動完成,它需要呼叫以下內容:
DiscoveryManager.getInstance().shutdownComponent();
5.Eviction 服務剔除
在預設的情況下,當Eureka客戶端連續90秒沒有向Eureka伺服器傳送服務續約,即心跳,Eureka伺服器會將該服務例項從服務註冊列表刪除,即服務剔除。
三、Eureka服務註冊
客戶端:
①DiscoveryClient初始化時,其中有一個initScheduledTasks()方法。該方法主要開啟了獲取服務註冊列表的資訊,如果需要向Eureka Server註冊,則開啟註冊,同時開啟了定時向Eureka Server服務續約的定時任務。
private void initScheduledTasks() { ...//省略了任務排程獲取註冊列表的程式碼 if (clientConfig.shouldRegisterWithEureka()) { ... // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); // InstanceInfo replicator instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return "statusChangeListener"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { instanceInfoReplicator.onDemandUpdate(); } }; ... }
②在①中呼叫了InstanceInfoReplicator 類的run()方法
public void run() { try { discoveryClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoveryClient.register(); instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } }
③ 在②中呼叫了DiscoveryClient類方法register(),該方法是通過Http請求向Eureka Client註冊
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
服務端:
④ApplicationResource類的addInstance ()就是服務端的入口
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
...//省略程式碼
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
⑤上面的register是呼叫的PeerAwareInstanceRegistryImpl的register()
public void register(InstanceInfo info, boolean isReplication) {
int leaseDuration = 90;
if(info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
////呼叫父類方法註冊
super.register(info, leaseDuration, isReplication);
// 同步Eureka中的服務資訊
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
}
⑥AbstractInstanceRegistry的register()
PeerAwareInstanceRegistryImpl繼承了AbstractInstanceRegistry,在PeerAwareInstanceRegistryImpl呼叫了父類的register,
AbstractInstanceRegistry有個成員變數來儲存註冊資訊:
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();
register方法: public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
this.read.lock();
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
EurekaMonitors.REGISTER.increment(isReplication);
if (gMap == null) {
ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
if (existingLease != null && existingLease.getHolder() != null) {
Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = (InstanceInfo)existingLease.getHolder();
}
} else {
Object var6 = this.lock;
synchronized(this.lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin += 2;
this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin * this.serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
((Map)gMap).put(registrant.getId(), lease);
AbstractInstanceRegistry.CircularQueue var20 = this.recentRegisteredQueue;
synchronized(this.recentRegisteredQueue) {
this.recentRegisteredQueue.add(new Pair(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() + ")"));
}
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 (!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})", new Object[]{registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication});
} finally {
this.read.unlock();
}
}
四、服務續約
服務端的續約介面在eureka-core:1.6.2.jar的 com.netflix.eureka包下的InstanceResource類下,介面方法為renewLease(),它是REST介面。為了減少類篇幅,省略了大部分程式碼的展示。其中有個registry.renew()方法,即服務續約三、Eureka特殊說明
1.Eureka的自我保護機制
預設情況下,如果Eureka Server在一定時間內沒有接收到某個微服務例項的心跳,Eureka Server將會登出該例項(預設90秒)。但是當網路分割槽故障發生時,微服務與Eureka Server之間無法正常通訊,這就可能變得非常危險了----因為微服務本身是健康的,此時本不應該登出這個微服務。
Eureka Server通過“自我保護模式”來解決這個問題----當Eureka Server節點在短時間內丟失過多客戶端時(可能發生了網路分割槽故障),那麼這個節點就會進入自我保護模式。一旦進入該模式,Eureka Server就會保護服務登錄檔中的資訊,不再刪除服務登錄檔中的資料(也就是不會登出任何微服務)。當網路故障恢復後,該Eureka Server節點會自動退出自我保護模式。
自我保護模式是一種對網路異常的安全保護措施。使用自我保護模式,而已讓Eureka叢集更加的健壯、穩定。
在Spring Cloud中,可以使用eureka.server.enable-self-preservation=false來禁用自我保護模式
2.Eureka的註冊延遲
⑴Eureka Client一啟動(不是啟動完成),不是立即向Eureka Server註冊,它有一個延遲向服務端註冊的時間,通過跟蹤原始碼,可以發現預設的延遲時間為40秒,原始碼在eureka-client-1.6.2.jar的DefaultEurekaClientConfig類下,程式碼如下:
public int getInitialInstanceInfoReplicationIntervalSeconds() {
return configInstance.getIntProperty(
namespace + INITIAL_REGISTRATION_REPLICATION_DELAY_KEY, 40).get();
}
⑵Eureka Server的響應快取 Eureka Server維護每30秒更新的響應快取,可通過更改配置eureka.server.responseCacheUpdateIntervalMs來修改。 所以即使例項剛剛註冊,它也不會出現在呼叫/ eureka / apps REST端點的結果中。
⑶Eureka Server重新整理快取
Eureka客戶端保留登錄檔資訊的快取。 該快取每30秒更新一次(如前所述)。 因 此,客戶端決定重新整理其本地快取並發現其他新註冊的例項可能需要30秒。
⑷LoadBalancer Refresh
Ribbon的負載平衡器從本地的Eureka Client獲取服務註冊列表資訊。Ribbon本身還維護本地快取,以避免為每個請求呼叫本地客戶端。 此快取每30秒重新整理一次(可由ribbon.ServerListRefreshInterval配置)。 所以,可能需要30多秒才能使用新註冊的例項。
綜上幾個因素,一個新註冊的例項,特別是啟動較快的例項(預設延遲40秒註冊),不能馬上被Eureka Server發現。另外,剛註冊的Eureka Client也不能立即被其他服務呼叫,因為呼叫方因為各種快取沒有及時的獲取到新的註冊列表。
四、例項
參考文章:http://blog.csdn.net/forezp/article/details/69696915 在myeclipse 環境下實現
我們需要創立一個Eureka Server和一個Eureka Client
1.建立服務註冊中心
1.1首先建立MAVEN工程,加入依賴:
<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.spring.cloud</groupId>
<artifactId>java-maven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<name>eurekaserver</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.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>
</properties>
<dependencies>
<!--eureka server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<!-- spring boot test-->
<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>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
1.2啟動一個服務註冊中心
package Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}
1.3配置檔案
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false //
fetchRegistry: false //這兩個false可以防止Eureka註冊自身
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
1.4啟動後介面沒有服務自然也沒有服務被發現
2.建立服務提供者
2.1首先建立MAVEN工程,加入依賴
<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>test</groupId>
<artifactId>java-maven-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-hi</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.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>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</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>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
2.2啟動一個客戶端:package test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceHiApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceHiApplication.class, args);
}
@Value("${server.port}")
String port;
@RequestMapping("/hi")
public String home(@RequestParam String name) {
return "hi "+name+",i am from port:" +port;
}
}
2.3配置檔案eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8762
spring:
application:
name: service-hi
2.4啟動
然後我們訪問:
http://localhost:8762/hi?name=heidou
可以在頁面看到: