之前几篇文章都是介绍了服务的搭建,注册,获取以及服务的负载均衡,今天来学习一个知识点就是如何保证服务?这个并不是springcloud特有的,因为它有完全独立的组件来实现,所以就算我们的项目不是springcloud也完全可以使用,所以熟悉它是很有必要的,然而springboot对它做了更简单的封装,比单独使用它简便的多...,好了,那么久开启今天的学习之路.........
Hystirx讲解
分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的做法是手动服务降级. 而Hystrix的出现,给我们提供了另一种选择.
服务雪崩效应的定义
服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程.如果所示:
上图中, A为服务提供者, B为A的服务调用者, C和D是B的服务调用者. 当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了.
服务雪崩效应形成的原因
我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:
服务提供者不可用
重试加大流量
服务调用者不可用
服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:
硬件故障
程序Bug
缓存击穿
用户大量请求
硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问.
缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用.
在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.
而形成 重试加大流量 的原因有:
用户重试
代码逻辑重试
在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑.
这些重试都会进一步加大请求流量.
最后, 服务调用者不可用 产生的主要原因是:
同步等待造成的资源耗尽
当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
服务雪崩的应对策略
针对造成服务雪崩的不同原因, 可以使用不同的应对策略:
流量控制
改进缓存模式
服务自动扩容
服务调用者降级服务
流量控制 的具体措施包括:
网关限流
用户交互限流
关闭重试
因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门.
用户交互限流的具体措施有: 1. 采用加载动画,提高用户的忍耐等待时间. 2. 提交按钮添加强制等待时间机制.
改进缓存模式 的措施包括:
缓存预加载
同步改为异步刷新
服务自动扩容 的措施主要有:
AWS的auto scaling
服务调用者降级服务 的措施包括:
资源隔离
对依赖服务进行分类
不可用服务的调用快速失败
资源隔离主要是对调用服务的线程池进行隔离.
我们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.
不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现.
Hystrix预防服务雪崩
Hystrix [hɪst'rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.
Hystrix的设计原则包括:
资源隔离
熔断器
命令模式
资源隔离
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:
商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:
调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:
Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩.
如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
熔断器模式
熔断器模式定义了熔断器开关相互转换的逻辑:
服务的健康状况 = 请求失败数 / 请求总数.
熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
当熔断器开关打开时, 请求被禁止通过.
当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.
熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
上面的知识点是对于hystirx一个理论讲解(部分来源于网络截图,如果有版权问题,请联系我,第一时间给删除掉),OK 接下来介绍如何在springcloud中使用它?
SpringCloud中使用Hystirx
Hystrix在springcloud主要是和ribbon或者fegin结合起来使用,所以接下来在这个中如何使用hystrix,都会讲解到,简单模拟一个业务场景,比如业务当中我们会经常遇到一个服务调用另一个服务,而当被调用的服务挂了,就会报错,在此我们希望如果发生此种情况,可以调用降级方法,返回默认值,而hystrix就非常适合做这样的事情,下面就分别通过ribbon和fegin来演示。
& Hystrix+Ribbon
为了方便演示,我们在之前的项目中新建一个父模块叫server_hystrix,然后在这个模块新建一个子模块叫server_hystrix_ribbon,
整体项目目前结构如下:
看一下server_ribbon_hystirx模块结构:
看一下项目的额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>server_hystrix</artifactId> <groupId>com.suning.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>server_ribbon_hystrix</artifactId> <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.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> </dependencies> </project>
红色标注的就是springcloud中集成hystrix组件,接下来看一下其他代码测试controller
package com.server.ribbon.hystrix.controller; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import com.server.ribbon.hystrix.bean.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @Author 18011618 * @Date 16:12 2018/7/9 * @Function */ @RestController public class UserRobbinHystrixController { @Autowired private RestTemplate restTemplate; @Value("${user.userServicePath}") private String userServicePath; @GetMapping("/ribbon/{id}") @HystrixCommand(fallbackMethod = "fallFindByIdBack") //@HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties = @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE")) public User findById(@PathVariable Long id) { //简化http模版请求 return this.restTemplate.getForObject(this.userServicePath+"/findUser/" + id, User.class); } /** * 熔断降级的方法 * 这里的方法参数和类型 一定要和原始方法是一致的 否则会报错 * @return */ public User fallFindByIdBack(Long id){ System.out.println("进入熔断降级方法"); return new User().setAge(Long.valueOf(28)).setId(Long.valueOf(0)).setName("返回").setUsername("jhp"); } }
方法体和之前演示的方法没有区别,但是多了一个注解,在方法上加了这个注解就代表对这个方法使用了Hystrix相关的功能,注解里面有个属性,是调用回调之后的处理方法,上面红色标注的方法就是这个功能,controller写完了,看一下main方法的类:
package com.server.ribbon.hystrix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** * @Author 18011618 * @Description * @Date 3:23 2018/7/11 * @Modify By */ @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class RobbinHystrixApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(RobbinHystrixApplication.class,args); } }
相比之前的应用多了一个新注解:,它是用来标识启用融断器功能.,最后看一下对应的配置文件:
spring: application: name: service-ribbon-hystrix #服务应用名称 server: port: 9004 #服务端口号 user: userServicePath: http://service-provider-user/ #服务提供者的url eureka: client: healthcheck: enabled: true #启动服务健康状态的检查 serviceUrl: defaultZone: http://admin:admin@localhost:8080/eureka #注册服务组件的url instance: prefer-ip-address: true #使用ip前缀 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000 #熔断的超时时间
配置文件也没有太多的变化,只是加了个熔断的超时时间(实际生产环境中,这里的配置可能会很多,主要是配置hystirx的其它特性,这里为了演示其基础功能,就不一一列举),那么现在依次启动eureka,server_provider_user,以及对应的,这个时候在浏览器端访问http://localhost:9004/ribbon/1,会出现正常结果:
这个时候我们关闭服务提供者的应用,再刷新浏览器看一下结果:
看到了吧,应用没有报错,而是调用了降级的方法,这样就说明hystrix已经成功结合到springcloud中,怎么样是不是很简单,但功能缺很强大,在这里稍微扩展一下,看一下@HystrixCommand注解的源码:
package com.server.ribbon.hystrix.bean; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface HystrixCommand { String groupKey() default ""; String commandKey() default ""; String threadPoolKey() default ""; String fallbackMethod() default ""; HystrixProperty[] commandProperties() default {}; HystrixProperty[] threadPoolProperties() default {}; Class<? extends Throwable>[] ignoreExceptions() default {}; ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER; HystrixException[] raiseHystrixExceptions() default {}; String defaultFallback() default ""; }
为什么要将这个,因为上面只是为了演示基础功能,所以没有讲解的很详细,实际生产环境中还会使用到其它的属性,通常最常用的就是红色标注的:
>fallbackMethod:熔断,降级,失败之后调用的方法
>commandProperties:配置其它的属性,比如资源隔离,这个是肯定会用到的,通常所说的隔离都是指线程隔离,如果要配置这个应该怎么配置呢:
@HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties = @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"))
上面配置就指定了资源隔离的方式,是通过信号量,当然隔离方式还有线程池,这两种有啥区别呢,看下面:
(1)线程池隔离模式:使用一个线程池来存储当前请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求先入线程池队列。这种方式要为每个依赖服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
(2)信号量隔离模式:使用一个原子计数器(或信号量)记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
附加一个生产环境中关于
@HystrixCommand的一个复杂配置实例
@HystrixCommand(fallbackMethod = "getDefaultUserName", threadPoolKey = "query_user", threadPoolProperties = { @HystrixProperty(name = CORE_SIZE, value = "10"), @HystrixProperty(name = MAX_QUEUE_SIZE, value = "10") }, commandProperties = { @HystrixProperty(name = CIRCUIT_BREAKER_ENABLED, value = "true"), @HystrixProperty(name = CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "1000"), @HystrixProperty(name = CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "25") } ) static String getUserName(String userID) throws InterruptedException { Thread.sleep(-1); return userID; } public String getDefaultUserName(String userID) { return ""; }
大家有没有觉得上面这种写法有点啰嗦,如果是没有关系,接下来再看使用更简单的一种:
& Hystrix+Feign
继续在server_hystrix新建一个模块叫server_hystrix_feign模块,看一下这个模块的代码结构:
重点看一下fegin包下面的:首先分析HystrixFeignclient.java
package com.server.fegin.hystrix.fegin; import com.server.ribbon.hystrix.bean.User; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * @Author 18011618 * @Description 定义一个fegin+引入对应降级实现类 * @Date 20:53 2018/7/10 * @Modify By */ @FeignClient(value = "service-provider-user",fallback = HystrixClientFallback.class) public interface HystrixFeginClient { @RequestMapping(value = "/findUser/{id}",method = RequestMethod.GET) User findById(@PathVariable(value = "id") Long id); }
这里需要使用一个新注解
value:指定的调用服务名称
fallback:处理熔断,降级失败的类,OK 那就看一下这个类的实现吧
package com.server.fegin.hystrix.fegin; import com.server.ribbon.hystrix.bean.User; import org.springframework.stereotype.Component; /** * @Author 18011618 * @Description 实现降级方法 * @Date 3:48 2018/7/11 * @Modify By */
class HystrixClientFallback implements HystrixFeginClient { @Override public User findById(Long id) { System.out.println("进入FeginClient熔断降级方法"); return new User().setAge(Long.valueOf(28)).setId(Long.valueOf(0)).setName("abc").setUsername("jack"); } }
这个类实现了刚刚的那个接口的方法,而这个方法就是默认方法....和上面的ribbon相比,就是把一个方法移到了一个单独的类中来实现:,OK 再看一下对应的main方法
package com.server.fegin.hystrix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; /** * @Author 18011618 * @Description * @Date 3:54 2018/7/11 * @Modify By */ @SpringBootApplication @EnableEurekaClient @EnableFeignClients @EnableCircuitBreaker public class ServerFeginHystrixApplication { public static void main(String[] args) { SpringApplication.run(ServerFeginHystrixApplication.class,args); } }
这个几乎一样都需要使用,最后看一下配置文件:
spring: application: name: service-fegin-hystrix #服务应用名称 server: port: 9005 #服务端口号 user: userServicePath: http://service-provider-user/ #服务提供者的url eureka: client: healthcheck: enabled: true #启动服务健康状态的检查 serviceUrl: defaultZone: http://admin:admin@localhost:8080/eureka #注册服务组件的url instance: prefer-ip-address: true #使用ip前缀 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000 #熔断的超时时间 feign: hystrix: enabled: true #在D版本的Spring Cloud中,它没有默认打开。需要在配置文件中配置打开它
注意这里和ribbon就有点不同了,因为默认在D版本的Feign没有打开,需要打开才能使用,一定要加这个配置,否则看不到任何效果...
OK 如果都没有问题,这个时候启动server_provider_user,然后再启动,在浏览器中输入http://localhost:9005/fegin/2 还是会出现正常结果
如果此时关闭服务提供者,熟悉浏览器再看效果:
到此为止说明fegin也能和hystrix成功整合,在这里有点要注意其实上面的代码有个地方是有问题的,就是忘记在处理失败熔断类上面少了一个注解,还有一点提醒一下,其实从代码结构角度来说,不应该把接口和实现类分开来写,因为从编程角度来说,这里完全适合使用内部接口来完成回调的具体实现,所以可以把那个代码优化成下面这样:
package com.server.fegin.hystrix.fegin; import com.server.ribbon.hystrix.bean.User; 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; /** * @Author 18011618 * @Description 定义一个fegin+引入对应降级实现类 * @Date 20:53 2018/7/10 * @Modify By */ @FeignClient(value = "service-provider-user",fallback = HystrixFeginClient.HystrixClientFallback.class) public interface HystrixFeginClient { @RequestMapping(value = "/findUser/{id}",method = RequestMethod.GET) User findById(@PathVariable(value = "id") Long id); /** * @Author 18011618 * @Date 10:53 2018/7/12 * @Function 处理熔断 失败 降级 的实例 */ @Component class HystrixClientFallback implements HystrixFeginClient { @Override public User findById(Long id) { System.out.println("进入FeginClient熔断降级方法"); return new User().setAge(Long.valueOf(28)).setId(Long.valueOf(0)).setName("abc").setUsername("jack"); } } }
直接通过封装一个内部回调来实现,看上去更加的符合编程思想和原则,细心的人可能发现使用fegin虽然能实现了hystrix功能,但却没有看到它的任何影子呢,这是为什么呢?这就说明了fegin对hystrix要实现了一层包装,完全的透明化,其实可以看一下fegin注解的源码:
不错就是这两个属性用来完成hystrix的功能,如果要熔断的功能或者业务比较复杂可以使用一个工厂来封装,然后直接使用
>fallbackFactory来标注,否则一般的业务直接使用fallback即可,到这里关于hystrix的常用功能就说完了..
版权声明:本文为博主原创文章,未经博主允许不得转载:https://blog.csdn.net/qq_18603599/article/details/80941711