在前兩章介紹了微服務簡介,spring-cloud簡介這一章學習spring cloud中的第一個組件,Eureka。這是一個用于服務注冊和發(fā)現(xiàn)的組件。分為Eureka Server和Eureka Client,Eureka Server為Eureka服務注冊中心,Eureka Client為Eureka客戶端。
基本架構
Eureka主要包括三種角色:
Register Service:服務注冊中心,他是一個Eureka Server,提供服務注冊和發(fā)現(xiàn)的功能。
Provider Service:服務提供者,它是一個Eureka Client,提供服務。
Consumer Service:服務消費者,它是一個Eureka Client,消費服務。
服務消費的基本流程
首先需要啟動一個Eureka Server,做為服務注冊中心,服務提供者Eureka Client向服務注冊中心Eureka Server注冊,將自己的服務名和IP地址等信息通過REST API的形式提交給服務注冊中心Eureka Server。同樣,服務消費者Eureka Client也向服務注冊中心Eureka Server注冊,同時服務消費者獲取一份服務注冊列表的信息,該列表包含了所有向服務注冊中心Eureka Server注冊的服務信息。獲取服務注冊列表信息之后,服務消費者就知道服務提供者的IP地址,可以通過Http遠程調度來消費服務提供者的服務。
編寫Eureka Server
第一步:引入Eureka Server的起步依賴spring-cloud-starter-eureka-server,以及spring boot測試的起步依賴spring-boot-starter-test。最后引入spring boot的maven插件spring-boot-maven-plugin,該插件的作用是可以試用maven插件的方式來啟動spring boot工程。
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
第二步:在application.yml中做程序的相關配置。Eureka Server會向自己注冊,這時需要配置eureka.client.registerWithEureka和eureka.client.fetchRegistry為false,防止自己注冊自己。配置如下:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
最后在工程啟用類加上注解@EnableEurekaServer,開啟Eureka Server的功能。代碼如下:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
啟動項目,訪問http://localhost:8761,可以在瀏覽器查看Eureka Server的主界面。在界面Instance currently registered with Eureka這一項上沒有任何注冊的實列,接下來編寫Eureka Client,并注冊到Eureka Server。
編寫Euraka Client
引入依賴:
<dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在工程配置文件application.yml中做相關配置。其中defaultZone為默認的Zone,來源于AWS的概念。區(qū)域(Region)和可用區(qū)(Availability Zone,AZ)是AWS的另外兩個概念。區(qū)域是指服務器所在的區(qū)域,比如北美洲、歐洲、亞洲等,每個區(qū)域一般由多個可用區(qū)組成。在配置中defaultZone是指Eureka Server的注冊地址。
server:
port: 8763
spring:
application:
name: eureka-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
在項目啟動類上加上注解@EnableEurekaClient開啟Eureka Client功能。代碼如下:
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
啟動Client,啟動成功后,控制臺會打印出如下信息:
DiscoveryClient_EUREKA-CLIENT/DESKTOP-NB1C2U4:eureka-client:8763 - registration status: 204
說明已經(jīng)向Eureka Server注冊了。在瀏覽器訪問http://localhost:8761,可在主界面看到有一個實列注冊,Application為EUREKA-CLIENT,Status為UP,端口為8763,說明注冊成功。
構建高可用的Eureka Server集群
在實際的項目中,可能由幾十個或者幾百個的微服務實例,這時Eureka Server承擔了非常高的負載。由于Eureka Server在微服務架構中有著舉足輕重的作用,所以需要對Eureka Server進行高可用集群。
第一步更改eureka-server的配置文件application.yml,在配置文件中采用多profile的格式,具體代碼如下:
spring:
profiles: peer1
server:
port: 8761
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://peer2:8762/eureka/
---
spring:
profiles: peer2
server:
port: 8762
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://peer1:8761/eureka/
由于是本地代建Eureka Server集群,需要在本機hosts文件做域名映射。
127.0.0.1 peer1
127.0.0.1 peer2
通過mvn clean package編譯項目,成功后在目錄target文件夾下生成jar包。通過java -jar命令并指定spring-profiles-active參數(shù)。命令如下:
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-client配置文件,注意其中只是向8761端口的服務器注冊,配置如下:
server:
port: 8763
spring:
application:
name: eureka-client
eureka:
client:
service-url:
defaultZone: http://peer1:8761/eureka/
啟動eureka-client,訪問 http://localhost:8762/,在DS Replicas選項中顯示了其它多實例節(jié)點。此時發(fā)現(xiàn)eureka-client在配置文件中只是向8761端口的server注冊,但在8762端口的服務器同樣可以看到eureka-client的注冊信息,可見peer1的注冊列表信息已經(jīng)同步到了peer2節(jié)點。
Eureka的一些概念
- Register-服務注冊
當Eureka Client向Eureka Server注冊時,client提供自身的元數(shù)據(jù),如IP地址、端口、運行狀態(tài)指標的Url、主頁地址等信息。 - Renew-服務續(xù)約
Eureka Client默認情況下每隔30秒發(fā)送一次心跳來進行服務續(xù)約。如果Eureka Server在90秒內沒有收到Eureka Client的心跳,Eureka Server會將Eureka Client實例從注冊列表中剔除。 - Fetch Registries-獲取服務注冊列表信息
Eureka Client從Eureka Server獲取服務注冊表信息,并緩存在本地,可根據(jù)注冊表信息查找服務,從而進行遠程調用。該注冊表信息每30秒更新一次。如果由于某種原因導致服務注冊列表信息不能及時匹配,Eureka Client會重新獲取整個注冊表信息。 - Cancel-服務下線
Eureka Client在程序關閉時可以向Eureka Server發(fā)送下線信息。發(fā)送請求后,該客戶端的實例信息將從Eureka Server的服務注冊列表中刪除。該下線請求不會自動完成,需要在程序關閉時調用以下代碼:
DiscoveryManager.getInstance().shutdownComponent();
- Eviction-服務剔除
默認情況下,當Eureka Client連續(xù)90秒沒有向Eureka Server發(fā)送服務續(xù)約(即心跳)時,Eureka Server會將該服務實例從服務注冊列表刪除。 - Eureka的自我保護模式
當一個新的Eureka Server出現(xiàn)時,會嘗試從相鄰的Peer節(jié)點獲取所有服務實例的注冊信息。如果從相鄰的Peer節(jié)點獲取信息時出現(xiàn)了故障,Eureka Server會嘗試其它的Peer節(jié)點。如果Eureka Server能夠成功獲取所有的服務實例信息,則根據(jù)配置信息設置服務續(xù)約的閾值。在任何時間,如果Eureka Server接收到的服務續(xù)約低于該配置的百分比(默認為15分鐘內低于85%),則服務器開啟自我保護模式,即不在剔除注冊列表的信息。這樣做的好處是如果是Eureka Server自身的網(wǎng)絡問題而導致Eureka Client無法續(xù)約,這樣在注冊列表中不會剔除注冊信息,這樣Eureka Client還可以被其他服務消費。同時這個功能也是有坑的,如果在服務較少時,服務由于意外情況掛掉后很容易閾值就低于85%,由于自我保護導致掛掉的服務在注冊列表中還是存在,但其它服務無法消費,這會迷惑開發(fā)人員排錯的方向。如果需要關閉該功能,在配置文件中添加如下代碼:
eureka:
server:
enable-self-preservation: false
源碼解析
接下來我們通過debug的形式,學習Eureka Client是如何進行注冊的。在工程的Maven的依賴包下,找到eureka-client-1.6.2.jar包,在com.netflix.discovery包下有一個DiscoverClient類,這個類是Eureka Client和Eureka Server交互的核心類。
啟動項目后首先會進入DiscoverClient的initScheduledTasks方法。在該方法中初始化并封裝了刷新服務注冊列表信息和發(fā)送心跳的定時任務。
在初始化定時任務后進入DiscoverClient的register()方法
具體進入EurekaHttpClient的register方法,可以看出通過http請求的Eureka Server路徑,以及攜帶的請求信息。
同時來跟蹤Eureka Server端的代碼,Eureka server服務端請求入口為
ApplicationResource類,如下所示,可以看出Eureka Client是通過http post的方式去服務注冊。
通過一系列的參數(shù)校驗。最后以以下方法進行注冊。
registry.register(info, "true".equals(isReplication));
進入register方法,實際執(zhí)行的是PeerAwareInstanceRegistryImpl的register方法,該方法如下:
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
//調用父類的注冊方法
super.register(info, leaseDuration, isReplication);
// 注冊成功后同步Eureka中的服務注冊信息
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
進入父類的注冊方法,在AbstractInstanceRegistry類中可以看到Eureka真正的服務注冊實現(xiàn)的代碼。
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = 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 = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// Since the client wants to cancel it, reduce the threshold
// (1
// for 30 seconds, 2 for a minute)
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
在該類中服務注冊信息其實就是存儲在一個 ConcurrentHashMap結構中。
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
流程總結:
Eureka Client:在DiscoveryClient先通過initScheduledTasks()方法封裝定時任務,然后調用register()方法通過http訪問服務器接口進行注冊。
Eureka Server:ApplicationResource類接收Http服務請求,調用PeerAwareInstanceRegistryImpl的register方法,PeerAwareInstanceRegistryImpl完成服務注冊后,調用replicateToPeers向其它Eureka Server節(jié)點(Peer)做狀態(tài)同步。
在以上只是對服務注冊進行了源碼跟蹤,感興趣的可以以同樣的方式對服務續(xù)約、服務剔除等進行源碼學習。
總結
在這一章節(jié)中,我們學習了Eureka的概念、架構。如何編寫Eureka Server和Eureka Client,并進行了Eureka Server高可用的演示。最后以服務注冊為列進行了源碼學習。在下一章學習如何使用RestTemplate和Ribbon結合作為服務消費者去消費服務。
PS:項目github地址:https://github.com/dzydzydzy/spring-cloud-example.git