限流的核心
高并发三把斧:限流、降级、缓存
- 限流:限流是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理,程序中的限流就是限制请求,请求包括客户端发起的请求,应用内服务间的请求,很多平台都有限流,例如我们调用第三方的api的时候,对方都会限制每天最多只能调用几次之类的,当请求量超过系统的最大容量后,访问延迟就会增加,超过峰值的流量会拖累整个系统,出现宕机。因此,需要提前流量控制,对于超过峰值的流量,可以直接拒绝掉或者选择随机拒绝。限流结合业务自定义配置,优先保证核心服务的正常响应,非核心服务可直接关闭。
- 降级:降级不是为了防止故障的发生,而是当故障发生后,怎么减小故障所造成的损失。比如,系统正常时提供的服务能力是100%,出现系统故障后,我们有措施能让系统服务能力不直接降到了0,而是还能提供部分(比如50%)的服务能力。场景:依赖的服务耗时增加、边缘业务降级
- 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
限流方案
1. 前端限流
浏览器、app、小程序
- JS提交将按钮置灰 ;
- 验证码、小游戏、问题回答
2. 接入层限流:LVS
对地区限流;对IP限流
接入层指的是请求流量的入口,我们可以在这里做很多控制,比如:负载均衡,缓存,限流等。
3. 代理层限流
IP限流、用户限流、硬件设备限流(Android设备号)、连接数限流、其他限流策略
nginx中针对限流有两个模块可以处理:
- ngx_http_limit_req_module;连接数限流模块
- ngx_http_limit_conn_module;请求限流模块
Nginx 常用于服务器反向代理,达到实现负载均衡和保护后端的应用服务器的目的。Nginx 主要通过限制访问频率和并发连接数两种方式达到限制目的,Nginx 配置文件支持丰富的配置命令,比如下面一种配置示例:
比如 limit_req_zone 的命令含义是对限制的对象(如 URL 地址、服务器地址和客户端 IP 地址等)设置最大访问速率,zone 部分定义了共享内存区的名称和大小。limit_conn 可以对指定的 IP 甚至是所在服务主机限制并发连接数量。
4. 服务层限流
Tomcat、springboot等
Server.xml:maxConnect(Http连接数限流)、maxThreads(线程数限流)
5. 接口层限流(API)、业务方法限流
线程数限流、计数器、Sentinel、Hystrix
Hystrix与Sentinel对比
比较项 | Sentinel | Hystrix | 说明 |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流)(模拟信号量) | 线程池隔 离/信号 量隔离 | Sentinel不创建线程依赖tomcat或jetty容器的 线程池,存在的问题就是运行容器的线程数量 限制了sentinel设置值的上限可能设置不准。 比如tomcat线程池为10,sentinel设置100是 没有意义的,同时隔离性不好 hystrix使用自己 创建的线程池,隔离性会更好 |
熔断降级策 略 | 基于响应时 间、异常比 率、异常数 | 基于异常 比率 | 快速失败的本质功能 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口 (基于 RxJava) | |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | |
扩展性 | 多个扩展点 | 插件的形式 | |
注解 | 支持 | 支持 | |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持(并发线程数或信号量大小) | 快速失败的本质功能 |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持(排队) | |
系统自适应保护 | 支持(仅对linux/unix生效) | 不支持 | 设置一个服务器最大允许处理量的阈值 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看接近实时数据 | 控制台是非常有竞争力的功能,因为能集中配置限制数据更方便,但是展示数据和实时性没有hystrix直观。 |
配置持久化 | ZooKeeper,Apollo,Nacos、本地文件 | Git/svn/本地文件 | Sentinel客户端采用直接链接持久化存储,应用客户端引用了更多的依赖,同样的存储链接可能有多个配置 |
动态配置 | 支持 | 支持 | |
黑白名单 | 支持 | 支持 | |
springcloud集成 | 高 | 非常高 | Spring boot使用hystrix集成度更高 |
整体优势 | 集中配置设置及监控+更细的控制规则 | 漂亮的界面+接近实时的统计结果 | docker容器化部署之后sentinel可能更会发挥作用 |
基于Hystrix微服务限流
引入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
修改application.properties
加上feign.hystrix.enabled=true ,启用hystrix
spring.application.name=service-order
server.port=9001
feign.hystrix.enabled=true
spring.cloud.nacos.discovery.server-addr=localhost:8848
修改启动类
加上@EnableCircuitBreaker,启用断路器
加上@EnableFeignClients,扫描@FeignClient注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@EnableFeignClients
public class ServiceOrderHystrixApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ServiceOrderHystrixApplication.class, args);
}
}
使用Hystrix线程池限流
@RequestMapping(value = "/testQPS1", method = RequestMethod.GET)
@ResponseBody
@HystrixCommand(commandProperties = {
/*线程池隔离*/@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "THREAD"),
/*线程超时时间*/@HystrixProperty(name =HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,value = "3000"),
/*设置超时的时候是否中断线程,默认为true*/
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_INTERRUPT_ON_TIMEOUT, value = "false")
})
public String testQPS1() {
try {
System.out.println(Thread.currentThread().getName() + ":testQPS1 before sleep 5s....");
Thread.sleep(1000 * 5);
System.out.println(Thread.currentThread().getName() + ":testQPS1 after sleep 5s....");
} catch (InterruptedException e) {
//e.printStackTrace();
}
return "ok";
}
使用Hystrix信号量限流
@RequestMapping(value = "/testQPS2", method = RequestMethod.GET)
@ResponseBody
@HystrixCommand(fallbackMethod = "flowError",
commandProperties = {
/*信号量隔离*/@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
/*线程超时时间*/ @HystrixProperty(name =HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,value = "3000"),
/*最大并发数*/ @HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value="3")
})
public String testQPS2() {
try {
System.out.println(Thread.currentThread().getName() + ":testQPS2 before sleep 5s....");
Thread.sleep(1000 * 2);
System.out.println(Thread.currentThread().getName() + ":testQPS2 after sleep 5s....");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}
使用FeignClient+fallback进行服务降级
- 新增FeignStorageService和FeignStorageServiceFallback
@FeignClient(value = "service-storage", fallback =
FeignStorageServiceFallback.class)
public interface FeignStorageService {
@RequestMapping("/storage")
String storage();
}
@Service
public class FeignStorageServiceFallback implements FeignStorageService {
@Override
public String storage() {
System.out.println("当前访问人数过多,请稍后再试!");
return "当前访问人数过多,请稍后再试!";
}
}
- 测试feign调用
@RequestMapping(value = "/orderByFeign", method = RequestMethod.GET)
public String orderByFeign(String params) {
String result = feignStorageService.storage();
return result;
}
基于Sentinel实战微服务限流
Sentinel 环境搭建
下载地址 : https://github.com/alibaba/Sentinel/releases
启动jar服务命令:
java -Dserver.port=10001 -Dcsp.sentinel.dashboard.server=localhost:10001 -jar sentinel-dashboard-1.7.1.jar
浏览器打开:http://127.0.0.1:10001 ,账号和密码都是sentinel
Springboot 整合 Sentinel
Springboot 整合 Sentinel有2种方式:
- 使用纯代码或者注解在工程中进行配置
- Sentinel 控制台 + 注解进行配置
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacosdiscovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
配置文件application.properties
spring.application.name=service-order
server.port=6001
spring.cloud.nacos.discovery.server-addr=localhost:8848
feign.sentinel.enabled=true
#禁用Sentinel 的赖加载
spring.cloud.sentinel.eager=true
# 配置Sentinel
spring.cloud.sentinel.transport.dashboard=localhost:10001
ribbon.ReadTimeout=3000
ribbon.ConnectTimeout=3000
logging.level.com.alibaba.nacos=ERROR
logging.level.com.netflix.loadbalancer=ERROR
logging.level.com.netflix.config=ERROR
management.endpoints.web.exposure.include=*
基于代码实现限流
resource:资源名,即限流规则的作用对象
limitApp:流控针对的调用来源,若为default则不区分调用来源
grade:限流阈值类型,即QPS还是并发线程数,0代表线程数,1代表QPS
count:限流阈值
strategy:调用关系限流策略
controlBehavior:流量控制效果(快速失败,Warm Up,排队等待)
clusterMode:是否集群
配置限流规则
public class SentinelController {
//限流规则名称,与作用的接口名称一致
private static final String LIMIT_KEY_1 = "testQPS1";
private static final String LIMIT_KEY_2 = "testQPS2";
//@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
@PostConstruct
public void initFlowQpsRule() {
//设置testQPS1接口限流规则
List<FlowRule> flowRules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
//设置限流规则名称
rule1.setResource(LIMIT_KEY_1);
//设置QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置QPS为1
rule1.setCount(1);
rule1.setLimitApp("default");
flowRules.add(rule1);
//设置testQPS2限流规则
FlowRule rule2 = new FlowRule();
//设置限流规则名称
rule2.setResource(LIMIT_KEY_2);
//设置QPS限流
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置QPS为2
rule2.setCount(2);
rule2.setLimitApp("default");
flowRules.add(rule2);
FlowRuleManager.loadRules(flowRules);
System.out.println("-------------------配置限流规则成功--------------------
---");
}
}
新增限流接口testQPS1
@GetMapping("/testQPS1")
public String testQPS1() {
Entry entry = null;
try {
entry = SphU.entry(LIMIT_KEY_1);
} catch (BlockException e) { // 限流后进入此异常
//e.printStackTrace();
System.out.println("当前访问人数过多,请稍后再试!");
return "当前访问人数过多,请稍后再试!";
} finally {
// SphU.entry与entry.exit()成对出现,否则会导致调用链记录异常
if (entry != null) {
entry.exit();
}
}
System.out.println("testQPS1 success");
return "testQPS1 success";
}
基于注解实现限流
// value的值即为限流配置的key
@SentinelResource(value = LIMIT_KEY_2, blockHandler = "testBlockHandler")
@GetMapping("/testQPS2")
public String testQPS2() {
System.out.println("testQPS2 success");
return "testQPS2 success";
}
public String testBlockHandler(BlockException e) {
System.out.println("当前访问人数过多,请稍后再试!");
return "当前访问人数过多,请稍后再试!";
}
流控参数
- 资源名:唯一名称,默认是请求路径,可自定义(后续讲解)。
- 针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制。
sentinel共有三种流控模式,分别是:
- 直接(默认):接口达到限流条件时,开启限流。
- 关联:当关联的资源达到限流条件时,开启限流 (适合做应用让步)。
直接流控模式
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流。前面的两个案例都是默认直接流控。
关联流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
配置流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景,底层采用令牌桶算法实现
- 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。底层采用漏桶算法实现
降级参数
-
RT(平均响应时间)
平均响应时间 ( DEGRADE_GRADE_RT ):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值( count ,以 ms 为单位),那么在接下的时间窗口( DegradeRule 中的 timeWindow ,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException )。注意 Sentinel默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
上图表示 需要1s持续进入5个请求,并且 平均响应时间大于 阈值,才会触发降级,打开断路器,等时间窗口结束,再关闭降级
-
异常比例
异常比例 ( DEGRADE_GRADE_EXCEPTION_RATIO ):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值( DegradeRule 中的 count )之后,资源进入降级状态,即在接下的时间窗口( DegradeRule 中的 timeWindow ,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0] ,代表 0% - 100%。
上图表示 QPS>=5 且异常比例超过阈值,触发降级,打开断路器,等时间窗口结束,再关闭降级
-
异常数
异常数 ( DEGRADE_GRADE_EXCEPTION_COUNT ):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
异常数是按分钟来统计的,所以时间窗口必须大于等于60s
上图就是表示,在一分钟内统计异常数超过阈值了,然后出发降级,打开断路器,等时间长款结束,关闭降级