1 搭建Eureka Server高可用集群
1.1 概述
- 在上面的章节,实现了单节点的Eureka Server的服务注册和服务发现功能。Eureka Client会定时连接到Eureka Server,获取注册表中的信息并缓存到本地。微服务在消费远程API的时候总是使用本地缓存中的数据。因此一般来说,即使Eureka Server出现宕机,也不会影响到服务之间的调用。但是如果Eureka Server宕机的时候,某些微服务也出现了不可用的情况,Eureka SClient中的缓存如果不被刷新,就可能影响到微服务的调用,甚至影响到整个应用系统的高可用。因此,在生产环境中,通常会部署一个高可用的Eureka Server集群。
- Eureka Server可用通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量的同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册时Eureka Server的默认行为。

1.2 搭建Eureka Server高可用集群
1.2.1 修改本机的host文件
- 由于个人计算机中进行测试很难模拟多主机的情况,Eureka配置server集群的时候需要修改host文件。在win10中host文件的路径是C:\Windows\System32\drivers\etc\hosts。
127.0.0.1 eureka7001.com127.0.0.1 eureka7002.com127.0.0.1 eureka7003.com
1.2.2 修改原先的Eureka Server
server: # 单机版的端口# port: 9000 port: 7001 #端口#配置Eureka Servereureka: instance: # 主机地址名称# hostname: localhost # 单机版的主机地址名称 hostname: eureka7001.com client: register-with-eureka: false # 是否将自己注册到注册中心 fetch-registry: false # 是否从Eureka中获取服务列表 service-url: # 配置暴露给Eureka Client的请求地址 # 单机版# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
- 将原先的Eureka的模块名修改为eureka_service7001,其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"> <parent> <artifactId>spring_cloud_demo</artifactId> <groupId>org.sunxiaping</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <!-- 此处修改 --> <artifactId>eureka_service7001</artifactId> <dependencies> <!-- 导入Eureka Server对应的坐标 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies></project>
package com.sunxiaping.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/** * @author 许威威 * @version 1.0 */@SpringBootApplication@EnableEurekaServer //开启Eureka Server//将启动类名称由EurekaApplication修改为Eureka7001Applicationpublic class Eureka7001Application { public static void main(String[] args) { SpringApplication.run(Eureka7001Application.class, args); }}
<?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> <packaging>pom</packaging> <modules> <module>product_service</module> <module>spring_cloud_common</module> <module>order_service</module> <!-- 此处修改 --> <module>eureka_service7001</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <groupId>org.sunxiaping</groupId> <artifactId>spring_cloud_demo</artifactId> <version>1.0</version> <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.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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-releases</id> <name>Spring Releases</name> <url>http://repo.spring.io/libs-release-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> <build> <finalName>spring_cloud_demo</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <delimiters> <delimit>$</delimit> </delimiters> </configuration> </plugin> </plugins> </build></project>
1.2.3 搭建eureka_service7002模块
- pom.xml(复制eureka_service7001模块的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"> <parent> <artifactId>spring_cloud_demo</artifactId> <groupId>org.sunxiaping</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eureka_service7002</artifactId> <dependencies> <!-- 导入Eureka Server对应的坐标 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies></project>
server: # 单机版的端口# port: 9000 port: 7002 #端口#配置Eureka Servereureka: instance: # 主机地址名称# hostname: localhost # 单机版的主机地址名称 hostname: eureka7002.com client: register-with-eureka: false # 是否将自己注册到注册中心 fetch-registry: false # 是否从Eureka中获取服务列表 service-url: # 配置暴露给Eureka Client的请求地址 # 单机版# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/
<?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> <packaging>pom</packaging> <modules> <module>product_service</module> <module>spring_cloud_common</module> <module>order_service</module> <!-- 此处修改 --> <module>eureka_service7001</module> <module>eureka_service7002</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <groupId>org.sunxiaping</groupId> <artifactId>spring_cloud_demo</artifactId> <version>1.0</version> <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.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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-releases</id> <name>Spring Releases</name> <url>http://repo.spring.io/libs-release-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> <build> <finalName>spring_cloud_demo</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <delimiters> <delimit>$</delimit> </delimiters> </configuration> </plugin> </plugins> </build></project>
package com.sunxiaping.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/** * @author 许威威 * @version 1.0 */@SpringBootApplication@EnableEurekaServer //开启Eureka Serverpublic class Eureka7002Application { public static void main(String[] args) { SpringApplication.run(Eureka7002Application.class, args); }}
1.2.4 搭建eureka_service7003模块
- pom.xml(复制eureka_service7001模块的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"> <parent> <artifactId>spring_cloud_demo</artifactId> <groupId>org.sunxiaping</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eureka_service7003</artifactId> <dependencies> <!-- 导入Eureka Server对应的坐标 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies></project>
server: # 单机版的端口# port: 9000 port: 7003 #端口#配置Eureka Servereureka: instance: # 主机地址名称# hostname: localhost # 单机版的主机地址名称 hostname: eureka7003.com client: register-with-eureka: false # 是否将自己注册到注册中心 fetch-registry: false # 是否从Eureka中获取服务列表 service-url: # 配置暴露给Eureka Client的请求地址 # 单机版# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
<?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> <packaging>pom</packaging> <modules> <module>product_service</module> <module>spring_cloud_common</module> <module>order_service</module> <!-- 此处修改 --> <module>eureka_service7001</module> <module>eureka_service7002</module> <module>eureka_service7003</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <groupId>org.sunxiaping</groupId> <artifactId>spring_cloud_demo</artifactId> <version>1.0</version> <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.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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-releases</id> <name>Spring Releases</name> <url>http://repo.spring.io/libs-release-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>http://repo.spring.io/libs-snapshot-local</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/libs-milestone-local</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> <build> <finalName>spring_cloud_demo</finalName> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <delimiters> <delimit>$</delimit> </delimiters> </configuration> </plugin> </plugins> </build></project>
package com.sunxiaping.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/** * @author 许威威 * @version 1.0 */@SpringBootApplication@EnableEurekaServer //开启Eureka Serverpublic class Eureka7003Application { public static void main(String[] args) { SpringApplication.run(Eureka7003Application.class, args); }}
1.2.5 修改商品微服务的yml,将注册地址改为Eureka Server集群地址
server: port: 9001 # 微服务的端口号spring: application: name: service-product # 微服务的名称 datasource: url: jdbc:mysql://192.168.237.100:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 jpa: generate-ddl: true show-sql: true open-in-view: true database: mysql# 配置 eurekaeureka: instance: # 主机名称:服务名称修改,其实就是向eureka server中注册的实例id instance-id: service-product:9001 # 显示IP信息 prefer-ip-address: true client: service-url: # 此处修改为 Eureka Server的集群地址 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/# 微服务info内容详细信息info: app.name: xxx company.name: xxx build.artifactId: $project.artifactId$ build.version: $project.version$
2 Eureka中的常见问题
2.1 服务注册慢
- 默认情况下,服务注册到Eureka Server的过程较慢。
- 服务的注册设计到心跳,默认心跳的间隔为30s。在实例、服务器和客户端都在本地缓存中具有相同的元数据之前,服务不可用于客户端发现。可用通过配置
eureka.instance.lease-renewal-interval-in-seconds(心跳续约间隔)来加快客户端连接到其他服务的过程。在生产中,最好使用默认值,因为在服务器内部有一些计算,它们对续约做出假设。
2.2 服务节点剔除问题
- 默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护机制。所以不能有效而迅速的剔除失效节点,这对于开发或测试会造成困扰。解决方案如下:
- 对于Eureka Client:配置开启健康检查,并设置续约时间。
# 配置 eurekaeureka: instance: # 主机名称:服务名称修改,其实就是向eureka server中注册的实例id instance-id: service-product:9001 # 显示IP信息 prefer-ip-address: true lease-renewal-interval-in-seconds: 5 # 发送心跳续约间隔(默认30秒) lease-expiration-duration-in-seconds: 10 # Eureka Client发送心跳给Eureka Server端后,续约到期时间(默认90秒) client: healthcheck: enable: true #开启健康检查(依赖spring-boot-actuator) service-url: # 此处修改为 Eureka Server的集群地址 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
- 对于Eureka Server:配置关闭自我保护,设置剔除无效节点的时间间隔。
#配置Eureka Servereureka: instance: # 主机地址名称# hostname: localhost # 单机版的主机地址名称 hostname: eureka7001.com client: register-with-eureka: false # 是否将自己注册到注册中心 fetch-registry: false # 是否从Eureka中获取服务列表 service-url: # 配置暴露给Eureka Client的请求地址 # 单机版# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ server: enable-self-preservation: false # 关闭自我保护 eviction-interval-timer-in-ms: 4000 #剔除时间间隔,单位:毫秒
3 Eureka的源码解析
3.1 Eureka服务注册核心源码解析
3.1.1 @EnableEurekaServer注解的作用
- 通过@EnableEurekaServer注解激活EurekaServer。
@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Import({EurekaServerMarkerConfiguration.class})public @interface EnableEurekaServer {}
- @EnableEurekaServer注解向Spring容器中导入了一个EurekaServerMarkerConfiguration组件,EurekaServerMarkerConfiguration组件的源码如下:
@Configurationpublic class EurekaServerMarkerConfiguration { //向容器中导入了Marker组件,Marker组件是实例化核心配置类的前提条件 @Bean public Marker eurekaServerMarkerBean() { return new Marker(); } class Marker { }}
3.1.2 自动装载核心配置类
- SpringCloud对EurekaServer的封装使得发布EurekaServer变得无比简单,根据自动状态的原则可以在spring-cloud-netflix-eureka-server-2.1.0.RELEASE.jar下找到spring.factories文件。

- 根据SpringBoot自动配置原理,即SpringBoot启动的时候会加载所有jar包下的META-INF下的spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的自动配置类。所以,SpringBoot启动的时候会加载EurekaServerAutoConfiguration(EurekaServer的自动配置类),其源码如下:
@Configuration@Import(EurekaServerInitializerConfiguration.class)@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)@EnableConfigurationProperties({ EurekaDashboardProperties.class,InstanceRegistryProperties.class })@PropertySource("classpath:/eureka/server.properties")public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter { //其他略}
- 接下来解释EurekaServerAutoConfiguration(Eureka Server自动配置类)的作用:
EurekaServerAutoConfiguration(Eureka Server自动配置类)生效的前提是容器中存在EurekaServerMarkerConfiguration.Marker组件(可以从@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)这段代码看出),而@EnableEurekaServer注解的作用就是向容器中添加EurekaServerMarkerConfiguration.Marker组件,所以如果不在启动类中配置@EnableEurekaServer的话,那么EurekaServerAutoConfiguration(Eureka Server自动配置类)就不会起作用。
从@EnableConfigurationProperties({ EurekaDashboardProperties.class,InstanceRegistryProperties.class })代码中,我们知道EurekaServerAutoConfiguration(Eureka Server)向容器中导入了EurekaDashboardProperties和InstanceRegistryProperties两个组件。
- EurekaDashboardProperties:配置EurekaServer管理台。
- InstanceRegistryProperties:配置期望续约数量和默认的通信数量。
通过@Import(EurekaServerInitializerConfiguration.class)引入启动类。
3.1.3 EurekaServerInitializerConfiguration
- EurekaServerInitializerConfiguration.java
@Configurationpublic class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered { private static final Log log = LogFactory.getLog(EurekaServerInitializerConfiguration.class); @Autowired private EurekaServerConfig eurekaServerConfig; private ServletContext servletContext; @Autowired private ApplicationContext applicationContext; @Autowired private EurekaServerBootstrap eurekaServerBootstrap; private boolean running; private int order = 1; @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } @Override public void start() { new Thread(new Runnable() { @Override public void run() { try { //TODO: is this class even needed now? eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext); log.info("Started Eureka Server"); publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())); EurekaServerInitializerConfiguration.this.running = true; publish(new EurekaServerStartedEvent(getEurekaServerConfig())); } catch (Exception ex) { // Help! log.error("Could not initialize Eureka servlet context", ex); } } }).start(); } private EurekaServerConfig getEurekaServerConfig() { return this.eurekaServerConfig; } private void publish(ApplicationEvent event) { this.applicationContext.publishEvent(event); } @Override public void stop() { this.running = false; eurekaServerBootstrap.contextDestroyed(this.servletContext); } @Override public boolean isRunning() { return this.running; } @Override public int getPhase() { return 0; } @Override public boolean isAutoStartup() { return true; } @Override public void stop(Runnable callback) { callback.run(); } @Override public int getOrder() { return this.order; }}
- 从源码中,可以看出EurekaServerInitializerConfiguration实现了SmartLifecycle,意味着Spring容器启动时会指定start()方法。加载所有的EurekaServer的配置。
3.1.4 EurekaServerAutoConfiguration
- EurekaServerAutoConfiguration的部分源码如下:
@Configuration@Import(EurekaServerInitializerConfiguration.class)@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)@EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class })@PropertySource("classpath:/eureka/server.properties")public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter { //实例化了Eureka Server的管控台Controller类EurekaController @Bean @ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true) public EurekaController eurekaController() { return new EurekaController(this.applicationInfoManager); } //实例化EurekaServerBootstrap类 @Bean public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) { return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext); } //实例化jersey相关配置类 @Bean public FilterRegistrationBean jerseyFilterRegistration( javax.ws.rs.core.Application eurekaJerseyApp) { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new ServletContainer(eurekaJerseyApp)); bean.setOrder(Ordered.LOWEST_PRECEDENCE); bean.setUrlPatterns( Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*")); return bean; } //jerseyApplication方法,在容器中存放了一个jerseyApplication对象, //jerseyApplication方法和Spring源码扫描@Component逻辑类似,扫描@Path和@Provider标签,然后封装成BeanDefinition, //封装到APPlication里面的Set容器。然后Filter过滤器来过滤URL进行映射到对象的Controller @Bean public javax.ws.rs.core.Application jerseyApplication(Environment environment, ResourceLoader resourceLoader) { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider( false, environment); // Filter to include only classes that have a particular annotation. // provider.addIncludeFilter(new AnnotationTypeFilter(Path.class)); provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); // Find classes in Eureka packages (or subpackages) // Set<Class<?>> classes = new HashSet<>(); for (String basePackage : EUREKA_PACKAGES) { Set<BeanDefinition> beans = provider.findCandidateComponents(basePackage); for (BeanDefinition bd : beans) { Class<?> cls = ClassUtils.resolveClassName(bd.getBeanClassName(), resourceLoader.getClassLoader()); classes.add(cls); } }}
3.1.5 暴露的服务端接口
- 由于集成了Jersey,我们可以找到Eureka Server的依赖包中的
eureka-core-1.9.8.jar,可以看到一些列的XxxResource。

- 上面的这些类都是通过Jersey发布的供客户端调用的服务接口。
3.1.6 服务端接收客户端的注册
- 在ApplicationResource的addInstance()方法中可以看到
this.registry.register(info, "true".equals(isReplication));。
@Produces({"application/xml", "application/json"})public class ApplicationResource { @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.getIPAddr())) { return Response.status(400).entity("Missing ip address").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()); } } } this.registry.register(info, "true".equals(isReplication)); return Response.status(204).build(); } } }
@Singletonpublic class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry { @Override public void register(final InstanceInfo info, final boolean isReplication) { //默认有效时长为90秒 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 Server服务 replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); } //其他略}
- 继续找到父类的register方法可以看到整个注册过程:
public abstract class AbstractInstanceRegistry implements InstanceRegistry { //线程安全的Map,存放所有注册的实例对象 private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>(); public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { try { read.lock(); Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName()); REGISTER.increment(isReplication); //如果第一个实例注册会给registry put进去一个空的 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; } } // 根据注册的实例对象id,获取已经存在的Lease 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); // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted // InstanceInfo instead of the server local copy. 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.expectedNumberOfClientsSendingRenews > 0) { // Since the client wants to register it, increase the number of clients sending renews this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1; updateRenewsPerMinThreshold(); } } 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()); } //将lease存入gMap 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(); } } //其他略 }
3.1.7 服务端接收客户端的续约
- 在InstanceResource的renewLease方法中完成客户端的心跳(续约)处理,其中最关键的方法就是
registry.renew(app.getName(), id, isFromReplicaNode);。
@Produces({"application/xml", "application/json"})public class InstanceResource { @PUT public Response renewLease( @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication, @QueryParam("overriddenstatus") String overriddenStatus, @QueryParam("status") String status, @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) { boolean isFromReplicaNode = "true".equals(isReplication); //服务端接收客户端的续约 boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode); // Not found in the registry, immediately ask for a register if (!isSuccess) { logger.warn("Not Found (Renew): {} - {}", app.getName(), id); return Response.status(Status.NOT_FOUND).build(); } // Check if we need to sync based on dirty time stamp, the client // instance might have changed some value Response response; if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) { response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode); // Store the overridden status since the validation found out the node that replicates wins if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode() && (overriddenStatus != null) && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus)) && isFromReplicaNode) { registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus)); } } else { response = Response.ok().build(); } logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus()); return response; }}
@Singletonpublic class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry { public boolean renew(final String appName, final String id, final boolean isReplication) { //客户端续约 if (super.renew(appName, id, isReplication)) { //同步到其他的Eureka Server服务 replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication); return true; } return false; } //其他略 }
- 继续找到父类的renew方法可以看到整个续约过程:
public abstract class AbstractInstanceRegistry implements InstanceRegistry { public boolean renew(String appName, String id, boolean isReplication) { RENEW.increment(isReplication); Map<String, Lease<InstanceInfo>> gMap = registry.get(appName); Lease<InstanceInfo> leaseToRenew = null; if (gMap != null) { //从Map中根据id获取实例对象的Lease对象 leaseToRenew = gMap.get(id); } if (leaseToRenew == null) { RENEW_NOT_FOUND.increment(isReplication); logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id); return false; } else { //获取实例对象 InstanceInfo instanceInfo = leaseToRenew.getHolder(); if (instanceInfo != null) { // touchASGCache(instanceInfo.getASGName()); InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus( instanceInfo, leaseToRenew, isReplication); if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) { logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}" + "; re-register required", instanceInfo.getId()); RENEW_NOT_FOUND.increment(isReplication); return false; } if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) { logger.info( "The instance status {} is different from overridden instance status {} for instance {}. " + "Hence setting the status to overridden status", instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()); //设置实例状态 instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus); } } //设置续约次数 renewsLastMin.increment(); leaseToRenew.renew(); return true; } }}
3.1.8 服务剔除
- 在AbstractInstanceRegistry的postInit()方法中开启了一个每60秒调用一次EvictionTask的evict的定时器。
public abstract class AbstractInstanceRegistry implements InstanceRegistry { public void evict(long additionalLeaseMs) { logger.debug("Running the evict task"); if (!isLeaseExpirationEnabled()) { logger.debug("DS: lease expiration is currently disabled."); return; } // We collect first all expired items, to evict them in random order. For large eviction sets, // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it, // the impact should be evenly distributed across all applications. List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>(); for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) { Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue(); if (leaseMap != null) { for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) { Lease<InstanceInfo> lease = leaseEntry.getValue(); if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { expiredLeases.add(lease); } } } } // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for // triggering self-preservation. Without that we would wipe out full registry. int registrySize = (int) getLocalRegistrySize(); int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold()); int evictionLimit = registrySize - registrySizeThreshold; int toEvict = Math.min(expiredLeases.size(), evictionLimit); if (toEvict > 0) { logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit); Random random = new Random(System.currentTimeMillis()); for (int i = 0; i < toEvict; i++) { // Pick a random item (Knuth shuffle algorithm) int next = i + random.nextInt(expiredLeases.size() - i); Collections.swap(expiredLeases, i, next); Lease<InstanceInfo> lease = expiredLeases.get(i); String appName = lease.getHolder().getAppName(); String id = lease.getHolder().getId(); EXPIRED.increment(); logger.warn("DS: Registry: expired lease for {}/{}", appName, id); internalCancel(appName, id, false); } } } //其他略}
3.2 Eureka服务发现核心源码解析
3.2.1 自动装载
- 在消费者导入的坐标有spring-cloud-netflix-eureka-client-2.1.0.RELEASE.jar找到的spring.factories可以看到所有自动装载的配置类:

3.2.2 服务注册
- EurekaClientAutoConfiguration中注册了DiscoveryClient组件。
@Configuration@EnableConfigurationProperties@ConditionalOnClass(EurekaClientConfig.class)@Import(DiscoveryClientOptionalArgsConfiguration.class)@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration", "org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration", "org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})public class EurekaClientAutoConfiguration { @Bean public DiscoveryClient discoveryClient(EurekaClient client, EurekaClientConfig clientConfig) { return new EurekaDiscoveryClient(client, clientConfig); } //其他leukemia }
- DiscoveryClient中有register方法,即服务注册方法:
@Singletonpublic class DiscoveryClient implements EurekaClient { //服务注册 boolean register() throws Throwable { logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier); EurekaHttpResponse httpResponse; try { httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo); } catch (Exception var3) { logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3}); throw var3; } if (logger.isInfoEnabled()) { logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode(); } //其他略}
3.2.3 服务下架
- DiscoveryClient中有shutdown方法,即服务下架方法:
@Singletonpublic class DiscoveryClient implements EurekaClient { @PreDestroy public synchronized void shutdown() { if (this.isShutdown.compareAndSet(false, true)) { logger.info("Shutting down DiscoveryClient ..."); if (this.statusChangeListener != null && this.applicationInfoManager != null) { this.applicationInfoManager.unregisterStatusChangeListener(this.statusChangeListener.getId()); } this.cancelScheduledTasks(); if (this.applicationInfoManager != null && this.clientConfig.shouldRegisterWithEureka() && this.clientConfig.shouldUnregisterOnShutdown()) { this.applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN); this.unregister(); } if (this.eurekaTransport != null) { this.eurekaTransport.shutdown(); } this.heartbeatStalenessMonitor.shutdown(); this.registryStalenessMonitor.shutdown(); logger.info("Completed shut down of DiscoveryClient"); } } //其他略 }
3.2.4 心跳续约
- DiscoveryClient的HeartbeatThread中定义了续约的操作:
@Singletonpublic class DiscoveryClient implements EurekaClient { private class HeartbeatThread implements Runnable { private HeartbeatThread() { } public void run() { if (DiscoveryClient.this.renew()) { DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); } } } boolean renew() { try { EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null); logger.debug("DiscoveryClient_{} - Heartbeat status: {}", this.appPathIdentifier, httpResponse.getStatusCode()); if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) { this.REREGISTER_COUNTER.increment(); logger.info("DiscoveryClient_{} - Re-registering apps/{}", this.appPathIdentifier, this.instanceInfo.getAppName()); long timestamp = this.instanceInfo.setIsDirtyWithTime(); boolean success = this.register(); if (success) { this.instanceInfo.unsetIsDirty(timestamp); } return success; } else { return httpResponse.getStatusCode() == Status.OK.getStatusCode(); } } catch (Throwable var5) { logger.error("DiscoveryClient_{} - was unable to send heartbeat!", this.appPathIdentifier, var5); return false; } } //其他略 }