分布式系统中,会出现哪些问题?
分布式系统中一定会遇到的一个问题:服务雪崩效应 或者叫级联效应
那么什么是服务雪崩效应呢?
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:
调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:
服务雪崩效应:因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,就叫服务雪崩效应讲了什么是服务雪崩效应
那么为什么服务不可用?
导致服务不可用的原因可能有几点: 程序Bug,大流量请求,硬件故障,缓存击穿
- 【大流量请求】:在秒杀和大促开始前,如果准备不充分,瞬间大量请求会造成服务提供者的不可用.
- 【硬件故障】:可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.
- 【缓存击穿】:一般发生在缓存应用重启, 缓存失效时高并发, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.而且,在服务提供者不可用的时候,会出现重试的情况:用户重试、代码逻辑重试
- 用户重试:在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,会不断刷新页面甚至提交表单.
- 代码逻辑重试:服务调用端的会存在大量服务异常后的重试逻辑.
- 这些重试最终导致:进一步加大请求流量.
那么,归根结底导致雪崩效应的最根本原因是:
- 大量请求线程同步等待造成的资源耗尽
- 当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,
- 服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
那么知道了分布式系统中的服务雪崩效应,以及产生的原因。那么,问题来了,怎么解决?
解决方案
那么解决方案有很多:如
1. 超时机制
2. 服务限流
3. 服务熔断
4. 服务降级
超时机制
服务级联失败(服务雪崩效应)的最根本原因是:大量请求线程同步等待造成的资源耗尽那么,在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽,而且,既然服务提供者已经不可用了,还在作死的请求的话,是毫无意的那么,如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样是不是一定程度上可以抑制消费者资源耗尽的问题。
服务限流(资源隔离)
也就是限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题,对于一些出问的服务可以限制流量访问,只分配固定线程资源访问,这样能使整体的资源不至于被出问题的服务耗尽,进而整个系统雪崩那么服务之间怎么限流,怎么资源隔离了?例如通过线程池+队列的方式,通过信号量的方式。如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
服务熔断
远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断。举个通俗易懂的例子,就跟我们现实生活中的“跳闸”一样,比如说家里有点短路了,那是不是闸会跳掉,等你把短路的问题找到并且修复后,然后你把这个闸一送,是不是整个家庭的电路又恢复了正常。这就是熔断
器。
所以,同样的道理,当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源。比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个依赖了,这个时候就应该使用熔断器避免资源浪费。
服务降级
有服务熔断,必然要有服务降级。所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据)这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。
实战Hystrix-降级,超时
在整个SpringCloud构建微服务的体系中,有一个提供超时机制,限流,熔断,降级最全面
的实现:Hystrix(豪猪)
1、引入Springcloud Hystrix依赖, 那么在哪里引入呢?
一定是在调用方来做降级,所以需要在消费者这边引入Hystrix,也就是我们的订单微服务方
2.在微服务的启动上添加@EnableHystrix的注解
3.用Hystrix的注解@HystrixCommand可以更简单的实现上面的降级逻辑直接在接口调用方的方法上增加注解@HystrixCommand(fallbackMethod ="findByIdFallback")
4、超时回退怎么实现?
在用户微服务工程(ms-provider-person)里将PersonController的getPersonById接口增加执行等待时间,让该接口的执行时间变长,Hystrix调用接口默认两秒超时,超时后会自动执行降级方法.
实战Hystrix-熔断,限流
熔断怎么实现?
首先在用户微服务工程(ms-provider-person)里将PersonController的getPersonById接口增加模拟报错代码
测试报错和正常的情况,我们可以看到当报错达到一定阈值时,会自动熔断,阈值可以配置,如下:
hystrix.command.default.circuitBreaker.requestVolumeThreshold:
一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds:
触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内都会拒绝request,也就是5000毫秒后才会关闭circuit。默认5000
熔断器原理
熔断器模式定义了熔断器开关相互转换的逻辑:
服务的健康状况 = 请求失败数 / 请求总数.
熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
1、当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
2、当熔断器开关打开时, 请求被禁止通过.
3、当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待.并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
限流,线程资源隔离怎么实现?
首先在用户微服务工程(ms-provider-person)里将PersonController的getPersonById接口等待的代码
在订单微服务工程中修改接口超时配置为20秒:
然后用注解配置线程池大小:
部分注解意思如下:
- CommandGroupKey:配置全局唯一标识服务分组的名称,比如,库存系统就是一个服务分组。当我们监控时,相同分组的服务会聚合在一起,必填选项。
- CommandKey:配置全局唯一标识服务的名称,比如,库存系统有一个获取库存服务,那么就可以为这个服务起一个名字来唯一识别该服务,如果不配置,则默认是简单类名。
- ThreadPoolKey:配置全局唯一标识线程池的名称,相同线程池名称的线程池是同一个,如果不配置,则默认是分组名,此名字也是线程池中线程名字的前缀。
- ThreadPoolProperties:配置线程池参数,coreSize配置核心线程池大小和线程池最大大小,keepAliveTimeMinutes是线程池中空闲线程生存时间(如果不进行动态配置,那么是没有任何作用的),maxQueueSize配置线程池队列最大大小,
- queueSizeRejectionThreshold限定当前队列大小,即实际队列大小由这个参数决定,通过改变queueSizeRejectionThreshold可以实现动态队列大小调整。
- CommandProperties:配置该命令的一些参数,如executionIsolationStrategy配置执行隔离策略,默认是使用线程隔离,此处我们配置为THREAD,即线程池隔离。
此处可以粗粒度实现隔离,也可以细粒度实现隔离,如下所示。
- 服务分组+线程池:粗粒度实现,一个服务分组/系统配置一个隔离线程池即可,不配置线程池名称或者相同分组的线程池名称配置为一样。
- 服务分组+服务+线程池:细粒度实现,一个服务分组中的每一个服务配置一个隔离线程池,为不同的命令实现配置不同的线程池名称即可。
- 混合实现:一个服务分组配置一个隔离线程池,然后对重要服务单独设置隔离线程池。本demo可以用Jmeter模拟多线程调用来验证结果
- 那么,这个是Hystrix基于线程池+队列的方式实现的限流,当然,还有另外一种,基于信号量来实现的。那么什么是信号量把,说白了就是计数器。当 计数器 + 1 > 设置的最大并发数 时,就限流,或者走降级渠道
Hystrix服务调用的内部逻辑
下图为Hystrix服务调用的内部逻辑:
1.构建Hystrix的Command对象, 调用执行方法.
2.Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.
3.若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若线程池已满, 则执行降级服务getFallback方法.
4.若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.
5.若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
6.若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
7.若服务执行成功, 返回正常结果.
8.若服务降级方法getFallback执行成功, 则返回降级结果.
9.若服务降级方法getFallback执行失败, 则抛出异常.
Hystrix Metrics的实现
Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑.
Feign整合Hystrix
Feign是以接口形式工作的,要如何整合Hystrix了?又是如何实现降级了?
事实上,SpringCloud默认已为Feign整合了Hystrix,只要Hystrix在项目的classpath中,Feign默认就会用断路器包裹所有方法。(注意:从Spring Cloud Dalston开始,Feign默认是不开启Hystrix的。
因此,如使用Dalston及以上版本请务必额外设置属性:feign.hystrix.enabled=true,否则断路器不会生效)
首先要添加Feign的依赖
Feign整合Hystrix的写法见<PersonFeignClient>类,只需使用@FeignClient注解的fallback属性就可以为指定名称Feign客户端添加降级方法
package com.cym.micreoserviceorder.feign;
import com.cym.micreoserviceorder.model.Person;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Feign的fallback的测试
* 使用@FeignClient的fallback的属性指定回退类
*/
@FeignClient(name = "microservice-provider-user",fallback = PersonFeignClient.FeignClientFallBack.class)
public interface PersonFeignClient {
@RequestMapping(value = "/person/getPersonById/{id}",method = RequestMethod.GET)
public Person getPersonById(@PathVariable("id") Integer id);
/**
*@component把FeignClientFallBack实例化到spring容器中
* 回退类FeignClientFallBack需要实现PersonFeignClient接口
*/
@Component
class FeignClientFallBack implements PersonFeignClient{
@Override
public Person getPersonById(Integer id) {
Person person = new Person();
person.setPersonId(-1);
person.setName("降级用户");
return person;
}
}
}
order的Controller
package com.cym.micreoserviceorder.controller;
import com.cym.micreoserviceorder.feign.PersonFeignClient;
import com.cym.micreoserviceorder.model.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private PersonFeignClient personFeignClient;
@RequestMapping("/getPersonById/{id}")
public Person getPersonById(@PathVariable("id") Integer id){
logger.info("==========用户请求中心=============");
return personFeignClient.getPersonById(id);
}
}
启动类
测试
先启动Eureka的服务类
然后启动服务提供者Persson类
最后启动Order类
我们先看看正常情况下是否能正常order的微服务访问person的微服务
然后我们把服务提供者的微服务关了看看。Hystrix的降级是否起作用
Feign禁用Hystrix
SpringCloud为Feign默认整合了Hystrix,也就是说只要Hystrix在项目的classpath中,Feign就会使用断路器包裹Feign客户端的所有方法(Dalston及以上版本默认Feign不开启Hystrix)。这样虽然方便,但有的场景并不需要该功能,如何为Feign禁用Hystrix呢?
全局禁用Hystrix
只需在application.yml中配置feign.hystrix.enabled=false即可
为指定Feign客户端禁用Hystrix:
增加< FeignDisableHystrixConfiguration >类
package com.cym.micreoserviceorder.feign;
import feign.Feign;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* 禁用Hystrix的功能
*/
@Configuration
public class FeignDisableHystrixConfiguration {
@Scope("prototype")
@Bean
public Feign.Builder feignBuider(){
return Feign.builder();
}
}
然后在FeignClient注解里加上configuration的属性配置,见下图:
测试: