SpringCloudEureka源码详解

概述

Spring Cloud Eureka是Spring Cloud Netflix项目下的服务治理模块。

由于微服务概念的引入,使大型服务在一定程度上彻底的解耦,当服务集群足够庞大的时候,服务治理成为了微服务的痛点之一。

Eureka是Spring Could中服务发现的推荐组件,保证服务的高可用性,它有着丰富的API,使得Eureka作为服务发现与治理都比较方便。

架构与原理

架构图

  • Eureka Server:服务的注册中心,负责维护注册的服务列表。
  • Service Provider:服务提供方,作为一个Eureka Client,向Eureka Server做服务注册、续约和下线等操作,注册的主要数据包括服务名、机器ip、端口号、域名等等。
  • Service Consumer:服务消费方,作为一个Eureka Client,向Eureka Server获取Service Provider的注册信息,并通过远程调用与Service Provider进行通信

Eureka Server作为一个独立的部署单元,以REST API的形式为服务实例提供了注册、管理和查询等操作。同时,Eureka Server也为我们提供了可视化的监控页面,可以直观地看到各个Eureka Server当前的运行状态和所有已注册服务的情况。如图:

eureka

原理:

服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,

然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。

当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。

服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。

源码解读:

eureka主体实现方式:

ApplicationResource类接收Http服务请求,调用PeerAwareInstanceRegistryImpl的register方法,PeerAwareInstanceRegistryImpl完成服务注册后,调用replicateToPeers向其它Eureka Server节点(Peer)做状态同步。

eureka client

启动时候会创建一个定时任务,定时任务会将本地的服务配置信息,也就是注册到远端的服务信息自动刷新到注册服务器上,实现了服务注册以及缓存更新的机制。

1、com.netflix.discovery.DiscoveryClient.java中的可以看到initScheduledTasks方法,它封装了一个instanceInfoReplicator的定时任务,以一定的时间(默认30秒)来刷新服务的缓存和心跳信息。

2、instanceInfoReplicator中的run方法调用register来实现注册功能,start方法实现了定时刷新调用,定时注册到eureka

eureka server

1、com.netflix.eureka.resources.ApplicationResource 中使用addInstance方法接收来自client的请求消息,然后进行处理,最终的注册信息缓存在ConcurrentHashMap中,实现服务缓存。

Eureka的自我保护机制:

在默认情况下,Eureka Server在默认90s时间内没有收到服务端的心跳(默认30秒一次心跳,三次心跳),会将该服务注销。在一般情况下,网络通信的故障率较高,在网络通信出现异常时,Eureka Server如果正常注销服务,

将会导致大部分服务不可用,这违背了微服务高可用的初衷,在这种情况下,Eureka Server有自我保护机制,当它在短时间内丢失过多的客户端时(默认15分钟内低于85%),该节点将进入自我保护模式,不再注销服务,并且同时继续提供新服务的注册,当网络故障修复之后,该节点能自动的退出自我保护模式。

总之一句话:不管好数据坏数据,一个不落。

核心特性

  • Eureka通过相互注册与复制支持高可用
  • Eureka支持用户认证
  • Eureka Client支持注册表缓存
  • Eureka提供保护模式以解决网络分区故障
  • Eureka提供健康检查
  • Eureka支持RESTful API

为什么选择Eureka而非Zookeeper作为服务发现组件,以下对比:

对比图

使用方法

服务注册过程:

对比图

当实例状态发生变化时(上线Or下线),都会请求到eureka-server发送一个状态,在一定的时间后,eureka会对该服务进行加入或者删除,然后进行eureka集群的缓存复制。

创建Eureka Sever服务

1.创建一个Spring Boot工程,命名问Eureka-Server,并在pom文件中引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2.创建启动类

