46-Eureka
1. 单机 Eureka 构建步骤
- 新增模块 cloud-eureka-server-7001
- 编写 pom.xml
<parent> <artifactId>study-cloud</artifactId> <groupId>cn.edu.nuist</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-eureka-server-7001</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>cn.edu.nuist</groupId> <artifactId>cloud-api-common</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
- application.yml
server: port: 7001 eureka: instance: hostname: localhost # Eureka服务端的实例名字 client: register-with-eureka: false # 标识不向注册中心注册自己 fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与 Eureka Server 交互的地址 (查询服务和注册服务都需要依赖这个地址)
- 启动类上新增注解:
@EnableEurekaServer
3. 集群 Eureka 搭建步骤
搭建 Eureka 集群,实现负载均衡 + 故障容错
cloud-eureka-server-7001#application.yml
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com # Eureka 服务端的实例名字
client:
register-with-eureka: false # 标识不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7002.com:7002/eureka/
cloud-eureka-server-7002#application.yml
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com # Eureka 服务端的实例名字
client:
register-with-eureka: false # 标识不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
修改客户端配置:
- defaultZone:把 Eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 Eureka Server 可以同步注册表;
- instance-id:自定义实例显示格式,建议加上版本号,便于多版本管理(e.g.
${spring.cloud.client.ipaddress}:${spring.application.name}:${server.port}:@project.version@
); - prefer-ip-address:是否使用 IP 注册,否则会使用主机名注册;
3. 服务发现
8001 启动类上新增 @EnableDiscoveryClient
,并在 controller 中新增方法来遍历服务列表:
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("service")
public Object discoveryService() {
List<String> serviceList = discoveryClient.getServices();
for (int i = 0; i < serviceList.size(); i++) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceList.get(i));
for (int j = 0; j < instances.size(); j++) {
log.info("[{}] {}:{}", serviceList.get(i),
instances.get(j).getHost(), instances.get(j).getPort());
}
}
return new CommonResult<>(HttpStatus.OK.value(), null, null);
}
4. 配置详解
4.1 元数据
Eureka 的元数据有两种:标准元数据和自定义元数据。
- 标准元数据:主机名、IP 地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用;
- 自定义元数据:可以使用
eureka.instance.metadata-map
配置,符合 KEY/VALUE 的存储格式。这 些元数据可以在远程客户端中访问。
instance:
prefer-ip-address: true
metadata-map:
cluster: cl1
region: rn1
4.2 客户端
服务提供者(也是 Eureka 客户端)要向 EurekaServer 注册服务,并完成服务续约等工作。
a. 服务注册
- 当我们导入了 eureka-client 依赖坐标,配置 Eureka 服务注册中心地址;
- 服务在启动时会向注册中心发起注册请求,携带服务元数据信息;
- Eureka 注册中心会把服务的信息保存在 Map 中。
b. 服务续约
服务每隔 30s 会向注册中心续约(心跳)一次(也称为“报活”),如果没有续约,租约在 90s 后到期,然后服务会被失效。每隔 30s 的续约操作我们称之为「心跳检测」。
往往不需要我们调整这两个配置。
# 向 Eureka 服务中心集群注册服务
eureka:
instance:
# 租约续约间隔时间,默认30s
lease-renewal-interval-in-seconds: 30
# 租约到期(服务失效时间默认90s),服务超过90s 没有发送心跳,EurekaServer 会将其从列表移除
lease-expiration-duration-in-seconds: 90
c. 获取服务列表
每隔 30s 服务会从注册中心中拉取一份服务列表,这个时间可以通过配置修改。往往不需要我们调整。
# 向 Eureka 服务中心集群注册服务
eureka:
client:
# 每隔多久拉取一次服务列表
registry-fetch-interval-seconds: 30
- 服务消费者启动时,从 EurekaServer 服务列表获取只读备份,缓存到本地;
- 每隔 30s,会重新获取并更新数据;
- 时间可以通过配置
eureka.client.registry-fetch-interval-seconds
修改。
4.3 服务端
a. 服务下线
- 当服务正常关闭操作时,会发送服务下线的 REST 请求给 EurekaServer;
- 服务中心接受到请求后,将该服务置为下线状态;
b. 失效剔除
Eureka Server会定时(间隔为 eureka.server.eviction-interval-timer-in-ms
,默认 60s)进行检查,如果发现实例在在一定时间(此值由客户端设置的 eureka.instance.lease-expiration-duration-in-seconds
定义,默认值为 90s)内没有收到心跳,则会注销此实例。
c. 自我保护
服务提供者 → 注册中心
定期的续约(服务提供者和注册中心通信),假如服务提供者和注册中心之间的网络有点问题,不代表服务提供者不可用,不代表服务消费者无法访问服务提供者。
如果在 15min 内超过 85% 的客户端节点都没有正常的心跳,那么 Eureka 就认为客户端与注册中心出现了网络故障,Eureka Server 自动进入「自我保护机制」。
为什么会有「自我保护机制」?
默认情况下,如果 Eureka Server 在一定时间内(默认 90s)没有接收到某个微服务实例的心跳,Eureka Server 将会移除该实例。但是当网络分区故障发生时,微服务与 Eureka Server 之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了「自我保护机制」。
服务中心页面会显示如下提示信息:
当处于「自我保护机制」时:
- 不会剔除任何服务实例(可能是服务提供者和 EurekaServer 之间网络问题),保证了大多数服务依然可用;
- Eureka Server 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用,当网络稳定时,当前 Eureka Server 新的注册信息会被同步到其它节点中;
- 在 Eureka Server 工程中通过
eureka.server.enable-self-preservation
配置可关停自我保护,默认是打开的。
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
怎么禁止自我保护(一般生产环境中不会禁止自我保护)?
出厂默认自我保护机制是开启的:eureka.server.enable-self-preservation=true
,可以将该配置项设置为 false 以禁用自我保护模式。
心跳检测与续约时间:
eureka:
server:
# enable-self-preservation: false
# eviction-interval-timer-in-ms: 2000
instance:
hostname: eureka7001.com # Eureka服务端的实例名字
# Eureka 客户端向服务端发送心跳的时间间隔,单位为秒(默认 30 秒)
# lease-renewal-interval-in-seconds: 1
# Eureka 服务端在接收到最后一次心跳后等待事件上限,单位为秒(默认 90 秒),超时将剔除服务
# lease-expiration-duration-in-seconds: 2
client:
register-with-eureka: false # 标识不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7002.com:7002/eureka/
# 设置与 Eureka Server 交互的地址 (查询服务和注册服务都需要依赖这个地址)
5. 源码解析
5.1 Server 启动过程
【入口】SpringCloud 充分利用了 SpringBoot 的自动装配特点。
(1)观察 eureka-server 的 jar 包,发现在 META-INF 下面有配置文件 spring.factories;
(2)SpringBoot 应用启动时会加载 EurekaServerAutoConfiguration 自动配置类;
(3)图中的 ① 表明需要一个 MarkerBean 才能装配 EurekaServer,而这个 Bean 是由 @EnableEurekaServer 引入的;
也就是说只有添加了 @EnableEurekaServer
注解,才会有后续的动作,这是成为一个 EurekaServer 的前提。
(4)图中的 ② 关注 EurekaServerAutoConfiguration;
而在 com.netflix.eureka.cluster.PeerEurekaNodes#start
方法中:
start 何时调用?回到主配置类中:
(5)再回到主配置类,看还注册了哪些 Bean:
(6)图中 ③ 关注 EurekaServerInitializerConfiguration;
现在进入 org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#contextInitialized
方法:
重点关注 initEurekaServerContext()
:
→ 先进入上图中的 syncUp 方法:
深入 com.netflix.eureka.registry.AbstractInstanceRegistry#register
(提供实例注册功能):
→ 继续进入 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#openForTraffic
:
进入 postInit()
查看:
5.2 Server 服务接口暴露
上面(5)曾提到在 Eureka Server 启动过程中在主配置类注册了 Jersey 框架(是⼀个发布 Restful 接口的框架,类似于 SpringMVC)。
注入的 Jersey 细节:
对外提供的接口服务,在 Jersey 中叫做资源:
这些就是使用 Jersey 发布的供 Eureka Client 调用的 Restful 风格服务接口(完成服务注册、⼼跳续约等接口)。
5.3 Server 服务注册接口
(1)ApplicationResource#addInstance()
中的代码:registry.register(info, "true".equals(isReplication));
(2)com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
注册服务信息并同步到其它 Eureka 节点;
AbstractInstanceRegistry#register()
:注册,实例信息存储到的注册表是⼀个 ConcurrentHashMap。
(3)super.register
过程前面已经看过了,这里不做复述。现在进入 replicateToPeers
复制到 Eureka 对等节点:
(4)进入 PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers
:
5.4 Server 服务续约接口
InstanceResource#renewLease
中完成客户端的心跳(续约)处理,关键代码:registry.renew(app.getName(), id, isFromReplicaNode);
进入 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
:
renew() → leaseToRenew.renew() → 对最后更新时间戳进行更新
5.5 Client 注册服务
引入 jar 就会被自动装配,分析 EurekaClientAutoConfiguration 类:
如果不想作为客户端,可以设置 eureka.client.enabled=false
:
回到主配置类 EurekaClientAutoConfiguration,思考:EurekaClient 启动过程要做什么事情?
- 读取配置文件;
- 启动时从 EurekaServer 获取服务实例信息;
- 注册自己到 EurekaServer(addInstance);
- 开启⼀些定时任务(心跳续约,刷新本地服务缓存列表)。
a. 读取配置文件
b. 获取服务实例信息
c. 注册自己
d. 开启定时任务
心跳续约、刷新本地服务缓存列表。
(1)刷新本地缓存
(2)心跳续约定时任务
5.6 服务下架
服务下架,服务死掉的时候就会调用 shutdown com.netflix.discovery.DiscoveryClient#shutdown
:
6. 主流服务中心
6.1 ZooKeeper
Zookeeper 是一个分布式服务框架,是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应 用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。简单来说 Zk 本质 = 存储 + 监听通知。
Zookeeper 用来做服务注册中心,主要是因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能及时的通知到监听客户端,这样作为调用方只要使用 Zookeeper 的客户端就能实现服务节点的订阅和变更通知功能了,非常方便。另外,Zookeeper 可用性也可以,因为只要半数以上的选举节点存活,整个集群就是可用的。
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
配置:
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.206.131:2181
注解:
@EnableDiscoveryClient
启动时报错,错误堆栈中有 main 函数,说明启动就报错了 -> jar 包依赖冲突:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!-- 排除 zk 3.5.3 -->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加zk 3.4.9 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
服务节点是临时节点:
6.2 Nacos
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说 Nacos 就是注册中心 + 配置中心的组合,帮助我们解决微服务开发必会涉及到的服务注册 与发现,服务配置,服务管理等问题。Nacos 是 Spring Cloud Alibaba 核心组件之一,负责服务注册与发现,还有配置。
后面会详细说。
6.3 Consul
Consul 是由 HashiCorp 基于 Go 语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件, 采用 Raft 算法保证服务的一致性,且支持健康检查。
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
配置:
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
注解:
@EnableDiscoveryClient
访问 http://localhost:8500 查看 Consul 的图形化界面:
6.4 区别
- AP:当网络分区出现后(B 同步数据失败),为了保证可用性,系统 B 可以返回旧值,保证系统的可用性;
- CP:当网络分区出现后(B 同步数据失败),为了保证一致性,系统 B 必须拒接请求,否则无法保证一致性。
7. 服务发现慢的原因
如果你刚刚接触 Eureka,对 Eureka 的设计和实现都不是很了解,可能就会遇到一些无法快速解决的问题,这些问题包括:新服务上线后,服务消费者不能访问到刚上线的新服务,需要过一段时间后才能访问?或是将服务下线后,服务还是会被调用到,一段时候后才彻底停止服务,访问前期会导致频繁报错?这些问题还会让你对 Spring Cloud 产生严重的怀疑,这难道不是一个 Bug?
【问题场景】上线一个新的服务实例,但是服务消费者无感知,过了一段时间才知道某一个服务实例下线了,服务消费者无感知,仍然向这个服务实例在发起请求这其实就是服务发现的一个问题,当我们需要调用服务实例时,信息是从注册中心 Eureka 获取的,然后通过 Ribbon 选择一个服务实例发起调用,如果出现调用不到或者下线后还可以调用的问题,原因肯定是服务实例的信息更新不及时导致的。
Eureka 服务发现慢的原因主要有两个,一部分是因为服务端缓存导致的,另一部分是因为客户端缓存导致的。
(1)服务端缓存
服务注册到注册中心后,服务实例信息是存储在注册表中的,也就是内存中。但 Eureka 为了提高响应速度,在内部做了优化,加入了两层的缓存结构,将 Client 需要的实例信息,直接缓存起来,获取的时候直接从缓存中拿数据然后响应给 Client。
第 1 层缓存是 readOnlyCacheMap,readOnlyCacheMap 是采用 ConcurrentHashMap 来存储数据的,主要负责定时 readWriteCacheMap 进行数据同步,默认同步时间为 30s 一次。
第 2 层缓存是 readWriteCacheMap,readWriteCacheMap 采用 Guava 来实现缓存。缓存过期时间默认为 180s,当服务下线、过期、注册、状态变更等操作都会清除此缓存中的数据。
Client 获取服务实例数据时,会先从一级缓存中获取,如果一级缓存中不存在,再从二级缓存中获取,如果二级缓存也不存在,会触发缓存的加载,从存储层拉取数据到缓存中,然后再返回给 Client。
Eureka 之所以设计二级缓存机制,也是为了提高 Eureka Server 的响应速度,缺点是缓存会导致 Client 获取不到最新的服务实例信息,然后导致无法快速发现新的服务和已下线的服务。
了解了服务端的实现后,想要解决这个问题就变得很简单了,我们可以缩短只读缓存的更新时间 eureka.server.response-cache-update-interval-ms
让服务发现变得更加及时,或者直接将只读缓存关闭 eureka.server.use-read-onlyresponse-cache=false
,多级缓存也导致 C 层面(数据一致性)很薄弱。
Eureka Server 中会有定时任务去检测失效的服务,将服务实例信息从注册表中移除,也可以将这个失效检测的时间缩短,这样服务下线后就能够及时从注册表中清除。
(2)客户端缓存
客户端缓存主要分为两块内容,一块是 Eureka Client 缓存,一块是 Ribbon 缓存。
Eureka Client 缓存
Eureka Client 负责跟 Eureka Server 进行交互,在 EurekaClient 中的 com.netflix.discovery.DiscoveryClient.initScheduledTasks()
方法中,初始化了一个 CacheRefreshThread 定时任务专门用来拉取 Eureka Server 的实例信息到本地。所以我们需要缩短这个定时拉取服务信息的时间间隔 eureka.client.registryFetchIntervalSeconds
来快速发现新的服务。
Ribbon 缓存
Ribbon 会从 EurekaClient 中获取服务信息,ServerListUpdater 是 Ribbon 中负责服务实例更新的组件,默认的实现是 PollingServerListUpdater,通过线程定时去更新实例信息。定时刷新的时间间隔默认是 30s,当服务停止或者上线后,这边最快也需要 30s 才能将实例信息更新成最新的。我们可以将这个时间调短一点,比如 3s。
刷新间隔的参数是通过 getRefreshIntervalMs()
方法来获取的,方法中的逻辑也是从 Ribbon 的配置中进行取值的。
将这些服务端缓存和客户端缓存的时间全部缩短后,跟默认的配置时间相比,快了很多。我们通过调整参数的方式来尽量加快服务发现的速度,但是还是不能完全解决报错的问题,间隔时间设置为 3s,也还是会有间隔。所以我们一般都会开启重试功能,当路由的服务出现问题时,可以重试到另一个服务来保证这次请求的成功。