SpringCloud服務治理-搭建一個實用的Eureka Server
Eureka的主要作用是實現各個微服務例項的自動化註冊與發現。
服務治理框架中,通常都會構建一個註冊中心, Spring Cloud Eureka 就扮演著這樣的角色。
Eureka Server即服務註冊中心,Eureka Client(微服務服務提供方) 主要處理服務的註冊與發現 ,Eureka 客戶端向註冊中心註冊自身提供的服務並週期性的傳送心跳來更新它的服務租約。
下面搭建了一個實用的服務註冊中心,並將其相關配置和行為記錄進行來簡要說明。
1 . 首先新建專案springcloud-eureka-server,加入基本依賴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>name.ealen</groupId><artifactId>springcloud-eureka-server</artifactId> <version>1.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-eureka-server</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Edgware.SR4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
2 . application.yml 應用的基本配置,以及Eureka的基本屬性配置,以下有部分基本上都是Eureka的預設值,但為了方便深入學習和加強理解,我將其寫在了裡面並做了說明。
特別注意,在單例項情況下 :
1).register-with-eureka,表示是否註冊自身到eureka伺服器,它的預設值為true,對註冊中心而言,是沒必要將自己註冊上去。
2).fetch-registry,表示是否從Eureka Server上拉取服務資訊,它的預設值為true,Eureka Server本身要做的就是集大成者,初始化啟動是不能拉取到任何服務資訊的,請設定為false
3).Eureka Server如果配置多個例項,即配置高可用,上面兩個預設值即可。多例項實際上就是將自己作為服務向其他服務註冊中心註冊自已,這樣就可以形成一組互相註冊的服務註冊中心,以實現服務清單的互相同步,達到高可用的效果。
server:
port: 8761
spring:
application:
name: springcloud-eureka-server
datasource:
url: jdbc:mysql://localhost:3306/yourdatabase
username: yourname
password: yourpass
jpa:
hibernate:
ddl-auto: create # 請自行修改,請自行修改,請自行修改
## 單例項配置
eureka:
instance:
hostname: localhost # 服務註冊中心例項的主機名
lease-renewal-interval-in-seconds: 30 # 客戶端向Eureka傳送心跳週期(s)
lease-expiration-duration-in-seconds: 90 # Eureka Server接收例項的最後一次發出的心跳後,刪除需要等待時間(s)
server:
enable-self-preservation: true # Eureka自我保護模式
client:
enabled: true # 啟用Eureka客戶端
register-with-eureka: false # 是否向服務註冊中心註冊自己
fetch-registry: false # 是否檢索發現服務
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 指定服務註冊中心的位置
registry-fetch-interval-seconds: 30 # Eureka client拉取服務註冊資訊間隔時間(s)
initial-instance-info-replication-interval-seconds: 40 # 最初複製例項資訊到Eureka伺服器所需時間(s)
instance-info-replication-interval-seconds: 30 # 複製例項變化資訊到Eureka伺服器所需要的時間間隔(s)
eureka-service-url-poll-interval-seconds: 300 # 輪詢Eureka服務端地址更改的間隔時間(s)
eureka-server-read-timeout-seconds: 8 # 讀取Eureka Server資訊的超時時間(s)
eureka-server-connect-timeout-seconds: 5 # 連線Eureka Server的超時時間(s)
eureka-server-total-connections: 200 # 從Eureka客戶端到所有Eureka服務端的連線總數
eureka-server-total-connections-per-host: 50 # 從Eureka客戶端到每個Eureka服務端主機的連線總數
eureka-connection-idle-timeout-seconds: 30 # Eureka服務端連線的空閒關閉時間(s)
heartbeat-executor-thread-pool-size: 2 # 心跳連線池的初始化執行緒數
cache-refresh-executor-thread-pool-size: 2 # 快取重新整理執行緒池的初始化執行緒數
## 關於Eureka自我保護模式
## 預設情況下,如果Eureka Server在一定時間內沒有接收到某個微服務例項的心跳,Eureka Server將會登出該例項(預設90秒)。
## 但是當網路分割槽故障發生時,微服務與Eureka Server之間無法正常通訊,這就可能變得非常危險了--因為微服務本身是健康的,此時本不應該登出這個微服務。
## Eureka Server通過'自我保護模式'來解決這個問題,當Eureka Server節點在短時間內丟失過多客戶端時(可能發生了網路分割槽故障),那麼這個節點就會進入自我保護模式。
## 一旦進入該模式,Eureka Server就會保護服務登錄檔中的資訊,不再刪除服務登錄檔中的資料(也就是不會登出任何微服務)。當網路故障恢復後,該Eureka Server節點會自動退出自我保護模式。
## 自我保護模式是一種對網路異常的安全保護措施。使用自我保護模式,而已讓Eureka叢集更加的健壯、穩定。
## 多例項配置(即高可用)
## Eureka高可用實際上就是將自己作為服務向其他服務註冊中心註冊自已,這樣就可以形成一組互相註冊的服務註冊中心,以實現服務清單的互相同步,達到高可用的效果。
#---
#server:
# port: 8761
#eureka:
# instance:
# hostname: localhost # host1
# client:
# service-url:
# defaultZone: http://${eureka.instance.hostname}:8762/eureka/ # 指定服務註冊中心例項2的位置
#spring:
# profiles: host1
#
#---
#server:
# port: 8762
#eureka:
# instance:
# hostname: localhost # host2
# client:
# service-url:
# defaultZone: http://${eureka.instance.hostname}:8761/eureka/ # 指定服務註冊中心例項1的位置
#spring:
# profiles: host2
3 . 為了對EurekaServer及註冊的EurekaClient的相關屬性進行記錄,這裡自定義相關屬性(參考InstanceInfo,EurekaClientConfigBean等例項原始碼屬性)
package name.ealen.model; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import java.util.Date; /** * Created by EalenXie on 2018/9/20 14:46. * 要記錄的Eureka Server資訊例項(這裡示例,按照自己需要自定義) */ @Table @Entity public class EurekaEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String uuid; //Eureka Server的唯一標識 private String applicationName; //Eureka Server的應用名 private String profile; //Eureka Server的應用啟動Profile private String defaultZone; //Eureka Server指定服務註冊中心的位置 private boolean enableSelfPreservation; //Eureka Server是否開啟自我保護模式 private String hostname; //Eureka Server的hostname private String instanceId; //Eureka Server的InstanceId private Integer leaseRenewalInterval; //Eureka Client向Eureka Server傳送心跳週期(s) private Integer leaseExpirationDuration; //Eureka Server接收例項的最後一次發出的心跳後,刪除需要等待時間(s) private String status; //Eureka Server的當前狀態 private Integer registryFetchInterval; //Eureka Server拉取服務註冊資訊間隔時間(s) private Integer instanceReplicationInterval; //Eureka Server複製例項變化資訊到Eureka伺服器所需要的時間間隔(s) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; //Eureka Server的本次啟動時間點 //Getter Setter........ }
package name.ealen.model; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import java.util.Date; /** * Created by EalenXie on 2018/9/21 14:20. * 要記錄的Eureka Client資訊例項(這裡示例,按照自己需要自定義) */ @Table @Entity public class EurekaClientEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String uuid; private String applicationName; //Eureka Client的appName private String instanceId; //Eureka Client的instanceId private String hostname; //Eureka Client的hostname private String status; //Eureka Client在Eureka Server上面的狀態 private String homePageUrl; //Eureka Client的首頁Url private String statusPageUrl; //Eureka Client的狀態頁Url private String healthCheckUrl; //Eureka Client的健康檢查頁Url private String eurekaInstanceId; //Eureka Client註冊的Eureka Server的InstanceId @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date registerTime; //Eureka Client註冊到Eureka Server的時間 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date lastLeaveTime; //Eureka Client上次的下線時間 //Getter Setter ....... }
4 . 新增基本的資料庫訪問EurekaRepository,EurekaClientRepository :
package name.ealen.dao; import name.ealen.model.EurekaEntity; import org.springframework.data.jpa.repository.JpaRepository; /** * Created by EalenXie on 2018/9/20 15:04. */ public interface EurekaRepository extends JpaRepository<EurekaEntity, Integer> { EurekaEntity findByInstanceId(String instanceId); }
package name.ealen.dao; import name.ealen.model.EurekaClientEntity; import org.springframework.data.jpa.repository.JpaRepository; /** * Created by EalenXie on 2018/9/20 15:04. */ public interface EurekaClientRepository extends JpaRepository<EurekaClientEntity, Integer> { EurekaClientEntity findByInstanceId(String instanceId); }
5 . Eureka重要的幾個動作, Eureka Server啟動,服務註冊,服務續約(監聽服務的心跳),服務下線(將掛掉的服務踢下線) 都可以通過相應的事件進行監聽並進行相關處理。
1).本例中首先為這些相關事件新增一些業務處理(只是記錄EurekaServer和Client相關配置)
package name.ealen.function; import com.netflix.appinfo.InstanceInfo; import name.ealen.dao.EurekaClientRepository; import name.ealen.dao.EurekaRepository; import name.ealen.model.EurekaClientEntity; import name.ealen.model.EurekaEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; import org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; import java.util.UUID; /** * Created by EalenXie on 2018/9/20 15:37. */ @Component public class EurekaEventHandler { private static final Logger log = LoggerFactory.getLogger(EurekaEventHandler.class); @Resource private EurekaRepository eurekaRepository; @Resource private EurekaClientRepository eurekaClientRepository; @Resource private EurekaInstanceConfigBean eurekaInstance; @Resource private EurekaClientConfigBean eurekaClientConfigBean; /** * 獲取Application Profiles */ private String getApplicationProfile(StandardEnvironment environment) { StringBuilder profile = new StringBuilder(environment.getDefaultProfiles()[0]); if (environment.getActiveProfiles().length != 0) { profile = new StringBuilder(environment.getActiveProfiles()[0]); if (environment.getActiveProfiles().length > 1) { for (int i = 1; i < environment.getActiveProfiles().length; i++) { profile.append(",").append(environment.getActiveProfiles()[i]); } } } return profile.toString(); } /** * Eureka啟動,記錄Eureka啟動的資訊(自定義EurekaEntity) * * @param eureka EurekaServer服務例項資訊 */ @Async public void recordEurekaStartUp(EurekaServerConfigBean eureka) { try { StandardEnvironment environment = (StandardEnvironment) eureka.getPropertyResolver(); String profile = getApplicationProfile(environment); MapPropertySource clientHostInfo = (MapPropertySource) environment.getPropertySources().get("springCloudClientHostInfo"); if (clientHostInfo != null) { EurekaEntity eurekaEntity; String hostname = eurekaInstance.getHostname(); log.info("Eureka Server Start by hostname : {}", hostname); String applicationName = eurekaInstance.getAppname(); String instanceId = eurekaInstance.getInstanceId(); log.info("Eureka Server Start with InstanceId : {}", instanceId); String defaultZone = eurekaClientConfigBean.getServiceUrl().get("defaultZone"); log.info("Eureka Server defaultZone : {}", defaultZone); boolean isEnableSelfPreservation = eureka.isEnableSelfPreservation(); log.info("Eureka Server enable-self-preservation : {}", isEnableSelfPreservation); Integer leaseRenewInterval = eurekaInstance.getLeaseRenewalIntervalInSeconds(); log.info("Eureka Server lease-renewal-interval-in-seconds : {}s", leaseRenewInterval); Integer leaseExpirationDuration = eurekaInstance.getLeaseExpirationDurationInSeconds(); log.info("Eureka Server lease-expiration-duration-in-seconds : {}s", leaseExpirationDuration); Integer registryFetchInterval = eurekaClientConfigBean.getRegistryFetchIntervalSeconds(); log.info("Eureka Server registry-fetch-interval-seconds : {}s", registryFetchInterval); Integer replicationInterval = eurekaClientConfigBean.getInstanceInfoReplicationIntervalSeconds(); log.info("Eureka Server instance-info-replication-interval-seconds : {}s", replicationInterval); if (applicationName != null) { eurekaEntity = eurekaRepository.findByInstanceId(instanceId); if (eurekaEntity == null) { eurekaEntity = new EurekaEntity(); eurekaEntity.setApplicationName(applicationName); eurekaEntity.setHostname(hostname); eurekaEntity.setInstanceId(instanceId); eurekaEntity.setUuid(UUID.randomUUID().toString().replace("-", "")); } eurekaEntity.setStartTime(new Date()); eurekaEntity.setProfile(profile); eurekaEntity.setEnableSelfPreservation(isEnableSelfPreservation); eurekaEntity.setDefaultZone(defaultZone); eurekaEntity.setRegistryFetchInterval(registryFetchInterval); eurekaEntity.setInstanceReplicationInterval(replicationInterval); eurekaEntity.setLeaseRenewalInterval(leaseRenewInterval); eurekaEntity.setLeaseExpirationDuration(leaseExpirationDuration); eurekaEntity.setStatus(eurekaInstance.getInitialStatus().toString()); eurekaRepository.save(eurekaEntity); log.info("Started Eureka Server Record Success "); } } } catch (Exception e) { log.warn("Started Eureka Server Record failure : {}", e.getMessage()); } } /** * 服務註冊,記錄註冊的服務例項資訊 * * @param instanceInfo 要註冊的服務例項資訊 */ @Async public void recordInstanceRegister(InstanceInfo instanceInfo) { try { log.info("Instance Register , name : {}", instanceInfo.getAppName()); log.info("Instance Register , id : {}", instanceInfo.getId()); log.info("Instance Register , ipAddress : {}", instanceInfo.getIPAddr()); log.info("Instance Register , status : {} ", instanceInfo.getStatus()); //Eureka伺服器在接收到例項的最後一次發出的心跳後,需要等待多久才可以將此例項刪除,預設為90秒 log.info("Instance Register , durationInSecs : {}s", instanceInfo.getLeaseInfo().getDurationInSecs()); EurekaClientEntity eurekaClientEntity = eurekaClientRepository.findByInstanceId(instanceInfo.getInstanceId()); if (eurekaClientEntity == null) { eurekaClientEntity = new EurekaClientEntity(); eurekaClientEntity.setApplicationName(instanceInfo.getAppName()); eurekaClientEntity.setInstanceId(instanceInfo.getInstanceId()); eurekaClientEntity.setHostname(instanceInfo.getHostName()); eurekaClientEntity.setUuid(UUID.randomUUID().toString().replace("-", "")); } eurekaClientEntity.setRegisterTime(new Date()); eurekaClientEntity.setHomePageUrl(instanceInfo.getHomePageUrl()); eurekaClientEntity.setHealthCheckUrl(instanceInfo.getHealthCheckUrl()); eurekaClientEntity.setStatusPageUrl(instanceInfo.getStatusPageUrl()); eurekaClientEntity.setEurekaInstanceId(eurekaInstance.getInstanceId()); eurekaClientEntity.setStatus(instanceInfo.getStatus().toString()); eurekaClientRepository.save(eurekaClientEntity); log.info("Instance Register {} Record Success ", instanceInfo.getInstanceId()); } catch (Exception e) { log.info("Instance Register {} Record failure : {}", instanceInfo.getInstanceId(), e.getMessage()); } System.out.println(); } /** * 服務下線,記錄下線的服務例項資訊 * * @param instanceId 要註冊的服務例項資訊的instanceId */ @Async public void recordInstanceCancel(String instanceId) { try { log.info("Instance Cancel "); EurekaClientEntity eurekaClientEntity = eurekaClientRepository.findByInstanceId(instanceId); eurekaClientEntity.setStatus(InstanceInfo.InstanceStatus.DOWN.toString()); eurekaClientRepository.save(eurekaClientEntity); log.info("Instance Cancel {} Record Success ", instanceId); } catch (Exception e) { log.info("Instance Cancel {} Record failure : {}", instanceId, e.getMessage()); } } }
2 . Eureka事件監聽配置,可在這裡自定義相關邏輯(例如服務下線 發郵件提醒) :
package name.ealen.listener; import name.ealen.function.EurekaEventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean; import org.springframework.cloud.netflix.eureka.server.event.*; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import javax.annotation.Resource; @Component public class EurekaServerEventListener { private static final Logger log = LoggerFactory.getLogger(EurekaServerEventListener.class); @Resource private EurekaEventHandler eurekaEventHandler; /** * Eureka Server 註冊事件 */ @EventListener public void eurekaRegister(EurekaRegistryAvailableEvent event) { log.info("Eureka Server Register at timestamp : {}", event.getTimestamp()); //write your logic.......... } /** * Eureka Server 啟動事件 */ @EventListener public void serverStart(EurekaServerStartedEvent event) { Object source = event.getSource(); if (source instanceof EurekaServerConfigBean) { EurekaServerConfigBean eureka = (EurekaServerConfigBean) source; eurekaEventHandler.recordEurekaStartUp(eureka); } //write your logic.......... } /** * 服務註冊事件 */ @EventListener(condition = "#event.replication==false") public void instanceRegister(EurekaInstanceRegisteredEvent event) { eurekaEventHandler.recordInstanceRegister(event.getInstanceInfo()); //write your logic.......... } /** * 服務下線事件 */ @EventListener(condition = "#event.replication==false") public void instanceCancel(EurekaInstanceCanceledEvent event) { eurekaEventHandler.recordInstanceCancel(event.getServerId()); //write your logic.......... } /** * 服務續約事件 */ @EventListener(condition = "#event.replication==false") public void instanceRenewed(EurekaInstanceRenewedEvent event) { //..... } }
6 . 執行效果
7 . 如果有服務註冊上去,檢視資料庫可以看到EurekaServe和EurekaClient的基本資訊記錄。
以上,就是個人搭建的實用EurekaServer的完整例項。
感謝各位提出意見和支援。