Hystricx之服务雪崩服务降级:
写在前面:
- 以下观点均为本仙鱼学习过程中的笔记感悟,若有不足之处请在留言中给出。
- 本篇博客内容为微服务服务雪崩和服务降级
什么是服务雪崩?
话不多说先上图:
- 上图是一条服务器调用链,正常就是A->B->C->D,假设服务D突然被雷劈中了,服务C通过3发送请求调用D时发现服务D崩了,但不知道具体原因也没有处理手段,请求一直堆积在服务C造成阻塞,消耗服务C的线程资源,慢慢的服务B也会阻塞,A也是,总而言之,
当服务中的一个服务崩溃导致整条服务链崩溃的状况,我们就称之为服务雪崩
- 你可能会说在分布式微服务架构中,服务D保障是集群就可避免上面的问题,但不能保障是服务C在请求服务D时是没有问题的,在过程中突然崩掉了…
如何解决:
-
当我们的consumer服务(如上面的服务C)去调用provider服务(如上面的服务D),provider服务出现问题或者访问provider服务的流量过高时,
我们为了保障一个良好的用户体验
,不能将异常数据直接返回给客户,我们要返回一些符合规范的数据,即为服务降级
。 -
我们这里说的服务降级只是解决服务雪崩的手段之一。
-
服务降级分为两类:
限流降级
:举个例子,就像早高峰坐地铁,是不是要排队走通道,当你赶到地铁站远远地看见人海排队很长时(人流量超多),那你有两个选择,一是排队慢慢向前移动,二是放弃地铁选择打滴去上班。即:不允许一次通过很大流量必须去排队为限流降级。熔断降级
:再举个生动的例子,假设你是你们公司前台小姐姐,有人来你们公司找你们老板,第一次你先打电话问一下老板在不在,然后告诉你老板不在,第二次再确认一下老板在不在…当第十次时你可能就烦了,心态爆炸,不去问老板在不在,直接回复”老板不在!“,后面还有人来找老板,根据心情,有时你会打电话看一下老板在不在,有时候直接回复:老板不在!那么:多次请求同一个出问题地服务,一开始(在默认次数内)还请求一下确定服务是不是可以调用,后面就直接认定是出问题了,不再调用服务,直接回复,再往后当再有请求隔一段时间确定一下是否可调用地方式我们称之为熔断降级。
Hystrix应用:
- 下面就开始码代码看代码实现来理解熔断了:
- 还有一个词语:
Circuit Breaker
:断路器了解一下,就像家中的电闸,当使用的电器过多 负载过大时会跳闸,这里我们就可以将Circuit Breaker理解为一个保护的something。
1 在Ribbon-Hystrix中的使用
1.1 consumer-ribbon-hystrix
代码示例:
-
导入依赖:groupId:
org.springframework.cloud
,artifactId:spring-cloud-starter-netflix-hystrix
-
SpringBoot启动文件上加上开启断路器的注解:
@EnableCircuitBreaker
这时:当我们的调用出现问题或者是流量过高的时候就会触发降级
@SpringBootApplication @EnableCircuitBreaker public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } }
-
在要调用的方法上加上注解:
@HystrixCommand(fallbackMethod = "newMethod")
fallbackMethod
表示当要调用的oldMethod()
方法出现问题时去调用newMethod()
方法;newMethod()
与oldMethod()
除了方法名和方法内的处理方式不一样,参数和返回值都是一样的,即我们上面所说的符合规范的数据;@Component public class Test { @HystrixCommand(fallbackMethod = "newMethod") public Object oldMethod(Map<String, Object> parameters) { //do stuff that might fail } public Object newMethod(Map<String, Object> parameters) { return /* something useful */; } }
那代码示例的实现原理是什么呢?
-
上面的
oldMethod()
方法中,我们加上了@HystrixCommand
注解,实际上在扫描到这个注解的时候会通过AOP对oldMethod()
方法进行包裹, 当这个方法发生异常时,会在catch中执行newMethod()
方法,返回符合规范的数据; -
官方文档给出的是继承
HystrixCommand<R>
抽象类,其中上面的oldMethod()
被包装在@Override
重写的run()
方法中,R
为泛型,与oldMethod()
方法的返回值类型一致; -
还有一点,这个默认是走
Thread
的,后面会补充,还有一种方式是走Semaphone
代码如下:
public class CommandHelloWorld extends HystrixCommand<Object> { protected CommandHelloWorld(HystrixCommandGroupKey group) { super(group); } protected CommandHelloWorld(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) { super(group, threadPool); } protected CommandHelloWorld(HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) { super(group, executionIsolationThreadTimeoutInMilliseconds); } protected CommandHelloWorld(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) { super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds); } protected CommandHelloWorld(Setter setter) { super(setter); } /** * 用于包装被@HystrixCommand注解修饰的方法 * @return * @throws Exception */ @Override protected Object run() throws Exception { return null; } }
-
测试调用方式如下:
//execute()相当于我们线程中的start(),调用时会触发run()方法 Object s = new CommandHelloWorld("Bob").execute(); //queue()方法是阻塞的 Future<Object> s = new CommandHelloWorld("Bob").queue(); //observe()相当于又开了一个线程进入阻塞状态,而主线程是不阻塞的 Observable<Object> s = new CommandHelloWorld("Bob").observe();
2 Hystrix
提供的两种降级的机制:
Thread
:在单独的线程(即自己新开的线程)上在执行,并发请求受线程池中线程数的限制,Thread降级是默认的降级机制Semaphone
:在调用的线程(即用户请求的线程)上执行,并发请求受信号量计数的限制- 原理图如下(下图包含了官方给出的默认数量):
2.1 Thread
应用:
-
代码演示同1.1(默认就是Thread)
-
由上图可知官方文档给出的默认的
Threadpool Size:10
即线程数为10个,那么不禁疑问,才10个线程,够用的吗?
-
答
:在大多数情况下,10个线程的默认值是很好的(通常可以减小)。 -
请看下图:一个线程在没有异常的情况下
30ms
可以发送一个请求,那1s
就可以处理300
个请求,10
个线程1s就是3000
个请求;当有问题的时候一个线程300ms
发送一个请求,1s就是3
个,10个线程1s就是30
个。这仅仅是1s而已,所以官方给的10个线程是绝对够用的。 -
而且从下图可以看出Thread是可以设置超时时间的(默认是1000ms),Semaphone只能被动等,等到所请求的provider超时,可以在
@HystrixCommand
注解中添加属性commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1")}
来设置(这里设置的是1ms,哈哈,你的请求能成功才怪~) -
默认是连续请求同一个请求失败
20
次(这20次走的是Thread新开的线程)后,开启熔断降级
,即:走的是用户请求的线程,当再次请求多次时,Thread新开的线程偶尔会去发起一次请求看看请求是否可用
2.2 Semaphone应用:
-
与前面的1.1的代码相比,只需要在
@HystrixCommand
注解中加上属性commandProperties = {@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHONE")}
即可,其他的没有变化,代码如下:@Component public class Test { @HystrixCommand(fallbackMethod = "newMethod",commandProperties = {@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHONE")}) public Object oldMethod(Map<String, Object> parameters) { //do stuff that might fail } public Object newMethod(Map<String, Object> parameters) { return /* something useful */; } }
3 Eureka-Feign-Hystrix应用
-
与在Ribbon中不同的是,在Feign中我们提供的都是服务的接口,我们总不能在接口上打注解吧,所以,有另外一种思路,当服务接口中的方法出现异常时,我们只要调用这个方法默认的实现方法就可以了,所以我们需要一个类去实现被调用的服务接口,假设我们有一个名为
UserFeign
的服务接口,代码如下:@FeignClient(value = "USERSERVICE") public interface UserFeign { @RequestMapping("/users/{id}") User getUserInfo(@PathVariable("id") int id); @GetMapping("/save") String save(User User); }
要对这个服务接口实现熔断降级,有下面的两种方式:
3.1 方式一:直接实现UserFeign
接口
-
application.yml
文件中配置开启断路器:feign: hystrix: enabled: true #必须开启这个,否无断路器无效
-
导入依赖:groupId:
org.springframework.cloud
,artifactId:spring-cloud-starter-netflix-hystrix
-
SpringBoot的启动文件加上
@EnableCircuitBreaker
注解@SpringBootApplication @EnableCircuitBreaker @EnableFeignClients public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } }
-
定义
UserFeignFallBack
类实现UserFeign
接口(1)
UserFeign
接口上指定实现它的类(哪个类实现它就写哪个类):即@FeignClient
注解中加上:fallback=UserFeignFallBack.class
属性//失败后执行UserFeignFallBack.java中对应的实现方法 @FeignClient(value = "USERSERVICE",fallback = UserFeignFallback.class) public interface UserFeign { @RequestMapping("/users/{id}") User getUserInfo(@PathVariable("id") int id); @GetMapping("/save") String save(User User); }
(2)定义
UserFeignFallBack
类实现UserFeign
接口@Component public class UserFeignFallback implements UserFeign { @Override public User getUserInfo(int id) { /***/ return ....; } @Override public String save(User user) { return "....."; } }
3.2 方式二:实现FallbackFactory<R>
接口
-
application.yml
文件中配置开启断路器:feign: hystrix: enabled: true #必须开启这个,否无断路器无效
-
导入依赖:groupId:
org.springframework.cloud
,artifactId:spring-cloud-starter-netflix-hystrix
-
SpringBoot的启动文件加上
@EnableCircuitBreaker
注解@SpringBootApplication @EnableCircuitBreaker @EnableFeignClients public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } }
-
定义
UserFeignFallbackFactory
类实现FallbackFactory<R>
接口(1)
UserFeign
接口上指定实现它的类(哪个类实现它就写哪个类):即@FeignClient
注解中加上:fallbackFactory=UserFeignFallbackFactory.class
属性//失败后执行UserFeignFallBack.java中对应的实现方法 @FeignClient(value = "USERSERVICE",fallbackFactory = UserFeignFallbackFactory.class) public interface UserFeign { @RequestMapping("/users/{id}") User getUserInfo(@PathVariable("id") int id); @GetMapping("/save") String save(User User); }
(2)
R
为泛型,要实现哪个服务就口,就写哪个类,这里问哦们实现的是UserFeign
,如下:@Component public class UserFeignFallbackFactory implements FallbackFactory<UserFeign> { @Override public UserFeign create(Throwable throwable) { return new UserFeign() { @Override public User getUserInfo(int id) { /***/ return ....; } @Override public String save(User user) { return "....."; } }; } }
如果需要
访问导致回退触发器的原因
,那么使用第二种方式。
4 Gateway-Zuul:
- 在网关中,当我们给定的路由(Service)的电路跳闸时,我们可以通过创建
FallbackProvider
类型的类(实现FallbackProvider
接口)来提供回退的响应,我们需要指定回退的路由ID(即指定的Service),并提供一个ClientHttpResponse
对象作为回退响应:
-
示例代码如下:
@Component class MyFallbackProvider implements FallbackProvider { /** * 设置当前是给哪个服务开启fallback,返回值就是服务的名字,如果是给所有的,返回* * @return */ @Override public String getRoute() { return "*"; } /** * 当出现问题的时候返回给调用者具体的返回内容 * @param route 返回给哪个service * @param cause 出现的异常 * @return */ @Override public ClientHttpResponse fallbackResponse(String route, final Throwable cause) { if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { return response(HttpStatus.INTERNAL_SERVER_ERROR); } } private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return status; } @Override public int getRawStatusCode() throws IOException { return status.value(); } @Override public String getStatusText() throws IOException { return status.getReasonPhrase(); } @Override public void close() { } /** * 响应正文 * @return * @throws IOException */ @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("恭喜你,你电脑中毒了,搬出去拆开晒晒太阳杀杀毒".getBytes()); } /** * 响应头 * @return * @throws IOException */ @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } } * 响应头 * @return * @throws IOException */ @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }