文章目录
1. Eureka简介
问题回顾分析:
在上一节的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至几十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦,这与DevOps的思想是背道而驰的。
Eureka做什么?
Eureka
:负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合调用者需求的服务告诉调用者。
同时,服务提供方与Eureka之间通过「心跳」机制进行监控,当某个服务提供方出现问题时,Eureka自然会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控。
这就好比是网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka就好比滴滴。
Eureka原理:
基本架构:
- Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
- 服务提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
- 心跳(续约):提供者定期通过HTTP方式向Eureka刷新自己的状态
工作原理图解析:
2. 入门案例
2.1 编写EurekaServer
Eureka是服务注册中心,只做服务注册;自身并不提供服务也不消费服务。可以搭建Web工程使用Eureka,也可以使用Spring Boot方式搭建。
基于上一节创建的父工程再创建一个eureka-server子工程作为服务注册中心
1.eureka-server子工程的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>springcloud1</artifactId>
<groupId>top.peng</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<!--引入netflix-eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
2.启动类:
@EnableEurekaServer //声明当前应用为Eureka服务
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.编写配置文件application.yml:
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
defaultZone: http://127.0.0.1:10086/eureka
# 不注册自己 不写默认为true
register-with-eureka: false
# 不拉取服务 不写默认为true
fetch-registry: false
4.启动服务,并访问:http://127.0.0.1:10086/
启动报错的看这篇文章:https://blog.csdn.net/qq_40932102/article/details/113481879
2.2 服务注册
在上一节编写的服务提供工程user-service上添加Eureka客户端依赖;自动将服务注册到EurekaServer服务地址列表。
1.添加依赖
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在启动类上开启Eureka客户端功能
通过添加@EnableDiscoveryClient
来开启Eureka客户端发现功能
@SpringBootApplication
@MapperScan("top.peng.user.dao")
@EnableDiscoveryClient //开启Eureka客户端发现功能
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
3.修改配置
server:
port: 80
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/jtest?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
application:
name: user-service
mybatis:
type-aliases-package: top.peng.user.beans
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
注意:
- 这里添加了
spring.application.name
属性来指定应用名称,将来会作为应用的id使用。 - 不用指定
register-with-eureka
和fetch-registry
,因为默认是true
4.重启项目,访问Eureka监控页面 http://127.0.0.1:10086/
可以发现user-service服务已经成功注册到Eureka中了
2.3 服务发现
在上一节编写的服务消费工程consumer-demo上添加Eureka客户端依赖;可以使用工具类DiscoveryClient根据服务名称获取对应的服务地址列表。
1.添加依赖
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在启动类上添加开启Eureka客户端发现的注解
@SpringBootApplication
@EnableDiscoveryClient //开启Eureka客户端
public class ConsumerApplication {
//省略
}
3.修改配置
spring:
application:
name: consumer-demo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
4.修改controller代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例:
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/{id}")
public User queryUserById(@PathVariable Long id){
//通过服务名称获取地址列表
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = serviceInstances.get(0);
//拼接url
String url = "http://" + serviceInstance.getHost() + ":" +serviceInstance.getPort() + "/user/" + id;
return restTemplate.getForObject(url, User.class);
}
}
5.Debug跟踪运行
访问http://localhost:8080/consumer/2
获取的地址列表:
生成的url:
访问结果:
3. Eureka详解
接下来我们详细讲解Eureka的原理及配置。
3.1 基础架构
Eureka架构中的三个核心角色:
-
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server
-
服务提供者
提供服务的应用,可以是Spring Boot应用,也可以是其它任意技术实现,只要对外提供的是REST风格服务即可。本例中就是我们实现的user-service
-
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer-demo
3.2 高可用的Eureka Server
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心 。
Eureka Server是一个web应用,可以启动多个实例(配置不同端口)保证Eureka Server的高可用。
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。而作为客户端,需要把信息注册到每个Eureka中。
如果有三个Eureka,则每一个EurekaServer都需要注册到其它几个Eureka服务中。
例如:有三个Eureka的端口分别为10086、10087、10088,则:
- 10086要注册到10087和10088上
- 10087要注册到10086和10088上
- 10088要注册到10086和10087上
搭建高可用的EurekaServer
假设要搭建两条EurekaServer的集群,端口分别为:10086和10087
-
修改原来的EurekaServer配置
server: port: ${port:10086} spring: application: name: eureka-server eureka: client: #eureka的服务地址,如果是集群,需要指定其他集群eureka地址 service-url: defaultZone: ${defaultZone:http://127.0.0.1:10086/eureka} # 不注册自己 #register-with-eureka: false # 不抓取服务 #fetch-registry: false
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:
- 删除了
register-with-eureka=false
和fetch-registry=false
两个配置。因为默认值是true,这样就会把自己注册到注册中心了。 - 把service-url的值改成了另外一台EurekaServer的地址,而不是自己
${port:10086}
为spel表达式,表示配置的port存在时,使用配置的port,没有配置时使用默认的port即10086。${defaultZone:http://127.0.0.1:10086/eureka}
同理。 - 删除了
-
另外一台在启动的时候可以指定端口port和defaultZone配置:
修改原来的启动配置组件:在如下界面中的 VM options 中设置 -DdefaultZone=http://127.0.0.1:10087/eureka
复制一份并修改:在如下界面中的 VM options 中 设置 -Dport=10087 -DdefaultZone=http://127.0.0.1:10086/eureka
- 启动测试,同时启动两台eureka server
可以发现,10086端口中有10087端口的Eureka服务,10087端口中也有10086端口的服务,这样我们就搭建了一个简单的高可用EurekaServer。
-
客户端注册服务到集群
因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:
eureka: client: service-url: # EurekaServer地址,多个地址以','隔开 defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
这样就可以实现注册的高可用,一个节点不可用时,可以注册到其他节点,待节点可用时,就会再自动同步到那个节点。
3.3 Eureka客户端和服务端配置
这个小节我们进行一系列的配置:
- Eureka客户端工程
- user-service 服务提供
- 服务地址使用ip方式
- 续约
- consumer-demo 服务消费
- 获取服务列表的频率
- user-service 服务提供
- Eureka服务端工程 eureka-server
- 失效剔除
- 自我保护
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
3.3.1 服务注册
服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否为true,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中 。
- 第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性,如:user-service
- 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081
- 值则是服务的实例对象,也就是说一个服务,这样可以同时启动多个不同实例,形成集群。
默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在 user-service 中添加配置如下:
eureka:
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名
修改完后先后重启 user-service 和 consumer-demo ;在调用服务的时候就已经变成ip地址;需要注意的是:不是在eureka中的控制台服务实例状态显示。
3.3.2 服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:「我还活着」。这个我们称为服务的续约(renew)。
有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置项:
eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
- lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
- lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
3.3.3 获取服务列表
当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。并且 每隔30秒会重新拉取并更新数据。可以在 consumer-demo 项目中通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 30 #每隔30秒更新一次服务列表数据
生产环境中,我们不需要修改这个值。但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
3.3.4 失效剔除和自我保护
如下的配置都是在Eureka Server服务端进行:
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:「我要下线了」。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到「服务下线」的请求。相对于服务提供者的「服务续约」操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。 可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒。
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%,当EurekaServer节点在短时间内丢失过多客户端(可能发生了网络分区故障)。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(默认为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(默认为60*1000ms)
4. 负载均衡Ribbon
在刚才的案例中,我们启动了一个user-service,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
接下来,我们就来使用Ribbon实现负载均衡。
4.1 启动两个服务实例
首先我们启动两个user-service实例,一个端口为9091,一个端口为9092
在user-service中配置如下端口:
server:
port: ${port:9091}
在启动配置中配置如下:
Eureka监控面板:
4.2 开启负载均衡
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码:
在RestTemplate的配置方法上添加@LoadBalanced
注解:
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@GetMapping("/{id}")
public User queryUserById(@PathVariable Long id){
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, User.class);
}
访问页面,查看结果
4.3 Ribbon原理
为什么现在只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然是有组件根据service名称,获取到了服务实例的ip和端口。因为 consumer-demo 使用的是RestTemplate,spring使用LoadBalancerInterceptor拦截器 ,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。
我们查看一下LoadBalancerInterceptor的源码:
4.4 负载均衡策略
Ribbon默认的负载均衡策略是简单的轮询,我们可以测试一下:
在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient来进行负载均衡的,其中有一个choose方法,是这样介绍的:
这个就是负载均衡获取实例的方法。
编写测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
public class LoadBalanceTest {
@Autowired
private RibbonLoadBalancerClient client;
@Test
public void test(){
for(int i = 0; i <100;i++){
ServiceInstance instance = client.choose("user-service");
System.out.println(instance.getHost()+":"+instance.getPort());
}
}
}
符合了我们的预期推测,确实是轮询方式。
SpringBoot也帮我们提供了修改负载均衡规则的配置入口:再服务使用者的配置文件中配置负载均衡规则
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
再次测试,发现结果变成了随机:
⏮ 上一节:SpringCloud微服务简单案例 ⏭下一节:Hystrix