咱们每一个系统在作压测的时候,都有一个处理峰值,当接近峰值继续接受请求的时候,会致使整个系统响应缓慢;为了保护系统,须要拒绝处理过载的请求,这就是咱们下面介绍的限流,经过设定一个峰值阈值,限制请求达到这个峰值,以此来保护系统;咱们常见的一些中间件好比tomcat,mysql,redis等等都有相似的限制。html
作限流的时候咱们有一些经常使用的限流算法包括:计数器限流,令牌桶限流,漏桶限流;mysql
令牌桶算法的原理是系统以必定速率向桶中放入令牌,填满了就丢弃令牌;请求来时会先从桶中取出令牌,若是能取到令牌,则能够继续完成请求,不然等待或者拒绝服务;令牌桶容许必定程度突发流量,只要有令牌就能够处理,支持一次拿多个令牌;nginx
漏桶算法的原理是按照固定常量速率流出请求,流入请求速率任意,当请求数超过桶的容量时,新的请求等待或者拒绝服务;能够看出漏桶算法能够强制限制数据的传输速度;web
计数器是一种比较简单粗暴的算法,主要用来限制总并发数,好比数据库链接池、线程池、秒杀的并发数;计数器限流只要必定时间内的总请求数超过设定的阀值则进行限流;面试
了解了限流算法以后,咱们须要知道在什么地方限流,以及如何限流;对于一个系统来讲咱们经常能够在接入层进行限流,这个大部分状况下能够直接使用nginx,OpenResty等中间件直接处理;也能够在业务层进行限流,这个须要根据咱们不一样的业务需求使用相关的限流算法来处理。redis
对于业务层咱们多是单节点的,也多是多节点用户绑定的,也多是多节点无绑定的;这时候咱们就要区分是进程内的限流仍是须要分布式限流。算法
对于进程内限流相对来讲仍是比较简单的,guava是咱们常用的利器,下面分别看看如何限制接口的总并发量,某个时间窗口的请求数,以及使用令牌桶和漏桶算法更加平滑的限流;sql
只须要配置一个总并发量,而后使用一个计算器记录每次请求,而后和总并发量比较便可:数据库
private static int max = 10; private static AtomicInteger limiter = new AtomicInteger(); if (limiter.incrementAndGet() > max){ System.err.println("超过最大限制数"); return; }
限制某个接口在指定时间以内的请求量,可使用guava的cache来缓存计数器,而后再设置过时时间;好比下面设置每分钟最大请求为100:api
LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long key) throws Exception { return new AtomicLong(0); } }); private static int max = 100; long curMinutes = System.currentTimeMillis() / 1000 * 60; if (counter.get(curMinutes).incrementAndGet() > max) { System.err.println("时间窗口请求数超过上限"); return; }
过时时间为一分钟,每分钟自动清零;这种处理方式可能会出现超限的状况,好比前59秒都没有消息,到60的时候一会儿来了200条消息,这时候先接受了100条消息,恰好到期计数器清0,而后又接受了100条消息;这种状况能够参考TCP的滑动窗口思路来解决。
计数器的方式仍是比较粗暴的,令牌桶和漏桶限流这两种算法相对来讲仍是比较平滑的,能够直接使用guava提供的RateLimiter类:
RateLimiter limiter = RateLimiter.create(2); System.out.println(limiter.acquire(4)); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire(2)); System.out.println(limiter.acquire()); System.out.println(limiter.acquire());
create(2)表示桶容量为2而且每秒新增2个令牌,也就是500毫秒新增一个令牌,acquire()表示从里面获取一个令牌,返回值为等待的时间,输出结果以下:
0.0 1.998633 0.49644 0.500224 0.999335 0.500186
能够看到此算法是容许必定突发状况的,第一次获取4个令牌等待时间为0,后面再获取须要等待2秒才能够,后面每次获取须要500毫秒。
如今大部分系统都采用了多节点部署,因此一个业务可能在多个进程内被处理,因此这时候分布式限流必不可少,好比常见的秒杀系统,可能同时有N台业务逻辑节点;
常规的作法是使用Redis+lua和OpenResty+lua来实现,将限流服务作成原子化,同时也要保证高性能;Redis和OpenResty都已高性能著称,同时也提供了原子化方案,具体以下所示;
Redis在服务端对消息的处理是单线程的,同时支持lua脚本的执行,能够将限流的相关逻辑用lua脚本实现,来保证原子性,大致实现以下:
-- 限流 key local key = KEYS[1] -- 限流大小 local limit = tonumber(ARGV[1]) -- 过时时间 local expire = tonumber(ARGV[2]) local current = tonumber(redis.call('get',key) or "0") if current + 1 > limit then return 0; else redis.call("INCRBY", key, 1) redis.call("EXPIRE", key, expire) return current + 1 end
以上使用计数器算法来实现限流,在调用lua的地方能够传入限流key,限流大小以及key的有效期;返回结果若是为0表示超出限流大小,不然返回当前累计的值。
OpenResty核心就是nginx,可是在这个基础之上加了不少第三方模块,ngx_lua模块将lua嵌入到了nginx中,使得nginx能够做为一个web服务器来使用;还有其余经常使用的开发模块如:lua-resty-lock,lua-resty-limit-traffic,lua-resty-memcached,lua-resty-mysql,lua-resty-redis等等;
本小节咱们先使用lua-resty-lock模块来实现一个简单计数器限流,相关lua代码以下:
local locks = require "resty.lock"; local function acquire() local lock = locks:new("locks"); local elapsed, err = lock:lock("limit_key"); local limit_counter = ngx.shared.limit_counter; --获取客户端ip local key = ngx.var.remote_addr; --限流大小 local limit = 5; local current = limit_counter:get(key); --打印key和当前值 ngx.say("key="..key..",value="..tostring(current)); if current ~= nil and current + 1 > limit then lock:unlock(); return 0; end if current == nil then limit_counter:set(key,1,5); --设置过时时间为5秒 else limit_counter:incr(key,1); end lock:unlock(); return 1; end
以上是一个对ip进行限流的实例,由于须要保证原子性,因此使用了resty.lock模块,同时也相似redis设置了过时时间重置,另一点须要注意对锁的释放;还须要设置两个共享字典
http { ... #lua_shared_dict <name> <size> 定义一块名为name的共享内存空间,内存大小为size; 经过该命令定义的共享内存对象对于Nginx中全部worker进程都是可见的 lua_shared_dict locks 10m; lua_shared_dict limit_counter 10m; }
接入层一般就是流量入口处,Nginx被不少系统用做流量入口,固然OpenResty也不例外,并且OpenResty提供了更强大的功能,好比这里将要介绍的lua-resty-limit-traffic模块,是一个功能强大的限流模块;在使用lua-resty-limit-traffic以前咱们先大体看一下如何使用OpenResty;
直接去官方下载便可:http://openresty.org/en/download.html,启动,重载,中止命令以下:
nginx.exe nginx.exe -s reload nginx.exe -s stop
打开ip+端口,能够看到:Welcome to OpenResty! 即表示启动成功;
首先须要在nginx.conf的http目录下作以下配置:
http { ... lua_package_path "/lualib/?.lua;;"; #lua 模块 lua_package_cpath "/lualib/?.so;;"; #c模块 include lua.conf; #导入自定义lua配置文件 }
这里自定义了一个lua.conf,有关lua的请求都在这里面配置,放在和nginx.conf一个路径下便可;已一个test.lua为例,lua.conf配置以下:
#lua.conf server { charset utf-8; #设置编码 listen 8081; server_name _; location /test { default_type 'text/html'; content_by_lua_file lua/api/test.lua; } }
这里把全部的lua文件都放在lua/api目录下,好比一个最简单的hello world:
ngx.say("hello world");
lua-resty-limit-traffic提供了限制最大并发链接数,时间窗口请求数,以及平滑限制请求数三种方式,分别对应:resty.limit.conn,resty.limit.count,resty.limit.req;相关文档能够直接在pod/lua-resty-limit-traffic中找到,里面有完整的实例;
如下会用到三个共享字典,事先在http下配置:
http { lua_shared_dict my_limit_conn_store 100m; lua_shared_dict my_limit_count_store 100m; lua_shared_dict my_limit_req_store 100m; }
提供的resty.limit.conn限制最大链接数,具体脚本以下:
local limit_conn = require "resty.limit.conn" --B<syntax:> C<obj, err = class.new(shdict_name, conn, burst, default_conn_delay)> local lim, err = limit_conn.new("my_limit_conn_store", 1, 0, 0.5) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == "rejected" then return ngx.exit(502) end ngx.log(ngx.ERR, "failed to limit req: ", err) return ngx.exit(500) end if lim:is_committed() then local ctx = ngx.ctx ctx.limit_conn = lim ctx.limit_conn_key = key ctx.limit_conn_delay = delay end local conn = err if delay >= 0.001 then ngx.sleep(delay) end
new()参数分别是:字典名称,容许的最大并发请求数,容许的突发链接数,链接延迟;
incoming()中commit是一个布尔值,当为true时表示记录当前请求的数量,不然就直接运行;
返回值:若是请求不超过方法中指定的conn值,则此方法返回0做为延迟以及当前时间的并发请求(或链接)数;
提供的resty.limit.count能够限制必定请求数在一个时间窗口内,具体脚本以下:
local limit_count = require "resty.limit.count" --B<syntax:> C<obj, err = class.new(shdict_name, count, time_window)> --速率限制在20/10s local lim, err = limit_count.new("my_limit_count_store", 20, 10) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err) return ngx.exit(500) end local local key = ngx.var.binary_remote_addr --B<syntax:> C<delay, err = obj:incoming(key, commit)> local delay, err = lim:incoming(key, true) if not delay then if err == "rejected" then return ngx.exit(503) end ngx.log(ngx.ERR, "failed to limit count: ", err) return ngx.exit(500) end
new()中指定的三个参数分别是:字典名称,指定的请求阈值,请求个数复位前的窗口时间,以秒为单位;
incoming()中commit是一个布尔值,当为true时表示记录当前请求的数量,不然就直接运行;
返回值:若是请求数在限制范围内,则返回当前请求被处理的延迟和将被处理的请求的剩余数;
提供的resty.limit.req能够已更加平滑的方式限制请求,具体脚本以下:
local limit_req = require "resty.limit.req" --B<syntax:> C<obj, err = class.new(shdict_name, rate, burst)> --限制在200个请求/秒如下,给与100个请求/秒的突发请求;也就说每秒请求最大能够200-300之间,超出300报错 local lim, err = limit_req.new("my_limit_req_store", 200, 100) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err) return ngx.exit(500) end local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == "rejected" then return ngx.exit(503) end ngx.log(ngx.ERR, "failed to limit req: ", err) return ngx.exit(500) end if delay >= 0.001 then local excess = err ngx.sleep(delay) end
new()三个参数分别是:字典名称,请求速率(每秒数)阈值,每秒容许延迟的过多请求数;
incoming()中commit是一个布尔值,当为true时表示记录当前请求的数量,不然就直接运行,能够理解为一个开关;
返回值:若是请求数在限制范围内,则此方法返回0做为当前时间的延迟和每秒过多请求的(零)个数;
更多能够直接查看官方文档:pod/lua-resty-limit-traffic目录下
本文首先介绍了常见的限流算法,而后介绍在业务层进程内和分布式应用分别是如何进行限流的,最后接入层经过OpenResty的lua-resty-limit-traffic模块进行限流。
能够关注微信公众号「 回滚吧代码」,第一时间阅读,文章持续更新;专一Java源码、架构、算法和面试。