写作背景
SpringCloud Netflix作为SpringCloud第一代产品很经典,而且公司的老项目还在用SpringCloud Netflix的技术栈,有必要对SpringCloud Netflix的各种核心组件回归复习一下了。
本次复习的主角是微服务注册中心Eureka,本文的书写思路是五个方面
- Eureka是用来干什么的,为什么会有Eureka
- Eureka的核心功能有哪些
- 上手搭建Eureka的服务端和客户端实战一下
- 从源码的角度验证一下Eureka的核心功能
- Eureka常见问题FAQ
Eureka是用来干什么的?
首先来想一个问题,在微服务的架构下,服务之间的调用关系如何维护?
服务A调用服务B,那我服务A需要在代码里记录服务B的访问的IP和端口,现在一个请求经过API网关然后调用多个下游服务,然后聚合各个下游服务返回请求的响应是很常见的,如果都是把下游服务的IP和端口硬编码在代码里,很不优雅。
还是拿服务A调用服务B举例,服务A想知道服务B的访问IP和端口,有没有一个组件可以告诉我服务A,我给你服务B的名字,你返回我服务B的访问IP和端口等信息,这样我服务A就不用自己在代码里去维护服务B的请求IP和端口了。Eureka的一个核心功能之一就是服务发现,服务A可以通过Eureka来发现服务B的访问IP和端口,在实现服务发现的基础上需要Eureka的客户端先进行服务注册,也就是Eureka的客户端程序也就是你的服务,比如服务B先将自己注册到Eureka的服务端,然后服务A就可以通过Eureka拉取到服务注册表信息找到服务B的访问IP和端口。
Eureka的核心功能
1、服务注册(register)
Eureka Client在服务启动时会发送Rest请求的方式向Eureka Server注册自己的服务,注册的时候会提供服务自身的一些元数据,比如IP和端口。Eureka Server在接收到注册请求后,会将这些元数据信息存储在一个双层的Map中,这个Map其实就是服务注册表。
2、服务续约(renew)
服务续约是Eureka Client在服务注册后,会定时(默认每30s)向Eureka Server发送心跳通知Eureka Server 我还活着,还是处于可用的状态,防止被Eureka Server剔除。
3、服务下线(cancel)
Eureka Client在服务关闭或者重启时,会主动向Eureka Server发送Rest请求,告诉Eureka Server自己要下线了,Eureka Server在收到下线请求后,会把该服务的状态设置为DOWN
4、服务同步
在生产环境下,为了防止单点问题,Eureka往往会搭建HA架构,Eureka Server之间会互相进行注册,构建一个Eureka Server集群,不同的Eureka Server之间会进行服务同步,来保证服务信息的一致性。
5、服务剔除(evict)
服务剔除是Eureka Server在启动时会启动一个定时任务,默认每60s扫描一次服务注册表,如果发现服务超过90s没有续约,那么就把这个服务实例剔除掉,后续在这个服务恢复之前,这个服务实例将不再对外提供服务。
6、自我保护
因为有服务剔除机制,那么就有可能因为是网络故障等原因,导致服务续约没有成功,而实际上服务还是可用的情况,但是Eureka Server把所有服务都剔除下线了,这样显然不太合理。为了防止因短期网络波动引起的服务续约失败,导致Eureka Server剔除所有服务的情况,就有了自我保护机制。具体的做法其实就是在短期内,统计服务续约失败的比例,如果达到了一个阈值,那么就触发自我保护Eureka Server不再提出任务服务,直到比例恢复正常后,才退出自我保护。
7、获取服务
Eureka Client在启动的时候,会发送一个REST请求给Eureka Server,获取服务注册表,并且缓存在Eureka Client本地,默认缓存30秒更新缓存一次。同时,为了性能考虑,Eureka Server也会维护一份只读缓存readOnlyCacheMap,该缓存每隔30秒更新一次
上手搭建Eureka的服务端和客户端实战一下
说明一下基于SpringBoot 2.x搭建的Eureka Server
1、pom.xml引入Eureka的依赖坐标
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2、启动了开启服务注册
/**
* @author zhangyu
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3、配置文件application.yml里增加Eureka 的配置信息
#端口
server:
port: 8761
spring:
application:
name: eureka-server
#eureka相关配置
eureka:
client:
#表示是否将自己注册到Eureka Server,默认为true,由于当前应用就是Eureka Server,故而设为false
register-with-eureka: false
# 表示是否从Eureka Server获取注册信息,默认为true,因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
#Eureka 服务端配置,其实都是默认配置,这里写出来增加记忆
server:
#是否开启自我保护机制,默认是true也就是开启
enable-self-preservation: true
#开启自我保护后,期望心跳次数的阈值
renewal-percent-threshold: 0.85
#是否开启只读缓存,默认开启
use-read-only-response-cache: true
#将readWriteCache读写缓存数据定时刷入readOnlyCache只读缓存的时间,默认30秒
response-cache-update-interval-ms: 30000
#readWriteCache读写缓存被动过期时间,默认180秒更新一次读写缓存
response-cache-auto-expiration-in-seconds: 180
启动eureka-server服务,然后访问http://localhost:8761/ 就会看到Eureka Server的UI界面
生产环境下一般访问Eureka界面都是要账户密码的,所以需要和Security整合
Eureka与Security整合安全访问
1、pom.xml引入Security的依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、开启Web的Security保护
/**
* @author zhangyu
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http.csrf().disable();
super.configure(http);
}
}
3、配置文件application.yml配置访问Security的账户和密码
spring:
application:
name: eureka-server
security:
user:
name: root
password: 123456
4、修改暴露给其他Eureka Client注册的地址
eureka:
client:
service-url:
#改动的在这里
defaultZone: http://${
spring.security.user.name}:${
spring.security.user.password}@localhost:8761/eureka/
重启eureka-server服务,然后再次访问http://localhost:8761/ 你会发现页面被转发到登录页面http://localhost:8761/login
输入root和123456才会进入Eureka Server的UI界面。
快速用SpringBoot搭建一个服务然后注册到Eureka Server
1、pom.xml引入Eureka Client的依赖坐标
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、启动类开启Eureka Client
启动类增加@EnableEurekaClient注解
@EnableEurekaClient
@SpringBootApplication
public class ServiceScreenApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceScreenApplication.class, args);
}
}
3、配置文件applicatiion.yml配置Eureka信息
#端口
server:
port: 8003
spring:
application:
name: fc-service-screen
#eureka相关配置
eureka:
client:
service-url:
defaultZone: http://root:123456@localhost:8761/eureka/
instance:
#显示的微服务名称
instance-id: ms-service-screen-8003
#eureka客户端向服务端发送心跳时间默认30s
lease-renewal-interval-in-seconds: 10
#Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒
lease-expiration-duration-in-seconds: 30
启动fc-service-screen服务,然后去刷新http://localhost:8761/ 页面看看服务注册上去了没
搭建Eureka HA架构
再新起一个Eureka Server服务,端口设置为8762,然后最关键的地方是application.yml里关于Eureka的两个配置要结合起来使用。
新搭建的eureka-server8762服务的配置文件
#端口
server:
port: 8762
spring:
application:
name: eureka-server8762
security:
user:
name: root
password: 123456
#eureka相关配置
eureka:
client:
# HA架构的关键配置就是下面两个配置联合使用
# 将自己注册到Eureka Server,默认为true
register-with-eureka: true
# 表示是否从Eureka Server获取注册信息,默认为true,因为本身就是Eureka Server不需要
fetch-registry: false
service-url:
defaultZone: http://${
spring.security.user.name}:${
spring.security.user.password}@localhost:8761/eureka/,http://${
spring.security.user.name}:${
spring.security.user.password}@localhost:8762/eureka/
#Eureka 服务端配置,其实都是默认配置,这里写出来增加记忆
server:
#是否开启自我保护机制,默认是true也就是开启
enable-self-preservation: true
#开启自我保护后,期望心跳次数的阈值
renewal-percent-threshold: 0.85
#是否开启只读缓存,默认开启
use-read-only-response-cache: true
#将readWriteCache读写缓存数据定时刷入readOnlyCache只读缓存的时间,默认30秒
response-cache-update-interval-ms: 30000
#readWriteCache读写缓存被动过期时间,默认180秒更新一次读写缓存
response-cache-auto-expiration-in-seconds: 180
关键的地方是register-with-eureka 设置为true,相当于本身也是Eureka Client,然后就是fetch-registry设置为false,因为eureka-server8762本身也是Eureka Server它在服务启动时会从相邻的Eureka Server节点拉取注册表数据,然后服务注册时也会往其他Eureka Server节点转发注册保持数据一致性。然后就是暴露给Eureka Client注册的地址变成两个,用逗号隔开。
我们启动eureka-server和eureka-server8762两个服务,然后再启动fc-service-screen服务,然后访问两个Eureka Server的UI界面看看
eureka-server的
eureka-server8762的
你会发现两个Eureka Server的服务注册信息是一样,但是我的fc-service-sreen的Eureka注册地址只配置了http://root:123456@localhost:8761/eureka/也就是eureka-server这一台的。后面源码会分析,这个就是Eureka的服务同步功能。
从源码的角度验证一下Eureka的核心功能
先说明一下,我用的SpringBoot版本是2.2.2.RELEASE,小于2.7也就是说SpringBoot自动装配等的配置还是在META-INF/spring.factories文件里。
EurekaServer自动装配源码
我们知道在Eureka Server的启动类有加@EnableEurekaServer这个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
这个注解导入了EurekaServerMarkerConfiguration,我们看下这个Marker配置类有啥
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
我们发现这个EurekaServerMarkerConfiguration里就是声明了一个叫Marker的Bean。我们想想SpringBoot的自动装配的原理,一般都是XXXAutoConfiguration,大胆猜测一下是不是有个EurekaServerAutoConfiguration,果然在spring-cloud-netflix-eureka-server-2.2.1.RELEASE.jar的META-INF/spring.factories文件里找到了。
@Configuration(proxyBeanMethods = false)
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({
EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
...
}
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)看这个,有EurekaServerMarkerConfiguration.Marker这个Bean,EurekaServerAutoConfiguration才会生效,所以上面说到的EurekaServerMarkerConfiguration里其实就是开启一个Marker的开关用于控制EurekaServer自动装配的开关。
然后再看看@Import(EurekaServerInitializerConfiguration.class) 一般这种导入的类肯定是很关键的,我们进去看一下
@Configuration(proxyBeanMethods = false)
public class EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle, Ordered {
}
EurekaServerInitializerConfiguration实现了SmartLifecycle接口,这个SmartLifecycle是spring-context包里的东西,它的作用是在Spring容器的refresh()方法里的finishRefresh()方法里会去调用SmartLifecycle的start()方法,我们看下EurekaServerInitializerConfiguration的start()方法
@Override
public void start() {
new Thread(() -> {
try {
// 初始化EurekaServer
eurekaServerBootstrap.contextInitialized(
EurekaServerInitializerConfiguration.this.servletContext);
log.info("Started Eureka Server");
publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
EurekaServerInitializerConfiguration.this.running = true;
publish(