乐趣区

关于java:限流浅析

前言

咱们每个零碎在做压测的时候,都有一个解决峰值,当靠近峰值持续承受申请的时候,会导致整个零碎响应迟缓;为了爱护零碎,须要回绝解决过载的申请,这就是咱们上面介绍的限流,通过设定一个峰值阈值,限度申请达到这个峰值,以此来爱护零碎;咱们常见的一些中间件比方 tomcat,mysql,redis 等等都有相似的限度。

限流算法

做限流的时候咱们有一些罕用的限流算法包含:计数器限流,令牌桶限流,漏桶限流;

  • 1. 令牌桶限流

令牌桶算法的原理是零碎以肯定速率向桶中放入令牌,填满了就抛弃令牌;申请来时会先从桶中取出令牌,如果能取到令牌,则能够持续实现申请,否则期待或者拒绝服务;令牌桶容许肯定水平突发流量,只有有令牌就能够解决,反对一次拿多个令牌;

  • 2. 漏桶限流

漏桶算法的原理是依照固定常量速率流出申请,流入申请速率任意,当申请数超过桶的容量时,新的申请期待或者拒绝服务;能够看出漏桶算法能够强制限度数据的传输速度;

  • 3. 计数器限流

计数器是一种比较简单粗犷的算法,次要用来限度总并发数,比方数据库连接池、线程池、秒杀的并发数;计数器限流只有肯定工夫内的总申请数超过设定的阀值则进行限流;

如何限流

理解了限流算法之后,咱们须要晓得在什么中央限流,以及如何限流;对于一个零碎来说咱们经常能够在接入层进行限流,这个大部分状况下能够间接应用 nginx,OpenResty 等中间件间接解决;也能够在业务层进行限流,这个须要依据咱们不同的业务需要应用相干的限流算法来解决。

业务层限流

对于业务层咱们可能是单节点的,也可能是多节点用户绑定的,也可能是多节点无绑定的;这时候咱们就要辨别是过程内的限流还是须要分布式限流。

过程内限流

对于过程内限流相对来说还是比较简单的,guava 是咱们常常应用的利器,上面别离看看如何限度接口的总并发量,某个工夫窗口的申请数,以及应用令牌桶和漏桶算法更加平滑的限流;

  • 限度接口的总并发量

只须要配置一个总并发量,而后应用一个计算器记录每次申请,而后和总并发量比拟即可:

private static int max = 10;
private static AtomicInteger limiter = new AtomicInteger();

if (limiter.incrementAndGet() > max){System.err.println("超过最大限度数");
    return;
}
  • 限度工夫窗口申请数

限度某个接口在指定工夫之内的申请量,能够应用 guava 的 cache 来缓存计数器,而后再设置过期工夫;比方上面设置每分钟最大申请为 100:

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

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+lua

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;

OpenResty 装置应用

  • 下载安装配置

间接去官网下载即可:http://openresty.org/en/download.html,启动,重载,进行命令如下:

nginx.exe
nginx.exe -s reload
nginx.exe -s stop

关上 ip+ 端口,能够看到:Welcome to OpenResty! 即示意启动胜利;

  • lua 脚本实例

首先须要在 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 模块

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 源码、架构、算法和面试。

退出移动版