1
2
3
4
5
6
@EnableEurekaServer //用来指定该项目为Eureka的服务注册中心
@SpringBootApplication
public class EurekaApp {
public static void main(String[] args) {
SpringApplication.run(EurekaApp.class, args);
}

3.配置server服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# server (eureka 默认端口为:8761)
server.port=8761
# spring
spring.application.name=spring-cloud-server
# eureka
# 是否注册到eureka
eureka.client.register-with-eureka=false
# 是否从eureka获取注册信息
eureka.client.fetch-registry=false
# eureka服务器的地址(注意:地址最后面的 /eureka/ 这个是固定值)
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/

# info自定义,读取pom文件中的内容
info.build.name=@project.name@
info.build.description=@project.description@
info.build.groupId=@project.groupId@
info.build.artifact=@project.artifactId@
info.build.version=@project.version@
# 指定环境
eureka.environment=dev

#指定数据中心
#eureka.datacenter=roncoo

# 配置自我保护模式
eureka.server.enable-self-preservation=true

#设置清理无效节点的时间间隔,默认60000,即是60s
eureka.server.eviction-interval-timer-in-ms=5000
#设置连接密码
security.basic.enabled=true
security.user.name=qb
security.user.password=123456

4.配置完成启动eureka server即可,访问http://localhost:8761/eureka/

Eureka高可用集群配置:

高可用

三注册中心,两两互相注册将eureka.client.serviceUrl.defaultZone值设置为其他两节点值即可。

创建Eureka Client项目

1.创建Springboot项目,引入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2.新增启动类

1
2
3
4
5
6
7
8
9
10
11
/**
*
* @EnableEurekaServer
* 用来指定该项目为Eureka的服务注册中心
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ClientApp {
public static void main(String[] args) {
SpringApplication.run(EurekaApp.class, args);
}

3.配置client连接上服务发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# server
server.port=8888

# spring
spring.application.name=spring-cloud-consumer

# eureka
eureka.client.serviceUrl.defaultZone=http://qb:123456@localhost:8761/eureka/

#自定义访问路径
eureka.instance.status-page-url-path=/info
#自定义实例ID
eureka.instance.instanceId=${spring.application.name}:${random.value}
#显示IP地址
eureka.instance.prefer-ip-address=true

#设置拉取服务注册信息时间,默认60s
eureka.client.registry-fetch-interval-seconds=30

#指定续约更新频率,默认是30s
eureka.instance.lease-renewal-interval-in-seconds=15

#设置过期剔除时间,默认90s
eureka.instance.lease-expiration-duration-in-seconds=45

4.启动后,能在Eurekaweb端上看到服务的列表

服务发现使用场景

服务发现并不是为了服务发现而服务发现,是为了使用一些必要的功能而必不可少的组件,服务发现的下游有丰富的内部服务调用工具

  • Ribbon,实现客户端的负载均衡。
  • Hystrix,断路器。
  • Feign,RESTful Web Service客户端,整合了Ribbon和Hystrix。

服务调用端负载均衡——Ribbon

Ribbon是Netflix发布的开源项目,主要功能是为REST客户端实现负载均衡。它主要包括六个组件:

  1. ServerList,负载均衡使用的服务器列表。这个列表会缓存在负载均衡器中,并定期更新。当Ribbon与Eureka结合使用时,ServerList的实现类就是DiscoveryEnabledNIWSServerList,它会保存Eureka Server中注册的服务实例表。
  2. ServerListFilter,服务器列表过滤器。这是一个接口,主要用于对Service Consumer获取到的服务器列表进行预过滤,过滤的结果也是ServerList。Ribbon提供了多种过滤器的实现。
  3. IPing,探测服务实例是否存活的策略。
  4. IRule,负载均衡策略,其实现类表述的策略包括:轮询、随机、根据响应时间加权,(可以自定义负载均衡策略,实现完之后可以重新注入ribbon)
  5. ILoadBalancer,负载均衡器。这也是一个接口,Ribbon为其提供了多个实现,比如ZoneAwareLoadBalancer。而上层代码通过调用其API进行服务调用的负载均衡选择。一般ILoadBalancer的实现类中会引用一个IRule。
  6. RestClient,服务调用器。顾名思义,这就是负载均衡后,Ribbon向Service Provider发起REST请求的工具。

Ribbon工作时会做四件事情:

1.优先选择在同一个Zone(区域)且负载较少的Eureka Server;
2.定期从Eureka更新并过滤服务实例列表;
3.根据用户指定的策略,在从Server取到的服务注册列表中选择一个实例的地址;
4.通过RestClient进行服务调用。

Ribbon的源码实现大致原理:

  • LoadBalancerClient : 继承了ServiceInstanceChooser接口,实现类是RibbonLoadBalancerClient.主要方法有choose(ServiceInstanceChooser用来选择instance) ,execute(LoadBalancerClient 用来执行).

  • ILoadBalancer:接口方法有addServers,chooseServer,markServerDown,getReachableServers,getAllServers.(负载均衡)实现类为BaseLoadBalancer 和 DynamicServerListLoadBalancer.

  • BaseLoadBalancer :主要由以下类进行配置IClientConfig(基本配置,用于初始化) IRule(路由策略) IPing (判断响应) (静态配置负载均衡)

  • DynamicServerListLoadBalancer: ServerList(用于从Eureka中获取服务列表) ServerListFilter(列表过滤) 动态配置负载均衡)

负载均衡过程:

1.RibbonLoadBalancerClient接收到一个serviceid之后,调用ServiceInstanceChooser的choose方法,choose方法首先得到 ILoadBalancer (当中有client列表)。再利用ILoadBalancer 的chooseServer方法得到普通Server实例并实例化RibbonServer,并返回。chooseserver会通过loadbalancer中的rule来返回正确的instance。

2.DynamicServerListLoadBalancer由iconfig初始化,初始化完成后调用updateListOfServers方法获得所有ServerList。(方法中通过ServerList实现类来访问EurekaClient中的注册列表)

3.BaseLoadBalancer中有一个PingTask任务,他每10秒钟会向EurekaClient发送一个Ping。如果从Eureka拉取的注册列表发生了改变,则重新更新列表。

4.LoadBalancerClient根据注册列表和IRule来进行负载均衡

Ribbon的应用

springcloud提供了默认的配置RibbonClientConfiguration。它提供了包含ILoadBalancer,ServerListFilter在内的许多配置。

你可以更改默认的配置,更改方法为在.property文件中添加.ribbon.*的配置项

服务调用端熔断——Hystrix

Netflix创建了一个名为Hystrix的库,实现了断路器的模式。

“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
hystrix
正常情况下,在请求失败频率较低的情况下,Hystrix还是会直接把故障返回给客户端。只有当失败次数达到阈值(默认在20秒内失败5次)时,断路器打开并且不进行后续通信,而是直接返回备选响应。

当然,Hystrix的备选响应也是可以由开发者定制的。
hystrix2

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面,Hystrix Dashboard Wiki上详细说明了图上每个指标的含义。

hystrix3

服务调用端代码抽象和封装——Feign

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。

它整合了Ribbon和Hystrix,从而让我们不再需要显式地使用这两个组件。

Feign还提供了HTTP请求的模板,通过编写简单的接口和插入注解,我们就可以定义好HTTP请求的参数、格式、地址等信息。

接下来,Feign会完全代理HTTP的请求,我们只需要像调用方法一样调用它就可以完成服务请求。

Feign具有如下特性:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持Hystrix和它的Fallback
  • 支持Ribbon的负载均衡
  • 支持HTTP请求和响应的压缩

以下是一个Feign的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients //启用fegin调用
public class Application
{
public static void main(String[] args)
{
SpringApplication.run(Application.class, args);
}
}
@FeignClient(name = "elements", fallback = ElementsFallback.class) //指定feign调用的服务和Hystrix Fallback(name即eureka的application name)
public interface Elements
{
@RequestMapping(value = "/index")
String index();
}

//Hystrix Fallback
@Component
public class ElementsFallback implements Elements
{
@Override
public String index()
{
return "熔断生效";
}
}

//测试类
@Component
public class TestController {
@Autowired
Elements elements;

@RequestMapping(value = "/testEureka", method = RequestMethod.GET)
public String testeureka()
{
return elements.index();
}
}

说明:
(1)使用 @Component 注解向SpringBoot中注入该组件。

(2)使用@FeignClient("XXX")注解来绑定该接口对应的服务。

注意:在启动类上加 @EnableFeignClients 注解,如果定义的Feign接口定义跟启动类不在一个包名下,还需要制定扫描的包名:@EnableFeignClients(basePackages = "xxx.xxx.xxx")

建议将接口定义,单独抽一个项目出来,后面打成公共的jar,这样无论是哪个项目需要调用接口,引入公共的接口SDK jar即可,不需要重新定义一遍。

注意事项

eureka在服务下线后30秒节点还存在,需妥善处理

替代方案

Eureka->consul consul实际也是CP型服务发现,并且监控的指标较多,可作为替代方案

Ribbon->nginx 负载均衡nginx有相对较为成熟的机制,但是配置项较多,谨慎使用

0%