SpringCloud--Eureka服務註冊和發現
Eureka是SpringCloud家族中的一個元件,因為它的有服務註冊和發現的機制,所以很適合用於做註冊中心。Eureka有服務端和客戶端,註冊中心作為服務端,我們提供的服務作為客戶端註冊到服務端上,由Eureka統一管理。
作為註冊中心,它內部執行機制是什麼樣的?下面我就帶著下面這些問題來學習Eureka。
1.如何去開發一個整合spring cloud eureka程式? 2.服務提供者怎麼註冊到服務中心的? 3.服務中心怎麼接收註冊請求? 4.服務中心怎麼儲存? 5.服務中心自身是怎麼實現高可用的? 6.服務叢集之間怎麼同步資訊?如何去重? 7.服務中心如何檢查服務提供者是否正常? 8.服務提供者如果下架服務?
1.如何去開發一個整合spring cloud eureka程式?
下面就開發一個偽叢集(在單機上)的Eureka程式:
服務端pom.xml檔案主要配置:
<!-- spring boot 封裝spring
starter封裝、自動配置autoconfiguration
-->
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId >
<version>Edgware.SR2</version>
<relativePath />
</parent>
<!-- 後面引用不用加版本號 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId >spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- spring-boot-starter-web web專案,整合容器tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring-boot-starter-actuator 管理工具/web 檢視堆疊,動態重新整理配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- cloud eureka元件 註冊中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
bootstrap.yml檔案:
# 應用名稱
spring:
application:
name: eureka-server
application.yml檔案:
# 上下文初始化載入
info:
name: Eureka server
contact: wenthkim
spring:
profiles:
active: eureka1
spring:
profiles: eureka1
server:
port: 8761
eureka:
client:
# 是否註冊到eurekaserver
registerWithEureka: false
# 是否拉取資訊
fetchRegistry: true
# eureka server地址
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
server:
# false 關閉自我保護,不管如何都要剔除心跳檢測異常的服務
enableSelfPreservation: true
# updatePeerEurekaNodes執行間隔
peerEurekaNodesUpdateIntervalMs: 10000000
waitTimeInMsWhenSyncEmpty: 0
instance:
hostname: eureka1
metadataMap:
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
啟動java類:
@SpringBootApplication
@EnableEurekaServer
public class EurekaApp {
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaApp.class).web(true).run(args);
}
}
下面把bootstrap.yml檔案的port和hostname(需要在自己電腦C:\Windows\System32\drivers\etc下的hosts檔案配置對應的域名)分別改成8761,8762,8763和eureka1,eureka2,eureka3,分別啟動Eureka程式即可搭建一個高可用的註冊中心。
啟動成功後,瀏覽器輸入http://localhost:8761/即看到成功介面
2.服務提供者怎麼註冊到服務中心的?
註冊到服務中心的主要都有哪些資訊:
ip、port(埠)、instance_id(例項id)、name(服務名)等。
當eureka客戶端啟動的時候,EurekaClientConfiguration讀書客戶端配置資訊,建立一個InstanceInfo例項,然後交給ApplicationInfoManager管理,下面看段程式碼:
@Configuration
@ConditionalOnMissingRefreshScope
protected static class EurekaClientConfiguration {
@Autowired
private ApplicationContext context;
@Autowired(required = false)
private DiscoveryClientOptionalArgs optionalArgs;
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config) {
return new CloudEurekaClient(manager, config, this.optionalArgs,
this.context);
}
@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
public ApplicationInfoManager eurekaApplicationInfoManager(
EurekaInstanceConfig config) {
//獲取配置 建立例項
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo);
}
}
在例項化過程中,DiscoveryClient的HeartbeatThread定時任務會不斷掃描,找到未註冊的例項,並註冊到服務中心,下面DiscoveryClient類的某段程式碼:
/**
* The heartbeat task that renews the lease in the given intervals.
* 心跳,註冊定時任務
*/
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
/**
* Renew with the eureka service by making the appropriate REST call
*/
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
//向註冊中心傳送一個註冊請求
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
//當註冊中心返回404,證明這個例項沒註冊,則進行註冊,否則返回註冊成功
if (httpResponse.getStatusCode() == 404) {
REREGISTER_COUNTER.increment();
logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
return register();
}
return httpResponse.getStatusCode() == 200;
} catch (Throwable e) {
logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
return false;
}
}
/**
* Register with the eureka service by making the appropriate REST call.
* 向服務端註冊方法
*/
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;
}
3.服務中心怎麼接收註冊請求?
通過rest介面接收,spring cloud 整合eureka server原生包中的Jersey RESTful介面,JerseyApi
下面跟著原始碼檢視接收流程(eureka server是通過filter攔截請求的),程式碼如下:
入口EurekaServerAutoConfiguration類下的jerseyFilterRegistration會增加一個filter用於攔截註冊請求
@Bean
public FilterRegistrationBean jerseyFilterRegistration(Application eurekaJerseyApp) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(2147483647);
//攔截上圖客戶端註冊請求
bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
return bean;
}
由ApplicationResource類下的addInstance受理請求,程式碼如下 :
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
if (this.isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (this.isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
} else if (this.isBlank(info.getAppName())) {
return Response.status(400).entity("Missing appName").build();
} else if (!this.appName.equals(info.getAppName())) {
return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();
} else if (info.getDataCenterInfo() == null) {
return Response.status(400).entity("Missing dataCenterInfo").build();
} else if (info.getDataCenterInfo().getName() == null) {
return Response.status(400).entity("Missing dataCenterInfo Name").build();
} else {
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
if (dataCenterInfo instanceof UniqueIdentifier) {
String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();
if (this.isBlank(dataCenterInfoId)) {
boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
if (experimental) {
String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
return Response.status(400).entity(entity).build();
}
if (dataCenterInfo instanceof AmazonInfo) {
AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;
String effectiveId = amazonInfo.get(MetaDataKey.instanceId);
if (effectiveId == null) {
amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());
}
} else {
logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
}
}
}
//進行註冊,註冊成功返回204,isReplication防止迴圈傳播
this.registry.register(info, "true".equals(isReplication));
return Response.status(204).build();
}
}
呼叫InstanceRegistry類的register方法註冊成功併發布EurekaInstanceRegisteredEvent註冊事件,程式碼如下:
/**
* 註冊併發布註冊事件
*/
public void register(InstanceInfo info, boolean isReplication) {
this.handleRegistration(info, this.resolveInstanceLeaseDuration(info), isReplication);
super.register(info, isReplication);
}
/**
* 釋出註冊事件
*/
private void handleRegistration(InstanceInfo info, int leaseDuration, boolean isReplication) {
this.log("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication " + isReplication);
this.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
}
4.服務中心怎麼儲存?
檢視AbstractInstanceRegistry類下的register方法可以清楚地看到客戶端資訊是存在ConcurrentHashMap裡面的。
5.服務中心自身是怎麼實現高可用的?
通過對等的eureka server例項,當eureka服務端啟動時,會找到配置檔案所填其它服務端地址,相互註冊。例子,比如現在有eureka1,eureka2,eureka3。當有服務註冊到eureka1時,eureka1會發出注冊事件,此時eureka2,eureka3會同步這個例項資訊。
6.服務叢集之間怎麼同步資訊?如何去重?
eurekaserver初始化時,維護了一個PeerEurekaNodes.peerEurekaNodes列表,
當需要同步更新資訊的時候, 遍歷所有節點,並傳播資訊,會呼叫PeerAwareInstanceRegistryImpl類的這幾個方法,cancel/register/renew,程式碼如下:
public boolean cancel(String appName, String id, boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Cancel, appName, id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
Object var4 = this.lock;
synchronized(this.lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin -= 2;
this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin * this.serverConfig.getRenewalPercentThreshold());
}
return true;
}
} else {
return false;
}
}
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);
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
}
public boolean renew(String appName, String id, boolean isReplication) {
if (super.renew(appName, id, isReplication)) {
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Heartbeat, appName, id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
return true;
} else {
return false;
}
}
/**
* 遍歷所有節點
*/
private void replicateToPeers(PeerAwareInstanceRegistryImpl.Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
this.numberOfReplicationsLastMin.increment();
}
if (this.peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
Iterator var8 = this.peerEurekaNodes.getPeerEurekaNodes().iterator();
while(var8.hasNext()) {
PeerEurekaNode node = (PeerEurekaNode)var8.next();
if (!this.peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
this.replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
}
} finally {
tracer.stop();
}
}
/**
* 對不同的事件進行處理
*/
private void replicateInstanceActionsToPeers(PeerAwareInstanceRegistryImpl.Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
CurrentRequestVersion.set(Version.V2);
switch(action) {
case Cancel:
node.cancel(appName, id);
break;
case Heartbeat:
InstanceStatus overriddenStatus = (InstanceStatus)this.overriddenInstanceStatusMap.get(id);
infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
node.register(info);
break;
case StatusUpdate:
infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
infoFromRegistry = this.getInstanceByAppAndId(appName, id, false);
node.deleteStatusOverride(appName, id, infoFromRegistry);
}
} catch (Throwable var9) {
logger.error("Cannot replicate information to {} for action {}", new Object[]{node.getServiceUrl(), action.name(), var9});
}
}
7.服務中心如何檢查服務提供者是否正常?
服務剔除: 長時間沒有給心跳 預設90秒
當eureka啟動初始化上下文的時候,會啟動一個定時任務EvictionTask(檢測異常服務)。
程式碼有點多就不一一貼出來了。程式碼入口為EurekaServerInitializerConfiguration類的start方法,最後調到AbstractInstanceRegistry類的postInit啟動EvictionTask。
當eureka server開啟自我保護的情況下,不會剔除任何服務。即eureka.server.enableSelfPreservation=true的時候。自我保護例子:
當一個服務有10個例項,預設提交心跳時間為30秒。
此時閥值為0.8,
那麼每分鐘需要收到心跳包的個數為:10*0.8*2=16,
如果一分鐘內,eureka收到的心跳請求沒達到16個,eureka則懷疑是網路抖動,此時不會剔除任何服務。
8.服務提供者如果下架服務?
當spring上下文關閉時下架eureka服務,程式入口EurekaDiscoveryClientConfiguration類的onApplicationEvent方法,最終呼叫DiscoveryClient類的shutdown,unregister,cancel方法下架服務,程式碼如下:
@EventListener(ContextClosedEvent.class)
public void onApplicationEvent(ContextClosedEvent event) {
// register in case meta data changed
stop();
this.eurekaClient.shutdown();
}
/**
* Shuts down Eureka Client. Also sends a deregistration request to the
* eureka server.
*/
@PreDestroy
@Override
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
/**
* unregister w/ the eureka service.
*/
void unregister() {
// It can be null if shouldRegisterWithEureka == false
if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
try {
logger.info("Unregistering ...");
EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
logger.info(PREFIX + appPathIdentifier + " - deregister status: " + httpResponse.getStatusCode());
} catch (Exception e) {
logger.error(PREFIX + appPathIdentifier + " - de-registration failed" + e.getMessage(), e);
}
}
}
從此開始微服務的學習之旅,本人正在入門學習,如理解有誤歡迎指正,歡迎有興趣的小夥伴一起交流。