高并发系统限流(二)

介绍

在前一篇文章中我们介绍了应用级限流方式,但是不能进行全局限流。本文我们就来介绍下可以进行全局限流的分布式限流和接入层限流

接入层限流

接入层限流通常指请求流量的入口,主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、限流、A/B测试、服务质量监控等。通常使用Nginx(OpenResty)接入层限流

可以使用nginx自带的两个模块:连接数限流模块ngx_http_limit_conn_module漏桶算法实现的请求限流模块ngx_http_limit_req_module.

ngx_http_limit_conn_module

limit_conn是对某个key对应的总的网络连接数进行限流,可以按照IP或者服务域名来限制总连接数。不是每个请求连接都会被计数器统计,只有被nginx处理的且已经读取整个请求头的请求连接会被计数器统计。配置示例如下:

http {
    # 配置限流key、存放key对应信息的共享内存区域大小。此处key为$binary_remote_addr表示ip地址,也可以使用$server_name作为key
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    # 配置记录被限流后的日志级别,默认error级别
    limit_conn_log_level error;
    # 配置被限流后返回的状态码,默认返回503
    limit_conn_status 503;
   ...
   server {
        location /limit {
          # 要配置存放key和计数器的共享内存区域和指定key的最大连接数
          # 此处指定的最大连接数为1(nginx最多同时并发处理1个连接)
          limit_conn addr 1;
        }
   ...
}
复制代码

主要执行流程如下:

未命名文件 (34).png

limit_conn可以限流某个key的总并发数/请求数,key可以根据需要变化。

ngx_http_limit_req_module

limit_req是漏桶算法实现,用于指定key对应的请求进行限流。

我们来看下配置示例:

http {
    # 配置限流key、存放key对应信息的共享内存区域大小,固定请求速率。此处key为$binary_remote_addr表示ip地.固定请求速率使用rate参数配置,支持10r/s和60r/m
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    # 配置记录被限流后的日志级别,默认error级别
    limit_conn_log_level error;
    # 配置被限流后返回的状态码,默认返回503
    limit_conn_status 503;
   ...
   server {
        location /limit {
          # 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)
          limit_req zone=one burst=5 nodelay;
        }
   ...
}
复制代码

执行过程如下所示:

1.请求进入首先判断最后一次请求时间相对于当前时间(第一次是0)是否需要限流,如果需要限流,则执行步骤2,否则执行步骤3

2.如果没有配置桶容量(burst=0),按照固定速率处理请求。如果请求被限流,则直接返回相应的错误码(503)。

如果配置了桶容量(burst>0)及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则以固定平均速率处理(需要延迟处理请求,延迟使用休眠实现)

如果配置了桶容量(burst>0)及非延迟模式(配置了nodelay),则不会按照固定速率处理请求,允许突发处理请求。如果桶满了,则请求被限流,直接返回状态503

3.如果没有被限流,正常处理请求

4.Nginx会在相应时机选择一些(3个节点)限流key进行过期处理,进行内存回收

OpenResty中的lua-resty-limit-traffic

上面介绍的两种使用比较简单,指定key、指定限流速率等就可以了。如果根据实际情况变化key、速率、桶大小等这种动态特性那么使用标准模块就很难实现。因此需要一种可编程方式解决此问题。OpenResty提供了Lua限流模块lua-resty-limit-traffic,可以按照更复杂的业务逻辑进行动态限流处理。提供了limit.connlimit.req实现,算法和nginx limit_conn和limit_req是一样的。

OpenResty 1.11.2.2+默认已经支持该限流库。低版本的话在使用之前需要下载lua-resty-limit-traffic模块并添加到OpenResty的lualib中。我们当前使用的OpenResty版本为1.19.9.1,所以可以直接使用。

我们来看一个limit.req的使用示例:

limit_traffic.lua

local limit_req = require "resty.limit.req"
local rate = 2 -- 固定平均速率 2r/s
local burst = 3 -- 桶容量
local error_status = 503
local nodelay = false -- 是否需要不延迟处理
local lim, err = limit_req.new("limit_req_store", rate, burst)
if not lim then -- 没定义共享字典
    ngx.log(ngx.ERR, "没定义共享字典", delay)
    ngx.exit(error_status)
end
local key = ngx.var.binary_remote_addr -- IP维度限流,如果想根据参数可以查询所需参数进行限流
-- 流入请求,如果请求需要被延迟,则delay > 0
local delay, err = lim:incoming(key, true)
if not delay and err == "rejected" then -- 超出桶大小了
    ngx.log(ngx.ERR, "超出桶大小:", err)
    ngx.exit(error_status)
end
if delay > 0 then -- 根据需要决定是延迟还是不延迟处理
    if nodelay then
        -- 直接突发处理
        ngx.log(ngx.ERR, "突发处理,正常需要延迟时间", delay)
    else
        ngx.sleep(delay) -- 延迟处理
        ngx.log(ngx.ERR, "延迟处理,延迟时间:", delay)
    end
end
复制代码

上面我们配置了固定平均速率 2r/s,桶容量设置为3. 其中nginx.conf我们简单配置下:

worker_processes 1;
error_log  logs/error.log;

events {
  worker_connections 1024;
}

http {
  lua_package_path "$prefix/lua/?.lua;$prefix/libs/?.lua;;";
  lua_shared_dict limit_req_store 10m;
  server {
    server_name localhost;
    listen 8080;
    charset utf-8;
    set $LESSON_ROOT lua/;
    error_log  logs/error.log;
    access_log logs/access.log;
    location /limit {
      default_type text/html;
      content_by_lua_file $LESSON_ROOT/limit_traffic.lua;
    }
  }
}
复制代码

启动openresty后,我们使用ab命令来并发请求下。

wukongdeMacBook-Pro:limit wukong$ ab -c 3 -n 3 http://127.0.0.1:8080/limit
复制代码

我们会发现有一个请求正常处理,另外两个进行了延迟处理。延迟处理的等待时间分别为500ms和1s,和我们设置的2r/s和桶容量3个是可以对应上的。如果想要突发处理的话可以在limit_traffic.lua中是否允许突发的变量nodelay设置为true.

2022/04/09 17:09:09 [error] 67100#12461017: *1008 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.499, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:09:09 [error] 67100#12461017: *1009 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.999, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
复制代码

如果同一时间并发请求远超过3个那后面的请求就会被拒绝,我们来简单测试看下,我们就以同时10个并发为例:

wukongdeMacBook-Pro:limit wukong$ ab -c 10 -n 10 http://127.0.0.1:8080/limit
复制代码

我们再来看下日志:

2022/04/09 17:16:53 [error] 67100#12461017: *1014 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1015 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1016 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1017 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1018 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1019 [lua] limit_traffic.lua:15: 超出桶大小rejected, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:53 [error] 67100#12461017: *1011 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.499, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:54 [error] 67100#12461017: *1012 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:0.999, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 17:16:54 [error] 67100#12461017: *1013 [lua] limit_traffic.lua:24: 延迟处理,延迟时间:1.498, client: 127.0.0.1, server: localhost, request: "GET /limit HTTP/1.0", host: "127.0.0.1:8080"
复制代码

我们会发现有6个请求被拒绝,1个请求已正常处理,3个请求延迟处理。

上面根据Nginx+lua实现简单的limit.req限流,示例代码已放到github,实际使用根据自身需要灵活运用。

另外Nginx也提供了limit_rate对流量限速,例如limit_rate 50K,表示限制下载速度为50K。

分布式限流

分布式限流最关键的是将限流服务做成原子化,解决方案:Redis+Lua或者Nginx+Lua,通过以上方案可实现高并发和高性能。

下面我们来看一个使用Redis+Lua实现时间窗口内某个接口的请求数限流,后期可改造为限流总并发/请求总资源数。Lua本身就是一种变成语言,可实现复杂令牌桶或漏桶算法。

Redis+Lua实现

@Component
@Slf4j
public class RedisLimit {
    @Autowired
    private JedisPool jedisPool;

    /**
     * 限流lua
     */
    private static final String LIMIT_LUA;

    static {
        LIMIT_LUA = new StringBuilder()
                //限流key
                .append("local key = KEYS[1] ")
                //限流大小
                .append("local limit = tonumber(ARGV[1]) ")
                .append("local current = tonumber(redis.call('get', key) or '0') ")
                //如果超出限流大小
                .append("if current + 1 > limit then ")
                .append(" return 0 ")
                //请求数+1并设置2s过期
                .append("else ")
                .append("redis.call('INCRBY', key, '1') ")
                .append("redis.call('expire', key, '2') ")
                .append(" return 1")
                .append(" end").toString();
    }

    /**
     * 是否需要限流
     * @return
     */
    public boolean acquire(){
        Jedis jedis = jedisPool.getResource();
        try {
            //ip
            String key = IpUtils.getIp();
            //限流大小(分布式配置)
            String limit = "1000";
            return (Long) jedis.eval(LIMIT_LUA, Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
        } catch (Exception e) {
            log.error("是否需要限流异常", e);
        } finally {
            if(null != jedis){
                jedis.close();
            }
        }
        return false;
    }
}
复制代码

调用acquire来知道是否需要限流。上面我们限流大小设置的为1000,可以使用分布式配置进行配置,可实时修改。

Nginx+Lua

limit_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 -- 计数器
    local key  = "ip" ..os.time()
    local limit = 5 -- 限流大小
    local current = limit_counter:get(key)

    if current ~= nil and current + 1 > limit then -- 如果超出限流大小
        lock:unlock()
        return 0
    end
    if current == nil then
        limit_counter:set(key, 1, 1) -- 第一次需要设置过期时间,设置key的值为1,过期时间为1s
    else
        limit_counter:incr(key, 1) -- 第二次开始加1即可
    end
    lock:unlock()
    return 1
end
ngx.log(ngx.ERR, "限流标识(1:正常 0:限流):", acquire())
复制代码

我们用ab命令简单测试下:

wukongdeMacBook-Pro:limit wukong$ ab -c 10 -n 10 http://127.0.0.1:8080/limit/lock
复制代码

输出日志如下:

2022/04/09 21:56:13 [error] 30012#12823604: *26 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *27 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *28 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *29 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *30 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):1, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *31 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *32 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *33 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *34 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
2022/04/09 21:56:13 [error] 30012#12823604: *35 [lua] limit_lock.lua:22: 限流标识(1:正常 0:限流):0, client: 127.0.0.1, server: localhost, request: "GET /limit/lock HTTP/1.0", host: "127.0.0.1:8080"
复制代码

可以看到有五个正常执行,和预期一致。

在上面nginx+lua的代码实现中我们使用了lua-resty-lock互斥锁来解决原子性问题,在实际使用过程中我们需要考虑获取锁的超时问题。

聊到现在你可能会问如果并发量特别大,redis或者nginx是否能扛的住,可以从以下方面考虑:

1.综合考虑下流量是不是真的特别大,当前限流是否已经满足 2.通过一致性哈希将分布式限流进行分片 3.可以当并发量太大时降级为应用级限流 ...

总结

本文主要讨论了分布式限流和接入层限流,实际使用中结合自身需要进行选择。应用级限流可参考之前一篇文章应用级限流

参考书籍:《亿级流量网站架构核心技术》

猜你喜欢

转载自juejin.im/post/7084606535573176351