在学习zookeeper的时候,重点就讲了服务注册与发现的流程,dubbo就是基于zookeeper来实现服务注册与发现的。
基于zookeeper的服务注册与发现大致流程如下
而在SpringCloud中,大量使用了Netflix的开源项目,其中Eureka就属于Netflix 提供的发现服务组件,所有的微服务都注册到Eureka中,它在其中扮演的就是注册中心的角色,后面所有的客户端直接从注册中心获取所需要的服务
Eureka 服务端
新建一个microcloud-eureka模块,这模块做的事情非常简单,既启动Eureka的服务端,pom文件如下
<?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>springcloud</artifactId>
<groupId>enjoy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>microcloud-eureka</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--启动Eureka的服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
注意:如果是Edgware或之前的版本,用的是springboot 1.5.或者更低的版本
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency>
【microcloud-eureka】修改application.yml文件,在里面配置eureka相关信息
server:
port: 7001
eureka:
instance: # eureak实例定义
hostname: localhost # 定义 Eureka 实例所在的主机名称
【microcloud-eureka】新增Eureka启动类,增加Eureka服务端注解
package cn.enjoy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaApp {
public static void main(String[] args) {
SpringApplication.run(EurekaApp.class,args);
}
}
运行main方法
上面会有一些出错信息,先不用管,后面再来处理!
在浏览器上执行http://localhost:7001/
服务提供方注册到Eureka
现在Eureka虽然有点小瑕疵,但现在已经能正常访问了,那么接下来就需要将用的微服务注册到Eureka服务当中,为后面客户端的使用做铺垫。
【microcloud-provider-product】修改pom文件,增加eureka客户端相关信息。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
注意:如果是Edgware或之前的版本,用的是springboot 1.5.或者更低的版本
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
【microcloud-provider-product】修改application.yml配置文件,在者个文件中定义要注册的eureka服务的地址
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
【microcloud-provider-product】修改启动类,在这个类上增加eureka客户端的注解信息
package cn.enjoy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@MapperScan("cn.enjoy.mapper")
@EnableEurekaClient
public class ProductApp{
public static void main(String[] args) {
SpringApplication.run(ProductApp.class,args);
}
}
加上这注解后,启动
发现Application的名字是UNKNOWN,为此应该为这单独取一个名字
【microcloud-provider-product】修改application.yml配置文件,为这个微服务起一个名字
spring:
application:
name: microcloud-provider-product
重新启动后
现在虽然成功的实现了微服务注册,但是现在看下STATUS,这个时候名称还有点乱,我们现在开源自定义一个路径名称
【microcloud-provider-product】修改application.yml配置文件,追加主机名称的显示:
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: microcloud-provider-product
另外一般情况下,当鼠标点击查看的时候应该以IP作为链接项
【microcloud-provider-product】修改application.yml配置文件
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: microcloud-provider-product
prefer-ip-address: true
修改后地址栏变为
点击状态栏
【microcloud-provider-product】如果想看状态信息需要增加actuator模块,这一块的内容已经在讲springboot的时候讲过,修改pom文件,增加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
【microcloud-provider-product】修改application.yml文件,追加info相关配置
info:
app.name: microcloud-provider-product
company.name: enjoy
build.artifactId: $project.artifactId$
build.modelVersion: $project.modelVersion$
注意:由于在yml文件中使用了$,这个时候启动是会报错的,因此还需要一个maven-resources-plugin插件的支持
【microcloud】在父工程增加插件,修改pom文件
<build>
<finalName>microcloud</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>
<delimiter>$</delimiter>
</delimiters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${jdk.version}</source><!-- 源代码使用的开发版本 -->
<target>${jdk.version}</target><!-- 需要生成的目标class文件的编译版本 -->
</configuration>
</plugin>
</plugins>
</build>
启动后:
其他配置
在前面启动eureka中,会发现启动会报错,虽然这些错误不影响使用
另外在关闭【microcloud-provider-product】项目后,刷新eureka发现项目还在,隔一段时间后会发现
这其实就是触发了安全模式
【microcloud-eureka】设置服务的清理间隔时间,修改application.yml文件
server:
port: 7001
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
instance: # eureak实例定义
hostname: localhost # 定义 Eureka 实例所在的主机名称
这个重新测试,服务提供方注册后,关闭服务发现服务实例依然还在。
【microcloud-provider-product】修改application.yml配置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: microcloud-provider-product
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
```
由于所有的服务都注册到了 Eureka 之中
这样如果配置了“lease-expiration-duration-in-seconds”此选项,
表示距离上一次发送心跳之后等待下一次发送心跳的间隔时间,如果超过了此间隔时间,则认为该微服务已经宕机了。
【microcloud-provider-product】对于注册到 Eureka 上的服务,可以通过发现服务来获取一些服务信息,修改ProductController,增加一个方法
```java
package cn.enjoy.controller;
import cn.enjoy.service.IProductService;
import cn.enjoy.vo.Product;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/prodcut")
public class ProductController {
@Resource
private IProductService iProductService;
@Resource
private DiscoveryClient client ; // 进行Eureka的发现服务
@RequestMapping(value="/get/{id}")
public Object get(@PathVariable("id") long id) {
return this.iProductService.get(id) ;
}
@RequestMapping(value="/add")
public Object add(@RequestBody Product product) {
return this.iProductService.add(product) ;
}
@RequestMapping(value="/list")
public Object list() {
return this.iProductService.list() ;
}
@RequestMapping("/discover")
public Object discover() { // 直接返回发现服务信息
return this.client ;
}
}
【microcloud-provider-product】修改ProductApp, 在主程序中启用发现服务项
package cn.enjoy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@MapperScan("cn.enjoy.mapper")
@EnableEurekaClient
@EnableDiscoveryClient
public class ProductApp{
public static void main(String[] args) {
SpringApplication.run(ProductApp.class,args);
}
}
访问:localhost:8080/prodcut/discover
可以发现DiscoveryClient包含的很多信息
可以看到在eureka里面显示的信息都可以在这里获取得到。
Eureka 安全机制
一般情况下Eureka 和服务的提供注册者都会在一个内网环境中,但免不了在某些项目中需要让其他外网的服务注册到Eureka,这个时候就有必要让Eureka增加一套安全认证机制了,让所有服务提供者通过安全认证后才能注册进来
【microcloud-eureka】修改pom文件,引入SpringSecurity的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
【microcloud-eureka】 修改application.yml文件,增加用户、密码验证
server:
port: 7001
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://admin:enjoy@localhost:7001/eureka
instance: # eureak实例定义
hostname: localhost # 定义 Eureka 实例所在的主机名称
spring:
security:
user:
name: admin
password: enjoy
```
【microcloud-provider-product】修改application.yml文件,增加验证信息
```yaml
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://admin:enjoy@localhost:7001/eureka
instance:
instance-id: microcloud-provider-product
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
重新启动
注意:如果是Edgware或之前的版本,做到这一步就行了,但使用现在版本,你会发现启动【microcloud-provider-product】后服务注册不上去
【microcloud-eureka】新增配置类EurekaSecurityConfig,重写configure方法,把csrf劫持关闭
package cn.enjoy;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class EurekaSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
super.configure(http);
}
}
```
## HA 高可用
学习Zookepper服务注册的时候,注册中心是能实现高可用的,但现在的Eureka还是单节点的情况,如果Eureka出现了错误,将会导致整个集群无法继续使用,这个时候就需要考虑Eureka的高可用了。
![](/images/posts/2019-05-28-87/bd234d63.png)
现在需要3个eureka ,每个eureka都需要配置hostname,所有先修改hosts文件内容如下
```bash
127.0.0.1 eureka1
127.0.0.1 eureka2
127.0.0.1 eureka3
【microcloud-eureka】为了方便操作,讲microcloud-eureka项目复制两份,分别复制为【microcloud-eureka2】、 【microcloud-eureka3】
【microcloud-eureka】修改application.yml配置文件,修改端口以及注册位置
server:
port: 7001
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
#defaultZone: http://admin:enjoy@localhost:7001/eureka
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka1 # 定义 Eureka 实例所在的主机名称
spring:
security:
user:
name: admin
password: enjoy
【microcloud-eureka2】修改application.yml配置文件
server:
port: 7002
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
#defaultZone: http://admin:enjoy@localhost:7001/eureka
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka2 # 定义 Eureka 实例所在的主机名称
spring:
security:
user:
name: admin
password: enjoy
【microcloud-eureka3】修改application.yml配置文件
server:
port: 7003
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
#defaultZone: http://admin:enjoy@localhost:7001/eureka
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka3 # 定义 Eureka 实例所在的主机名称
spring:
security:
user:
name: admin
password: enjoy
启动eureka,eureka2,eureka3,进入服务的后台查看副本
登陆http://localhost:7001/
【microcloud-provider-product】修改application.yml配置文件,配置多台enreka的注册
server:
port: 8080
mybatis:
mapper-locations: # 所有的mapper映射文件
- classpath:mapping/*.xml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 配置当前要使用的数据源的操作类型
driver-class-name: com.mysql.cj.jdbc.Driver # 配置MySQL的驱动程序类
url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=GMT%2B8 # 数据库连接地址
username: root # 数据库用户名
password: root1234% # 数据库连接密码
application:
name: microcloud-provider-product
# security:
# user:
# roles:
# - USER # 授权角色
# name: root
# password: enjoy
logging:
level:
cn.enjoy.mapper: debug
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
#defaultZone: http://admin:enjoy@localhost:7001/eureka
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance:
instance-id: microcloud-provider-product
prefer-ip-address: true
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
info:
app.name: microcloud-provider-product
company.name: enjoy
build.artifactId: $project.artifactId$
build.modelVersion: $project.modelVersion$
打包发布
在真实项目中,需要讲Eureka发布到具体服务器上进行执行,打包部署其实和springboot里面讲的大同小异
和properties文件稍微有点不同,对于properties文件,不同的环境会有不同的配置文件比如application-dev.properties,application-test.properties,application-pro.properties等
但如果是yml文件,所有的的配置都再同一个yml文件中
【microcloud-eureka】修改application.yml文件
spring:
profiles:
active:
- dev-7001
---
server:
port: 7001
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka1 # 定义 Eureka 实例所在的主机名称
spring:
profiles: dev-7001
security:
user:
name: admin
password: enjoy
application:
name: microcloud-eureka
---
server:
port: 7002
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka2 # 定义 Eureka 实例所在的主机名称
spring:
profiles: dev-7002
security:
user:
name: admin
password: enjoy
application:
name: microcloud-eureka2
---
server:
port: 7003
eureka:
server:
eviction-interval-timer-in-ms: 1000 #设置清理的间隔时间,而后这个时间使用的是毫秒单位(默认是60秒)
enable-self-preservation: false #设置为false表示关闭保护模式
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://admin:enjoy@eureka1:7001/eureka,http://admin:enjoy@eureka2:7002/eureka,http://admin:enjoy@eureka3:7003/eureka
instance: # eureak实例定义
hostname: eureka3 # 定义 Eureka 实例所在的主机名称
spring:
profiles: dev-7003
security:
user:
name: admin
password: enjoy
application:
name: microcloud-eureka3
【microcloud-eureka】添加一个打包插件,修改pom文件
<?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>springcloud</artifactId>
<groupId>enjoy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>microcloud-eureka</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<finalName>eureka-server</finalName>
<plugins>
<plugin> <!-- 该插件的主要功能是进行项目的打包发布处理 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration> <!-- 设置程序执行的主类 -->
<mainClass>cn.enjoy.EurekaApp</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
【microcloud-eureka】 在pom文件所在目录
mvn clean install package
接下来就可以在项目的编译目录发现
eureka-server.jar 文件
采用默认的方式执行 eureka-server.jar
那么此时将运行在 7001 端口上:java -jar eureka-server.jar
运行其它的两个 profile 配置:
- 运行“dev-7002”profile:java -jar eureka-server.jar –spring.profiles.active=dev-7002;
- 运行“dev-7003”profile:java -jar eureka-server.jar –spring.profiles.active=dev-7003
原理解析
EurekaClient
EurekaClient为了简化开发人员的工作量,将很多与EurekaServer交互的工作隐藏起来,自主完成。具体完成的工作如下
应用启动阶段
- 读取与 Eureka Server交互的配置信息,封装成 EurekaClientConfig
- 读取自身服务实例配置信息,封装成EurekalnstanceConfig
- 从Eureka server拉取注册表信息并缓存到本地
- 服务注册
- 初始化发送心跳、缓存刷新(拉取注册表信息更新本地缓存)和按需注册(监控服务实例信息变化,决定是否重新发起注册,更新注册表中的服务实例元数据)定时任务
应用执行阶段
- 定时发送心跳到Eureka Server中维持在注册表的租约
- 定时从 Eureka Server中拉取注册表信息,更新本地注册表缓存
- 监控应用自身信息变化,若发生变化,需要重新发起服务注册
应用销毁阶段
从 Eureka Server注销自身服务实例
应用启动阶段与运行阶段
Eureka Client通过Starter的方式引人依赖, Spring Boot将会为项目使用以下的自动配置类
- EurekaClientAutoConfiguration:EurekeClient 自动配置类,负责Eureka关键Beans的配置和初始化,如AppplicationInfoManager和 EurekaClientConfig等。
- RibbonEurekaAutoConfiguration:Ribbon负载均衡相关配置。
- EurekaDiscoveryClientConfiguration:配置自动注册和应用的健康检查器。
读取应用自身配置
通过 EurekaClientAutoConfiguration配置类, Spring boot帮助 Eureka Client完成很多必要Bean的属性读取和配置
-
EurekaClientConfig
封装 Eureka Client与 Eureka Server交互所需要的配置信息。 Spring Cloud为其提供了一个默认配置类的EurekaClientConfigBean,可以在配置文件中通过前缀 eureka.client属性名进行属性覆盖 -
ApplicationInfoManager
作为应用信息管理器,管理服务实例的信息类 InstanceInfo和服务实例的配置信息类 EurekaInstanceConfig -
InstanceInfo
封装将被发送到 Eureka Server进行服务注册的服务实例元数据。它在Eurek Server的注册表中代表一个服务实例,其他服务实例可以通过 Instancelnfo了解该服
的相关信息从而发起服务请求 -
EurekaInstanceConfig
封装EurekaClient自身服务实例的配置信息,主要用于构建InstanceInfo通常这些信息在配置文件中的eureka.instance前缀下进行设置, SpringCloud通过EurekalnstanceConfigBean配置类提供了默认配置 -
DiscoveryClient
Spring Cloud中定义用来服务发现的客户端接口
对于DiscoveryClient可以具体查看 EurekaDiscoveryClient,EurekaDiscoveryClient又借助EurekaClient来实现
另外在netflix包里面还有一个DiscoveryClient,按名字翻译其实就是服务发现客户端,他是整个EurekaClient的核心,是与EurekaServer进行交互的核心所在
运行服务
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}
this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();
clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}
//传入BackupRegistry(NotImplementedRegistryImpl)备份注册中心
this.backupRegistryProvider = backupRegistryProvider;
this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
localRegionApps.set(new Applications());
fetchRegistryGeneration = new AtomicLong(0);
remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
//从eureka server拉起注册表信息 eureka.client.fetch-register
if (config.shouldFetchRegistry()) {
this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// 当前的客户端是否应该注册到erueka中 eureka.client.register-with-eureka
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
//如果既不需要注册,也不需要拉去数据,直接返回,初始结束
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, this.getApplications().size());
return; // no need to setup up an network tasks and we are done
}
try {
//线程池大小为2,一个用户发送心跳,另外个缓存刷新
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
//初始化client与server交互的jersey客户端
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
AzToRegionMapper azToRegionMapper;
if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
} else {
azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
}
if (null != remoteRegionsToFetch.get()) {
azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
}
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
//拉取注册表的信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
//将服务实例进行注册
// call and execute the pre registration handler before all background tasks (inc registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
//初始心跳定时任务,缓存刷新
initScheduledTasks();
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, this.getApplications().size());
}
总结
DiscoveryClient构造函数做的事情
- 相关配置赋值
- 备份注册中心的初始化,实际什么事都没做
- 拉取Server注册表中的信息
- 注册前的预处理
- 向Server注册自身
- 初始心跳定时任务,缓存刷新等定时任务
(重要步骤)拉取Server注册表中的信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false))
//是否全量拉去
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
// If the delta is disabled or if it is the first time, get all
// applications
//如果增量拉取被禁止全量拉去
Applications applications = getApplications();
if (clientConfig.shouldDisableDelta()
|| (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
|| forceFullRegistryFetch
|| (applications == null)
|| (applications.getRegisteredApplications().size() == 0)
|| (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
{
//全量拉取
getAndStoreFullRegistry();
} else {
//增量拉取
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
//打印注册表上所有服务实例信息
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
onCacheRefreshed();
// Update remote status based on refreshed data held in the cache
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
全量拉取
一般只有在第一次拉去注册表信息的时候,全量拉取调用 getAndStoreFullRegistry()方法
private void getAndStoreFullRegistry() throws Throwable {
//拉取注册表的版本信息
long currentUpdateGeneration = fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
Applications apps = null;
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
//拉取成功
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
apps = httpResponse.getEntity();
}
logger.info("The response status is {}", httpResponse.getStatusCode());
}
增量拉取
getAndUpdateDelta(applications)增量拉取
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
//拉取信息
Applications delta = null;
EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
delta = httpResponse.getEntity();
}
//如果拉取失败进行全量拉取
if (delta == null) {
logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
+ "Hence got the full registry.");
getAndStoreFullRegistry();
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
try {
//跟新本地缓存
updateDelta(delta);
reconcileHashCode = getReconcileHashCode(applications);
} finally {
fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
// There is a diff in number of instances for some reason
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
reconcileAndLogDifference(delta, reconcileHashCode); // this makes a remoteCall
}
} else {
logger.warn("Not updating application delta as another thread is updating it already");
logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
}
}
```
##### (重要步骤)服务注册
```java
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
register()方法负责服务的注册
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
//把自身的实例发送给服务端
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
return httpResponse.getStatusCode() == 204;
}
(重要步骤) 定时任务
initScheduledTasks()是负责定时任务的相关方法。
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// 拉取服务默认30秒,eureka.client.register-fetch-interval-seconds
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// 心跳服务,默认30秒
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
服务下线
com.netflix.discovery.DiscoveryClient#shutdown
@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()
&& clientConfig.shouldUnregisterOnShutdown()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
//关闭与server连接的客户端
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
//关闭相关监控
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
EurekaServer
总览
EurekaServer 是服务的注册中心,负责Eureka Client的相关信息注册,主要职责
- 服务注册
- 接受心跳服务
- 服务剔除
- 服务下线
- 集群同步
EurekaServerAutoConfiguration 是通过配置文件注册。
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs) {
this.eurekaClient.getApplications();
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient, this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(), this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
这里面有个InstanceRegistry就是重点需要关注的了
首先看下最上层的接口
public interface LeaseManager<T> {
//注册
void register(T var1, int var2, boolean var3);
//下线
boolean cancel(String var1, String var2, boolean var3);
//跟新
boolean renew(String var1, String var2, boolean var3);
//服务剔除
void evict();
}
PeerAwareInstanceRegistryImpl是一个子类的实现,在上面的基础上扩展对集群的同步操作,使Eureaka Server集群信息保持一致
服务注册
com.netflix.eureka.registry.AbstractInstanceRegistry#register 这方法是负责服务的注册的。
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//获取读锁
this.read.lock();
// gMap 其实可以发现,这里注册中心其实是个ConcurrentHashMap
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
EurekaMonitors.REGISTER.increment(isReplication);
if(gMap == null) {
ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
//key 为appName,如果存在,返回存在的值,否则添加,返回null
gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
if(gMap == null) {
gMap = gNewMap;
}
}
//根据instanceId获取实例的租约
Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
if(existingLease != null && existingLease.getHolder() != null) {
Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
//如果该实例的租约已经存在,比较最后的更新时间戳大小,取最大值的注册信息信息
if(existingLastDirtyTimestamp.longValue() > registrationLastDirtyTimestamp.longValue()) {
registrant = (InstanceInfo)existingLease.getHolder();
}
} else {
Object var6 = this.lock;
//如果租约不存在,注册一个新的实例
synchronized(this.lock) {
if(this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin += 2;
this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin * this.serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
//创建新的租约
Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
if(existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
//保存租约到map中
((Map)gMap).put(registrant.getId(), lease);
//获得最近注册队列
AbstractInstanceRegistry.CircularQueue var20 = this.recentRegisteredQueue;
synchronized(this.recentRegisteredQueue) {
this.recentRegisteredQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), registrant.getAppName() + "(" + registrant.getId() + ")"));
}
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(!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
if(overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
if(InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
//释放锁
this.read.unlock();
}
}
```
#### 接受心跳服务
在Eureka Client完成服务的注册后,需要定时向Eureka Server发送心跳请求(默认30s),维持自己在EurekaServer的租约有效性
```java
public boolean renew(String appName, String id, boolean isReplication) {
EurekaMonitors.RENEW.increment(isReplication);
//根据appName获取服务集群租约集合
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if(gMap != null) {
leaseToRenew = (Lease)gMap.get(id);
}
//如果租约不存在,直接返回false
if(leaseToRenew == null) {
EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
return false;
} else {
InstanceInfo instanceInfo = (InstanceInfo)leaseToRenew.getHolder();
if(instanceInfo != null) {
//得到服务的最终状态
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(instanceInfo, leaseToRenew, isReplication);
if(overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
//如果状态为UNKNOWN,取消续约
logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}; re-register required", instanceInfo.getId());
EurekaMonitors.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", new Object[]{instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()});
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
this.renewsLastMin.increment();
//跟新续约有效时间
leaseToRenew.renew();
return true;
}
}
服务剔除
如果Eureka Client在注册后,由于服务的崩溃或网络异常导致既没有续约,也没有下线,那么服务就处于不可知的状态,需要剔除这些服务
com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long) 这是个定时任务调用的方法
com.netflix.eureka.registry.AbstractInstanceRegistry#postInit中使用AbstractInstanceRegistry.EvictionTask 负责调用(默认60s)
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
//如果自我保护状态,不允许剔除服务
if(!this.isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
} else {
List<Lease<InstanceInfo>> expiredLeases = new ArrayList();
//遍历注册表registry,获取所有过期的租约
Iterator var4 = this.registry.entrySet().iterator();
while(true) {
Map leaseMap;
do {
if(!var4.hasNext()) {
//获取注册表租约总数
int registrySize = (int)this.getLocalRegistrySize();
int registrySizeThreshold = (int)((double)registrySize * this.serverConfig.getRenewalPercentThreshold());
//计算最多允许剔除的阈值
int evictionLimit = registrySize - registrySizeThreshold;
//两者中取小的值,为本常剔除的数量
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if(toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", new Object[]{Integer.valueOf(toEvict), Integer.valueOf(expiredLeases.size()), Integer.valueOf(evictionLimit)});
Random random = new Random(System.currentTimeMillis());
//逐个剔除
for(int i = 0; i < toEvict; ++i) {
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = (Lease)expiredLeases.get(i);
String appName = ((InstanceInfo)lease.getHolder()).getAppName();
String id = ((InstanceInfo)lease.getHolder()).getId();
EurekaMonitors.EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
//剔除
this.internalCancel(appName, id, false);
}
}
return;
}
Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry = (Entry)var4.next();
leaseMap = (Map)groupEntry.getValue();
} while(leaseMap == null);
Iterator var7 = leaseMap.entrySet().iterator();
while(var7.hasNext()) {
Entry<String, Lease<InstanceInfo>> leaseEntry = (Entry)var7.next();
Lease<InstanceInfo> lease = (Lease)leaseEntry.getValue();
if(lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
}
服务下线
EurekaClient在应用销毁时候,会向Eureka Server发送下线请求
对于服务端的服务下线,其主要代码对应在com.netflix.eureka.registry.AbstractInstanceRegistry#cancel
public boolean cancel(String appName, String id, boolean isReplication) {
return this.internalCancel(appName, id, isReplication);
}
protected boolean internalCancel(String appName, String id, boolean isReplication) {
boolean var10;
try {
//读锁,防止被其他线程进行修改
this.read.lock();
EurekaMonitors.CANCEL.increment(isReplication);
//根据appName获取服务实例集群
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
//移除服务实例租约
if(gMap != null) {
leaseToCancel = (Lease)gMap.remove(id);
}
AbstractInstanceRegistry.CircularQueue var6 = this.recentCanceledQueue;
synchronized(this.recentCanceledQueue) {
this.recentCanceledQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), appName + "(" + id + ")"));
}
InstanceStatus instanceStatus = (InstanceStatus)this.overriddenInstanceStatusMap.remove(id);
if(instanceStatus != null) {
logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
}
//租约不存在,返回false
if(leaseToCancel == null) {
EurekaMonitors.CANCEL_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
boolean var17 = false;
return var17;
}
//设置租约的下线时间
leaseToCancel.cancel();
InstanceInfo instanceInfo = (InstanceInfo)leaseToCancel.getHolder();
String vip = null;
String svip = null;
if(instanceInfo != null) {
instanceInfo.setActionType(ActionType.DELETED);
this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
//设置缓存过期
this.invalidateCache(appName, vip, svip);
logger.info("Cancelled instance {}/{} (replication={})", new Object[]{appName, id, Boolean.valueOf(isReplication)});
var10 = true;
} finally {
//释放锁
this.read.unlock();
}
return var10;
}
集群同步
如果Eureka Server是通过集群方式进行部署,为了为维护整个集群中注册表数据一致性所以集群同步也是非常重要得事情。
集群同步分为两部分
- EurekaServer在启动过程中从他的peer节点中拉取注册表信息,并讲这些服务实例注册到本地注册表中;
- 另一部分是eureka server每次对本地注册表进行操作时,同时会讲操作同步到他的peer节点中,达到数据一致;
Eureka Server初始化本地注册表信息
在eureka server启动过程中,会从它的peer节点中拉取注册表来初始化本地注册表,这部分主要通过com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#syncUp 他从可能存在的peer节点中,拉取peer节点中的注册表信息,并将其中的服务实例的信息注册到本地注册表中。
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
//根据配置休停下
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
//获取所有的服务实例
Applications apps = eurekaClient.getApplications();
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
try {
//判断是否可以注册
if (isRegisterable(instance)) {
//注册到自身的注册表中
register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
count++;
}
} catch (Throwable t) {
logger.error("During DS init copy", t);
}
}
}
}
return count;
}
通过这一步保证了eureka启动时的数据一致性
Eureka Server之间注册表信息同步复制
为了保证Eureka Server集群运行时候注册表的信息一致性,每个eureka server在对本地注册表进行管理操作时,会将相应的信息同步到peer节点中。
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
等方法中,都回调用方法
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
//向peer集群中的每一个peer进行同步
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
//根据action调用不同的同步请求
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}