1、系统架构演变
1.1 分布式服务架构
优点:
-
将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
问题:
-
服务越来越多,需要管理每个服务的地址(例如,A需要调用B、C、D、E四个服务,便需要配置四个服务的地址)
-
调用关系错综复杂,难以理清依赖关系(上图可见)
-
服务过多,服务状态难以管理,无法根据服务情况动态管理(例如,A需要调用B,B采用集群,集群有三个服务B1, B2, B3,到底调用哪个服务?怎么负载均衡?如果B1宕机了A怎么知道?)
1.2 面向服务架构(SOA)
当服务越来越多,需增加一个调度中心来实现服务治理。如:dubbo
服务治理要做什么?
-
服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
-
服务监控,使得服务调用透明化
-
服务状态监控等
问题:
-
一般而言,SOA架构只专注于服务治理,但是若项目拆分成多个微服务,只做服务治理还远远不够。
1.3 微服务架构
微服务架构是一种使用一套小服务来开发单个应用的方式或途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是RESTFUL API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务可使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
微服务的特点:
-
单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
-
微:微服务的服务拆分粒度很小,例如一个用户管理或部门管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
-
面向服务:面向服务是说每个服务都要对外暴露Restful(参数是xml或json格式的http接口)风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Restful的接口即可。
-
自治:自治是说服务间互相独立,互不干扰
-
团队独立:每个服务都是一个独立的开发团队,人数不能过多。
-
技术独立:因为是面向服务,提供Restful接口,使用什么技术没有别人干涉
-
数据库分离:每个服务都使用自己的数据源
-
部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
-
微服务架构图:
2、远程调用方式介绍
RPC&HTTP
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下2种:
-
RPC:Remote Produce Call远程过程调用,RPC基于Socket(套接字),工作在会话层,可自定义数据格式,早期的webservice,现在热门的dubbo,都是RPC的典型代表 。物理层-数据链路层(帧)-网络层(ip)-传输层(tcp、udp)-会话层(session)-表示层(编码解码,加密解密)-应用层(http)
-
缺点:服务的提供方和调用方必须采用同一种开发语言
-
优点:速度快,效率高。
-
-
Http:http其实是一种网络传输协议,基于TCP,工作在应用层,规定了数据传输的格式(例如请求头,响应头)。现在浏览器客户端与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。Rest风格请求就是使用http实现。
-
缺点:1、消息封装臃肿;2、速度相对于RPC慢一点
-
优势:对服务的提供方和调用方没有任何技术限定,自由灵活,更符合微服务理念。
-
3、RestTemplate远程调用
步骤:
第一步:编写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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>cn.itcast</groupId>
<artifactId>http-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
第二步:编写启动类:cn.it.DemoApplication.java
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args){
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
第三步:编写测试类:cn.it.RestTemplateTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestTemplateTest {
@Autowired
private RestTemplate restTemplate;
/** 发送get请求 */
@Test
public void testSendGet() {
String content = restTemplate.getForObject("https://www.jd.com",
String.class);
System.out.println("content = " + content);
}
/** 发送post请求 */
@Test
public void testSendPost() {
String content = restTemplate.postForObject("https://www.jd.com",
HttpEntity.EMPTY, String.class);
System.out.println("content = " + content);
}
}
get请求:restTemplate.getForObject("请求url", "响应数据类型");
post请求:restTemplate.postForObject("请求url","请求头|请求体","响应数据类型");
说明:如果响应数据为json字符串,响应数据类型可以直接用实体类接收,已经帮我们进行了反序列化
4、SpringCloud介绍
简介
SpringCloud是Spring旗下的项目之一,官网地址:
https://spring.io/projects/spring-cloud
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
Spring Cloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务注册与发现,智能路由,负载均衡,熔断器,消息总线,集群状态检测等功能;协调分布式环境中各个系统,为各类服务提供模板性配置。其主要涉及的组件包括:
-
Eureka:注册中心
-
Zuul、Gateway:服务网关
-
Ribbon:负载均衡
-
Feign:服务调用
-
Hystrix:熔断器
5、Eureka介绍
Eureka
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务管理的自动注册与发现、状态监控。
原理图
-
EurekaServer:就是服务注册中心(可以是一个集群),对外暴露自己的地址
-
提供者:启动后向EurekaServer注册自己的服务信息(ip、端口、微服务名)
-
消费者:向EurekaServer订阅服务,服务启动时会拉取一次服务列表,并且通过定时任务定期更新服务列表
-
心跳(续约):提供者定期通过发送http请求至Eureka的方式刷新自己的状态
注意:
1、Eureka分两个部分: EurekaServer服务端 + EurekaClient客户端(服务提供者或服务消费者)
2、服务注册、服务心跳续约、服务拉取等都是通过调用eurekaServer的http接口实现
6、Eureka服务端:注册中心
实现步骤
第一步:修改父工程springcloud-demo的pom文件,添加spring-cloud依赖配置,这点和加入springBoot父工程作用类似,此依赖中加入了许多依赖包的限定。
<properties>
<mapper.verion>2.1.5</mapper.verion>
<springcloud.version>Greenwich.SR2</springcloud.version>
<!--修改mysql版本,默认是8.X.X-->
<mysql.version>5.1.47</mysql.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- spring-cloud (导入pom文件)
scope: import 只能在<dependencyManagement>元素里面配置
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 通用mapper启动器 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mapper.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
第二步:配置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>springcloud-demo</artifactId>
<groupId>cn.it</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<!-- 配置eureka服务端启动器(集成了web启动器) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
第三步:编写启动类 : EurekaServerApplication
package cn.it;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer // 声明当前应用为eureka服务(启用eureka服务)
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args){
SpringApplication.run(EurekaServerApplication.class, args);
}
}
第四步:编写配置文件: application.yml,这里要注意一点是eureka-server本身也是一个客户端(与高可用有关,后续章节会讲到),所以也需要配置服务端的地址,目前服务端就是自己,因此配置自己的地址即可。
server:
port: 8761 # eureka服务端,默认端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果做集群,需要写其它服务实例(节点)的地址。
defaultZone: http://localhost:8761/eureka
fetch-registry: false # 不拉取服务
register-with-eureka: false # 不注册服务
第六步:启动服务,并访问:http://127.0.0.1:8761
7、Eureka客户端:服务注册
7.1 实现步骤
第一步:在user-service模块中添加eureka客户端启动器依赖
<!-- 配置eureka客户端启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
第二步:在启动类上开启Eureka客户端,添加 @EnableDiscoveryClient 来开启Eureka客户端
@SpringBootApplication
@MapperScan("cn.it.user.mapper")
@EnableDiscoveryClient // 开启Eureka客户端(2.1.x版本不加也行)
public class UserApplication {
public static void main(String[] args){
SpringApplication.run(UserApplication.class, args);
}
}
第三步:修改application.yml,添加eureka客户端配置
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud
username: root
password: root
application:
# 应用名称(服务id)
name: user-service
# 配置eureka服务端地址
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
注意:这里我们添加了spring.application.name属性来指定应用名称,将来会作为服务的id使用。
8、Eureka客户端:服务发现
实现步骤
-
第一步:在user-consumer模块中添加eureka客户端启动器依赖
<!-- 配置eureka客户端启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
第二步:在启动类上开启Eureka客户端,添加 @EnableDiscoveryClient 来开启Eureka客户端
package cn.it.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class ConsumerApplication {
public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
第三步:修改application.yml,添加eureka客户端配置
server:
port: 8080
spring:
application:
name: user-consumer # 应用名称
eureka:
client:
service-url: # eurekaServer地址
defaultZone: http://localhost:8761/eureka
第四步:修改Controller代码
package cn.it.consumer.controller;
import cn.it.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
/** 注入发现者 */
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
/** 根据主键id查询用户 */
@GetMapping("/{id}")
public User findOne(@PathVariable("id")Long id){
// 根据服务id获取该服务的全部服务实例
List<ServiceInstance> instances = discoveryClient
.getInstances("user-service");
// 获取第一个服务实例(因为目前我们只有一个服务实例)
ServiceInstance serviceInstance = instances.get(0);
// 获取服务实例所在的主机
String host = serviceInstance.getHost();
// 获取服务实例所在的端口
int port = serviceInstance.getPort();
// 定义服务实例访问URL
String url = "http://" + host + ":" + port + "/user/" + id;
System.out.println("服务实例访问URL: " + url);
return restTemplate.getForObject(url, User.class);
}
}
9、Eureka服务端:高可用
高可用介绍
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka注册中心。
服务同步
当存在多个Eureka Server节点时,每个节点都配置其他节点的地址,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的服务注册请求转发到集群中的其他节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
而作为客户端,需要把信息注册到每个Eureka中:
如果有三个EurekaServer节点,则每一个节点都需指定其他节点的地址,例如:有三个节点端口分别为8761、8762、8763,则:
-
8761节点的配置中指定8762和8763的地址
-
8762节点的配置中指定8761和8763的地址
-
8763节点的配置中指定8761和8762的地址
说明:
1、不仅仅是注册服务,心跳续约、服务下线等其他请求也会转发到其他节点。
2、若某服务A服务注册的时候,收到注册请求的节点(server1)转发请求到其他节点(server2)失败(例如网络波动),等到服务A进行心跳续约的时候,server1收到心跳续约请求,并转发到server2,server2若发现该服务在自己缓存中不存在,就会把该服务注册到自己。
3、若某服务A服务注册的时候,收到注册请求的节点(server1)转发请求到其他节点(server2),发现server2不可用,连接不上,此时server1内部有一个批处理流,会保存本次转发失败的请求,每隔1s钟重试一次,这样等server2节点恢复了,就可以收到该注册请求了,实现最终数据一致性。
实现步骤
-
第一步:修改eureka-server的配置(application.yml)
server:
port: ${port:8761} # eureka服务端,默认端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
client:
service-url: # Eureka服务地址;如果是集群则是其它服务地址,后面要加/eureka
defaultZone: ${defaultZone:http://localhost:8761/eureka}
fetch-registry: true # 拉取服务
register-with-eureka: true # 注册服务
说明:
1、在上述配置文件中的${}表示在jvm启动时候若能找到对应port或者defaultZone参数则使用传入的参数,若无则使用冒号后面的默认值。
2、把service-url的值改成了其他EurekaServer的地址,而不是自己。
2、fetch-registry和register-with-eureka最好是都设置为true,这样server在启动的时候,会去service-url中配置的其他节点
中拉取已有的服务列表
第二步:每一台在启动的时候指定端口port和defaultZone配置
第三步:依次启动8761、8762节点,浏览器访问8761或8762:
第四步:eureka客户端修改配置,由于此时EurekaServer节点不止一个,因此user-service注册服务或者user-consumer获取服务的时候,service-url参数需要变化,这样即使某个节点不可用了,也可以使用其他的节点,实现高可用。
# 配置eureka
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
10、Eureka客户端:服务提供者的其他配置
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-eureka=true 参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个restful风格的http请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。
-
第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性
-
第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:localhost:user-service:9001
值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。
Map<String, Map<String, instance>> allMap;
Map<String, instance> instanceMap;
instanceMap.put("localhost:userService:9001", instance);
instanceMap.put("localhost:userService:9002", instance);
allMap.put("userService", instanceMap);
服务注册时默认使用的是主机名,如果想用ip进行注册,可以在user-service中添加配置:
# 配置eureka
eureka:
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名称
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew)
有两个重要参数可以修改服务续约的行为:
# 配置eureka
eureka:
instance:
lease-renewal-interval-in-seconds: 30 # 服务续约(renew)的间隔时间,默认为30秒
lease-expiration-duration-in-seconds: 90 # 服务失效时间,默认值90秒
-
lease-renewal-interval-in-seconds:服务续约(renew)的间隔时间,默认为30秒
-
lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中剔除,这两个值在生产环境不要修改,默认即可。
获取服务列表
当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka Server服务的列表拉取下来,然后缓存在本地。并且 每隔30秒 会重新获取并更新数据。可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 30 # 获取服务间隔时间(默认30秒)
11、Eureka服务端:失效剔除及自我保护
服务下线
当手动发送服务下线的REST请求给Eureka Server或debug模式下关闭服务,告诉服务注册中心:“我要下线了”。EurekaServer 接受到请求之后,将该服务置为下线状态,也就是从服务列表中剔除。
失效剔除
当服务由于内存溢出等原因变得不可用,亦或是正常关闭服务,此时服务注册中心并未收到“服务续约”的请求。服务注册中心在启动时会创建一个定时任务,每隔一段时间(默认为60秒)将当前服务列表中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。
可以通过以下参数对其进行修改,单位是毫秒。 某个服务实例宕机了,最长多久检测到并且剔除 = 60 + 90
eureka.server.eviction-interval-timer-in-ms: 60000 # eureka-server 服务剔除定时任务执行周期(毫秒)
测试失效剔除步骤:
-
修改eureka-server配置:
eureka:
client:
service-url:
defaultZone : ${defaultZone:http://localhost:8761/eureka/}
fetch-registry: true
register-with-eureka: trueserver:
eviction-interval-timer-in-ms: 4000 # 设置4s执行一次检测定时任务
enable-self-preservation: false # 关闭自我保护机制
修改user-service配置:
eureka:
client:
service-url:
#配置eureka server 服务地址
defaultZone: http://localhost:8761/eureka/
fetch-registry: false # 拉取服务
register-with-eureka: true # 注册服务instance:
prefer-ip-address: true # 指定更偏向用ip
ip-address: 127.0.0.1
lease-renewal-interval-in-seconds: 5 # 5s发送一次心跳续约
lease-expiration-duration-in-seconds: 15 # 15s未发送心跳续约就失效
自我保护
在生产环境下,因为网络延迟等原因,EurekaServer未收到的心跳续约数量非常多,超标了,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。因此会开启自我保护机制,EurekaServer在这段时间内不会剔除任何服务实例(否则服务其实是好的,岂不是误杀了),直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好容错处理。
自我保护检查周期:默认每分钟检查一次
计算公式:最后一分钟实际受到的客户端实例心跳续约数(Renews ) < (每分钟应该受到心跳续约的总数 * 85%) = Renews threshold
-
Renews threshold
:Eureka Server 期望每分钟收到客户端实例续约的总数 *85%。 -
Renews (last min)
:Eureka Server 最后 1 分钟实际收到客户端实例续约的总数。 -
若启动两个实例,通过计算可得出:
每分钟应发心跳续约总数:2 * 60/30 = 4 (2为服务实例个数、一分钟60s、每个实例每30s发送一次心跳续约)
可得出:阈值 = 4 * 85 % = 3.4,结果会取4 ,因此,如果上一分钟收到的心跳续约数< 4 便于触发自我保护
12、Ribbon负载均衡
第一步:启动两个服务实例(user-service),一个9001,一个9002。
修改user-service的配置文件如下:
修改运行配置:
复制一份配置(9002的配置)
分别启动两个实例
第二步:开启负载均衡(user-consumer)
因为eureka-client启动器中已经传递依赖了Ribbon,所以我们无需引入新的依赖,直接修改代码:
在RestTemplate的配置方法上添加 @LoadBalanced 注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
第三步:修改ConsumerController调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
/** 根据主键id查询用户 */
@GetMapping("/{id}")
public String findOne(@PathVariable("id")Long id){
/*// 根据服务id获取该服务的全部服务实例
List<ServiceInstance> instances = discoveryClient
.getInstances("user-service");
// 获取第一个服务实例(因为目前我们只有一个服务实例)
ServiceInstance serviceInstance = instances.get(0);
// 获取服务实例所在的主机
String host = serviceInstance.getHost();
// 获取服务实例所在的端口
int port = serviceInstance.getPort();
// 定义服务实例访问URL
String url = "http://" + host + ":" + port + "/user/" + id;*/
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
第四步:访问user-consumer,发现可以正常访问,并可以在9001和9002的控制台查看日志输出情况,你会发现 9001与9002 轮询访问
了解:Ribbon默认的负载均衡策略是轮询(com.netflix.loadbalancer.RoundRobinRule)
若要修改负载均衡策略,可以在user-consumer的配置文件中添加如下配置,便切换为随机策略
# 格式:{服务名称}.ribbon.NFLoadBalancerRuleClassName
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
13、Hystrix熔断器
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
-
作用:防止雪崩
什么是雪崩问题?
一个微服务中,可能会对外提供多个HTTP接口,以下每个Dependency当成一个HTTP接口:
如果此时,某个HTTP接口出现异常(调用超时)假如HTTP接口 I 发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞。
服务器支持的线程数是有限的(tomcat默认200个线程),若请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它HTTP接口都不可用,并且假如有其他的微服务需要调用这个接口,岂不是也跟着阻塞,也拖累了其他的微服务所在服务器也资源耗尽?这就叫做雪崩效应。
这就好比银行的柜台窗口,假如现在有10个窗口,每个窗口理解成一个线程,每个窗口都可以办理各种业务(例如存钱、取钱、贷款)。这个时候1个客户需要办理贷款业务,耗费时间很久,一直占用一个窗口。后面又来了9个客户也做贷款,也各自占用了一个窗口,那10个窗口岂不是都不能处理其他业务了?
Hystrix解决雪崩问题的手段主要是服务降级,它包括两种方式:
-
线程隔离
-
服务熔断(断路器)
Hystrix熔断器:线程隔离原理
-
Hystrix为每个HTTP接口调用分配一个小的线程池(默认10个线程),用户的请求将不再直接访问接口,而是通过线程池中的空闲线程来访问接口,如果线程池已满,或者请求超时,或请求接口异常,则会进行降级处理,什么是服务降级?
服务降级:保证服务弱可用。
-
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)。
-
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有影响。
-
触发Hystrix服务降级的条件
-
请求报错
-
线程池已满
-
请求超时
-
14、Hystrix熔断器:动手实践线程隔离
第一步:引入依赖,在user-consumer消费端系统的pom.xml文件添加如下依赖:
<!-- 配置hystrix启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
第二步:开启熔断器,在启动类上添加注解:@EnableCircuitBreaker
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
@EnableCircuitBreaker // 开启熔断器
public class ConsumerApplication {
// ......
}
//或者
@SpringCloudApplication
public class ConsumerApplication {
// ......
}
第三步:编写降级逻辑,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用@HystrixCommond注解来完成,改造ConsumerController:
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
/** 注入发现者 */
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
/** 根据主键id查询用户 */
@GetMapping("/{id}")
@HystrixCommand(fallbackMethod = "findOneFallback")
public String findOne(@PathVariable("id")Long id){
/*// 根据服务id获取该服务的全部服务实例
List<ServiceInstance> instances = discoveryClient
.getInstances("user-service");
// 获取第一个服务实例(因为目前我们只有一个服务实例)
ServiceInstance serviceInstance = instances.get(0);
// 获取服务实例所在的主机
String host = serviceInstance.getHost();
// 获取服务实例所在的端口
int port = serviceInstance.getPort();
// 定义服务实例访问URL
String url = "http://" + host + ":" + port + "/user/" + id;*/
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
public String findOneFallback(Long id){
log.error("查询用户信息失败。id:{}", id);
return "对不起,网络太拥挤了!";
}
}
要注意,因为熔断的降级方法必须跟原方法保证相同的参数列表和返回值声明。而失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把findOne的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
说明:
1、@HystrixCommand(fallbackMethod="findOneFallBack"):用来声明一个降级逻辑的方法
2、@HystrixCommand默认分配了10个线程,可以修改:
@HystrixCommand(threadPoolProperties = { @HystrixProperty(name = "coreSize", value = "3") // 线程池核心线程数大小,默认10个 })
第四步:测试降级逻辑,当user-service正常提供服务时,访问与以前一致,但是当将user-service关掉时,会发现页面返回了降级处理信息:
第五步:使用默认fallback方法:刚才是为一个接口指定降级方法,如果接口很多,那岂不是要写很多降级方法?因此可以在整个类上加一个注解,指定一个默认fallback方法
package cn.it.consumer.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/consumer")
@Slf4j
// 该类中所有方法返回类型要与该方法的返回类型一致,且必须采用String作为返回值
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
/** 注入发现者 */
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
/** 根据主键id查询用户 */
@GetMapping("/{id}")
@HystrixCommand
public String findOne(@PathVariable("id")Long id){
/*// 根据服务id获取该服务的全部服务实例
List<ServiceInstance> instances = discoveryClient
.getInstances("user-service");
// 获取第一个服务实例(因为目前我们只有一个服务实例)
ServiceInstance serviceInstance = instances.get(0);
// 获取服务实例所在的主机
String host = serviceInstance.getHost();
// 获取服务实例所在的端口
int port = serviceInstance.getPort();
// 定义服务实例访问URL
String url = "http://" + host + ":" + port + "/user/" + id;*/
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
public String findOneFallback(Long id){
log.error("查询用户信息失败。id:{}", id);
return "对不起,网络太拥挤了!";
}
public String defaultFallback(){
return "默认提示:对不起,网络太拥挤了!";
}
}
第七步:超时配置,在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystrix的默认超时时长为1秒,我们可以通过配置修改这个值(项目中一般使用它的默认值1秒),在user-consumer的yml文件中添加超时配置:
# 线程隔离
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
15、Hystrix熔断器:服务熔断原理
状态机有3个状态:
-
Closed:关闭状态(断路器关闭),访问该请求都正常访问。
-
Open:打开状态(断路器打开),访问该请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求达到阈值,则触发熔断,断路器会打开。默认失败比例的阈值是:请求失败比例超过50% 或 请求失败次数超过20次。 这个时候访问这个接口全部直接返回降级信息。
-
Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S),5S后断路器会自动进入半开状态。
-
此时再访问一次请求,若这个请求是正常的,则会关闭断路器,变成关闭状态.
-
否则重新变成打开状态,再次进行5秒休眠计时。
-
实现步骤
第一步:为了能够精确控制请求的成功或失败,在user-consumer的调用业务中加入一段逻辑
/** 根据主键id查询用户 */
@GetMapping("/{id}")
@HystrixCommand
public String findOne(@PathVariable("id")Long id){
if (id == 1){
throw new RuntimeException("太忙了!");
}
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
说明:这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了注释user-service中的休眠逻辑),执行流程如下:
1、当访问:http://localhost:8080/consumer/1,肯定失败,返回降级逻辑。
2、当访问:http://localhost:8080/consumer/2,肯定成功。
3、当我们疯狂访问id为1的请求时(超过20次),就会触发熔断。断路器会打开,一切请求都会被降级处理。
4、此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有5秒左右。
5、5秒后进入半开状态之后,若再访问id为2的请求是可以的。
配置服务熔断参数(user-consumer)
/** 根据主键id查询用户 */
@GetMapping("/{id}")
@HystrixCommand(commandProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="20000"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="50")
})
public String findOne(@PathVariable("id")Long id){
if (id == 1){
throw new RuntimeException("太忙了!");
}
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
第三步:访问user-consumer测试
-
请求 http://localhost:8080/consumer/2 1次(失败),必须等到20秒后才能成功。