SpringCloud服务治理-搭建一个实用的Eureka Server
Eureka的主要作用是实现各个微服务实例的自动化注册与发现。
服务治理框架中,通常都会构建一个注册中心, Spring Cloud Eureka 就扮演着这样的角色。
Eureka Server即服务注册中心,Eureka Client(微服务服务提供方) 主要处理服务的注册与发现 ,Eureka 客户端向注册中心注册自身提供的服务并周期性的发送心跳来更新它的服务租约。
下面搭建了一个实用的服务注册中心,并将其相关配置和行为记录进行来简要说明。
简单的技术栈 : SpringBoot+SpringCloud+JPA ,完整Demo的项目地址 : https://github.com/EalenXie/springcloud-eureka-server
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的完整实例。
感谢各位提出意见和支持。