格物致知,格Hystrix
某电子商务网站在一个黑色星期五发生过载.过多的并发请求,导致用户支付的请求延迟很久没有响应,在等待很长时间后最终失败。支付失败又导致用户重新刷新页面并再次尝试支付,进一步增加了服务器的负载,最终整个系统都崩溃了。
雪崩效应 当依赖的服务不可用时,服务自身会不会被拖垮?这是我们要考虑的问题。
断路器模式
一个远程调用对应着一个线程/进程。如果响应太慢,这个线程/进程就得不到释放。资源就会被耗尽,最终导致服务不可用。
断路器可以理解为对容易导致错误的操作的代理。这种代理能够统计一段时间内调用失败的次数,并决定是正常请求依赖的服务还是直接返回。
断路器可以实现快速失败,当它在一段时间内检测到许多类似的错误(例如超时),就会在之后的一段时间内,强迫对该服务的调用快速失败,即不再请求所依赖的服务。这样,应用程序就不须浪费CPU时间去等待长时间的超时。
断路器也可以自动诊断依赖的服务是否已经恢复正常。
- 正常情况下断路器关闭,可正常请求依赖的服务。
- 当一段时间内,请求失败率达到一定阈值,断路器就会打开。此时,不会再去请求依赖的服务。
- 断路器打开一段时间后,会自动进入‘半开’状态。此时,断路器可允许一个请求访问依赖的服务。如果请求能够调用成功,则关闭断路器;否则继续保持打开状态。
使用Hystrix实现容错
Hytrix是一个实现了超时机制和短路器模式的工具类库。
- 包裹请求
- 跳闸机制
- 资源隔离: Hystrix 为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就会被立即拒绝,而不是排队等候,从而加速失败判定。
- 监控: Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功/失败/超时/被拒绝的请求等。
- 回退机制:当失败/超时/被拒绝,或当断路器打开时,执行回退逻辑。逻辑可以自定义,例如返回一个缺省值。
- 自我修复:‘半开’状态。
事上练
通用方式整合Hystrix
Spring Cloud 自动将Srping bean 与该注解@HystrixCommand封装在一个连接到Hystrix断路器的代理中。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix //@EnableCircuitBreaker
// 这是用来测试feign 和@FeignClient 注解的接口同时使用
public class MovieApp
@Autowired
private LoadBalancerClient loadBalancerClient;
public User findByIdFallback(Long id) {
User user = new User();
user.setId(-1L);
user.setName("默认用户");
return user;
}
@HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties= {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000")
}, threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10")
})
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
// return this.restTemplate.getForObject("http://user-service/ycq/" + id, User.class);
return this.userFeignClient.findById(id);
}
正常情况下返回结果:
{ id: 1, username: “account1”, name: “张三”, age: 20, balance: 100 }
关闭用户提供者服务则返回:
{ id: -1, username: null, name: “默认用户”, age: null, balance: null }
Hystrix断路器的状态监控与深入理解
关闭服务提供者,hytrix健康状态为UP,说明短路器还是关闭的。
访问Acture提供的health端点,可以查看到:
db: {
status: "UP",
database: "H2",
hello: 1
},
refreshScope: {
status: "UP"
},
hystrix: {
status: "UP"
}
强调-执行回退逻辑并不代表断路器已经打开,因为我们的失败率还没有达到阈值(默认是5秒内20次失败)
持续快速请求服务提供者,直到请求快速返回。可以看到断路器打开,不会再请求服务提供者。
refreshScope: {
status: "UP"
},
hystrix: {
status: "CIRCUIT_OPEN",
openCircuitBreakers: [
"user-service/ycq::UserFeignClient#findById(Long)",
"MovieController::findById"
]
}
Hystrix线程隔离策略与传播上下文
Hystrix的隔离策略两种: 分别是线程隔离和信号量隔离。
- THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。
- SEMAPHORE(信号量隔离):使用该方式,HystrixCommand将会在调用线程上执行,开销相对较小,并发请求受到信号量个数的限制。
Hystrix中默认并且推荐使用线程隔离(THREAD),因为这种方式有一个除网络超时以外的额外保护。
一般来说,只有当调用负载异常高时(例如每个实例每秒调用数百次)才需要信号量隔离,因为这种场景下使用THREAD开销会比较高。信号量隔离一般仅适用于非网络调用的隔离。
可以使用execution.isolation.strategy属性指定隔离策略。
正常情况下,默认为线程隔离, 保持默认即可。
如果发生找不到上下文运行时异常,可考虑将隔离策略设置为SEMAPHORE。
Feign使用Hystrix
前面讲的是使用注解@HystrixCommand的fallbackMethod属性实现回退的。然而,Feign是以接口形式工作的,它没有方法体。
Spring Cloud默认已为Feign整合了Hystrix,只要Hystrix在项目的classpath中,Feign默认就会用断路器包裹所有方法。
// 1.这是用来测试feign
@FeignClient(name = "user-service/ycq", configuration = FeignLogConfiguration.class,
fallback = FeignClientFallback.class)
public interface UserFeignClient {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable("id") Long id);
}
@Component
class FeignClientFallback implements UserFeignClient {
public User findById(Long id) {
User user = new User();
user.setId(-1L);
user.setUsername("为Feign使用Hytrix添加回退");
return user;
}
当服务提供者未启动时,返回如下:
{
id: -1,
username: “为Feign使用Hytrix添加回退”,
name: null,
age: null,
balance: null
}
通过Fallback Factory检查回退原因
方法和刚才的为Feign使用Hytrix添加回退一样。事上练后发现走了fallback分支,fallbackFactory,没起效果,删除掉fallback配置就行了。
@FeignClient(name = "user-service/ycq", configuration = FeignLogConfiguration.class,
fallback = FeignClientFallback.class,
fallbackFactory = FeignClientFallbackFactory.class)
@Component
class FeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
protected static final Logger LOGGER = LoggerFactory.getLogger(FeignClientFallbackFactory.class);
public UserFeignClient create(final Throwable cause) {
return new UserFeignClient() {
public User findById(Long id) {
FeignClientFallbackFactory.LOGGER.info("fallback; reason was:", cause);
User user = new User();
user.setId(-1L);
user.setUsername("为Feign使用Hytrix添加回退这个还可以打印日志");
return user;
}
};
}
}
fallbackFactory刚才的方法里面可以对异常类型进行判断返回不同的回退结果。注意cause有可能是null(该Bug在Feign 9.4.0中已被解决)
为Feign禁用Hystrix
前面讲过,在Spring Cloud中,只要Hystrix在项目的classpath中,Feign就会使用断路器包裹Feign客户端的所有方法。这样方便但很多场景下无用,如何禁用。
- 指定服务禁用
@Configuration
// 想要禁用Hystrix的@FeignClient引用该配置类即可。
public class FeignDisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
测试的话,则需要谨慎了,这里之事对单个的服务设置的。所以可以5秒内刷新多次,看hystrix的状态是否为打开。如果关闭,也就是状态为UP,则满足我们的测试点。 可惜错了,说明这种配置还是有问题的。
// @HystrixCommand(fallbackMethod = "findByIdFallback", commandProperties= {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000"),
// @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000")
// }, threadPoolProperties = {
// @HystrixProperty(name = "coreSize", value = "1"),
// @HystrixProperty(name = "maxQueueSize", value = "10")
// })
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
注释掉才行。当然下面的回退策略也不生效了。
// 1.这是用来测试feign
@FeignClient(name = "user-service/ycq", configuration = FeignDisableHystrixConfiguration.class,
// fallback = FeignClientFallback.class,
fallbackFactory = FeignClientFallbackFactory.class)
public interface UserFeignClient {
- 全局禁用
在application.yml 中配置 feign.hystrix.enabled = false
Hystrix的监控
包含starter-hystrix的项目都有了这个功能,访问actuator端点hystrix.stream即可。
Hystrix Dashboard可视化监控数据
- 加入@EnableHystrixDashboard
- 配置application.xml server.port=8030
- 加入依赖
@SpringBootApplication
@EnableHystrixDashboard
public class Monitor
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
<version>1.2.4.RELEASE</version>
</dependency>
缺点:只能使用/hystrix.stream端点监控单个微服务实例。
事上练: 因为访问量太少,Dashboard显示的图片不是很直观,这个时候我想到了压测工具JMeter。 场景:100万个用户5秒内请求,抢红包(科大讯飞面试考官很感兴趣)。
先设置JMeter参数:
我的电脑是8CPU,可以看到JMeter显示每秒处理完595个请求。跑了几秒,后面全开始报错了。
java.net.BindException: Address already in use: connect
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source)
at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source)
at java.net.AbstractPlainSocketImpl.connect(Unknown Source)
at java.net.PlainSocketImpl.connect(Unknown Source)
at java.net.SocksSocketImpl.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at org.apache.http.conn.scheme.PlainSocketFactory.connectSocket(PlainSocketFactory.java:121)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:180)
at org.apache.jmeter.protocol.http.sampler.hc.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:318)
at org.apache.jmeter.protocol.http.sampler.MeasuringConnectionManager$MeasuredConnection.open(MeasuringConnectionManager.java:114)
短时间内new socket操作过多
而socket.close()操作并不能立即释放绑定的端口
而是把端口设置为TIME_WAIT状态
过段时间(默认240s)才释放(用netstat -na可以看到)
最后系统资源耗尽
(windows上是耗尽了pool of ephemeral ports 这段区间在1024-5000之间)
参考:https://blog.csdn.net/ywb201314/article/details/51258777
格这个问题:
首先C:\Users\16558>netstat -na > netstat.txt 将日志保存后查看进程:
发现所有进程大部分处于TIME_WAIT状态, 而进程总数量为16700多。过了一会儿这个峰值就变成了300个。 知行合一,用知识来证明这个事情是这样的,确实是这个毛病。
后来发现,原来是JMeter我们设置的用户100万太多了,设置1万<16700才行。这种测100万并发的,得搞个压测的微服务(肉鸡够多)才行。
上图是改成1万个用户请求后的结果,吞吐是1000。
字体颜色标记的意思在右上角有对应的解释,比如红色是Failure等。
到此为止吧,以后再慢慢格这个,已经黔驴技穷了,有人有啥想法,我可以继续格。
Turbine聚合监控数据
刚才讨论的使用/hystrix.stream端点监控单个微服务实例,显然不适用,一般我们会部署多个实例。Turbine可以将所有相关的/hystrix.stream端点的数据聚合到一个组合的/turbine.stream中。效果图:
这里首先新建一个工程 hystrix-turbine,利用它整合多个微服务数据。
三步到位:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
application.yml配置
server:
port: 8013
spring:
application:
name: hystrix-turbine-app
eureka:
client:
service-url:
defaultZone: http://localhost:8111/eureka
instance:
prefer-ip-address: true
turbine:
appConfig: film-service2, film-service
clusterNameExpression: "'default'"
@SpringBootApplication
@EnableTurbine
public class HystrixTurbineApp
{
public static void main( String[] args )
{
SpringApplication.run(HystrixTurbineApp.class, args);
}
}
最后打开Hystrix Dashboard首页,在URL一栏加上地址即可:
使用消息中间件收集数据
场景: 微服务与Turbine网络不通,此时,可借助消息中间件实现数据收集。各个微服务将Hystrix Command的监控数据发送至消息中间件,Turbine消费消息中间件中的数据。如以RabbitMQ为例:
ActiveMQ安装传送门
首先在设置断路器hystrix和turbine(监控)中application.yml中加入:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
- 改造微服务,服务消费者film-service增加如下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
- 改造Turbine
启动类:注释掉@EnableTurbine,增加@EnableTurbineStream
/**
* Hello world!
*
*/
@SpringBootApplication
//@EnableTurbine
@EnableTurbineStream
public class HystrixTurbineApp
{
public static void main( String[] args )
{
SpringApplication.run(HystrixTurbineApp.class, args);
}
}
依赖:注释掉starter-turbine,增加starter-turbine-stream和starter-stream-rabbit
<!-- <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>