在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
缓存
缓存的目的是提升系统访问速度和增大系统处理容量降级
降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开限流
限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
什么是限流
通过对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机,限流的根本目的就是为了保障服务的高可用
常见的应用限流有两种算法
- 漏桶算法
- 令牌桶算法
漏桶算法比较简单,就是将流量放入桶中,漏桶同时也按照一定的速率流出,如果流量过快的话就会溢出(漏桶并不会提高流出速率
)。溢出的流量则直接丢弃。
如图所示
漏桶算法
虽说简单,但却不能应对实际场景,比如突然暴增的流量。
令牌桶
会以一个恒定的速率向固定容量大小桶中放入令牌,而如果请求需要被处理时则取走一个或多个令牌。当桶中没有令牌则将当前请求丢弃或阻塞,则拒绝服务。
操作系统的信号量是个很重要的概念,Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
信号量的本质是控制某个资源可被同时访问的个数,在一定程度上可以控制某资源的访问频率,但不能精确控制。
Semaphore又称信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量,
一般来说,在网关系统中,还有一个参数叫并发控制,就是某一个资源可以被同时访问的个数。这种情况下,我们可以使用Semaphore来控制。
Semaphore不同于互斥锁。互斥锁是某个资源只能支持同时一个访问,而Semaphore可以支持多个访问,但是加上了总数的控制。
相比之下令牌桶可以应对一定的突发流量.
Guava中开源出来一个令牌桶算法的工具类RateLimiter,可以轻松实现限流的工作,RateLimiter 对简单的令牌桶算法做了一些工程上的优化,具体的实现是 SmoothBursty。需要注意的是,RateLimiter 的另一个实现 SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter 中的时间窗口能且仅能为 1s,如果想搞其他时间单位的限流,只能另外造轮子。
RateLimiter 有一个有趣的特性是「前人挖坑后人跳」,也就是说 RateLimiter 允许某次请求拿走超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它先走掉后面的请求等一等呢?Guava 的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。
- Guava RateLimiter只能应用于单进程,多进程间协同控制便无能为力
- Guava RateLimiter能够处理突发请求(预消费),这里rest接口调用频率限制是固定的,不需要更不能使用预消费能力,否则将会导致接口调用失败
微服务限流
spring-cloud-zuul-ratelimit是和zuul整合提供分布式限流策略的扩展,只需在yaml中配置几行配置,就可使应用支持限流
添加依赖
<dependency> <groupId>com.marcosbarbero.cloud</groupId> <artifactId>spring-cloud-zuul-ratelimit</artifactId> <version>1.7.1.RELEASE</version> </dependency支持的限流粒度
- 服务粒度 (默认配置,当前服务模块的限流控制)
- 用户粒度 (详细说明,见文末总结)
- ORIGIN粒度 (用户请求的origin作为粒度控制)
- 接口粒度 (请求接口的地址作为粒度控制)
- 以上粒度自由组合,又可以支持多种情况。
- 如果还不够,自定义RateLimitKeyGenerator实现
zuul:
ratelimit:
key-prefix: your-prefix
enabled: true
repository: REDIS
behind-proxy: true
default-policy: #deprecated - please use "default-policy-list"
limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
- origin
- url
default-policy-list: #optional - will apply unless specific policy exists
- limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
- origin
- url
policies: #deprecated - please use "policy-list"
myServiceId:
limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
- origin
- url
policy-list:
myServiceId:
- limit: 10 #optional - request number limit per refresh interval window
quota: 1000 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- user
- origin
- url
- type: #optional value for each type
- user=anonymous
- origin=somemachine.com
- url=/api #url prefix
#限流 #对应用来标识请求的key的前缀 zuul.ratelimit.key-prefix=your-prefix zuul.ratelimit.enabled=true #对应存储类型(用来存储统计信息) zuul.ratelimit.repository=redis #开启代理 zuul.ratelimit.behind-proxy=true #deprecated - please use "default-policy-list" #可选 - 每个刷新时间窗口对应的请求数量限制 zuul.ratelimit.policy-list.snjx-api.limit=10 # 刷新时间窗口的时间,默认值 (秒) zuul.ratelimit.policy-list.snjx-api.refreshInterval=60 #可选- 每个刷新时间窗口对应的请求时间限制(秒) zuul.ratelimit.policy-list.snjx-api.quota=1000 #可选 限流类型 zuul.ratelimit.policy-list.snjx-api.type[1]=user zuul.ratelimit.policy-list.snjx-api.type[2]=origin zuul.ratelimit.policy-list.snjx-api.type[3]=url #user是通过授权用户进行区分,也包括匿名用户 zuul.ratelimit.policy-list.snjx-api.type[user]=anonymous #origin是通过客户端IP地址区分 zuul.ratelimit.policy-list.snjx-api.type[origin]=somemachine.com #url类型的限流就是通过请求路径区分 zuul.ratelimit.policy-list.snjx-api.type[url]=/snjx-api/** #url prefix
以上配置意思是:60秒内允许10个访问,并且要求总请求时间小于1000秒
可以使用Spring Boot Actuator 提供的服务状态,动态设置限流开关
用户限流的实现:如果你的项目整合 Shiro 或者 Spring Security 安全框架,那么会自动维护request域UserPrincipal,如果是自己的框架,请登录成功后维护request域UserPrincipal,才能使用用户粒度的限流。未登录默认是:anonymous