Limitação de corrente do sistema de alta simultaneidade (2)

introduzir

No artigo anterior, introduzimos o método de limitação de corrente no nível do aplicativo, mas não podemos executar a limitação de corrente total. Neste artigo, apresentaremos a limitação de corrente distribuída e a limitação de corrente da camada de acesso que pode realizar a limitação de corrente global .

Limitação de corrente da camada de acesso

A limitação de corrente da camada de acesso geralmente se refere à entrada de tráfego de requisições, cujos principais objetivos são: balanceamento de carga, filtragem de requisições ilegais, agregação de requisições, cache, limitação de corrente, testes A/B e monitoramento da qualidade do serviço. Normalmente , a limitação de corrente da camada de acesso Nginx (OpenResty) é usada .

Dois módulos que vêm com o nginx podem ser usados: o módulo de limitação de corrente do número de conexão ngx_http_limit_conn_module e o módulo de limitação de corrente de solicitação ngx_http_limit_req_module implementado pelo algoritmo de balde vazado .

ngx_http_limit_conn_module

limit_conn é limitar o número total de conexões de rede correspondentes a uma determinada chave. O número total de conexões pode ser limitado de acordo com o IP ou nome de domínio do serviço . Nem toda conexão de solicitação será contada pelo contador, apenas a conexão de solicitação processada pelo nginx e todo o cabeçalho da solicitação lido será contada pelo contador . O exemplo de configuração é o seguinte:

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;
        }
   ...
}
复制代码

O principal processo de execução é o seguinte:

未命名文件 (34).png

limit_conn pode limitar o número total simultâneo/número de solicitação de uma chave, e a chave pode ser alterada conforme necessário.

ngx_http_limit_req_module

limit_req é a implementação do algoritmo leaky bucket, que é usado para especificar a solicitação correspondente à chave para limitar a corrente.

Vejamos um exemplo de configuração:

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;
        }
   ...
}
复制代码

O processo de execução é o seguinte:

1. Solicitar entrada Primeiro julgue se a hora da última solicitação em relação à hora atual (a primeira vez é 0) precisa limitar a corrente , se precisar limitar a corrente, vá para a etapa 2, caso contrário, vá para a etapa 3

2. Se a capacidade do bucket não estiver configurada (burst=0), as solicitações serão processadas a uma taxa fixa. Se a solicitação for limitada, o código de erro correspondente (503) será retornado diretamente.

如果配置了桶容量(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.可以当并发量太大时降级为应用级限流 ...

总结

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

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

Acho que você gosta

Origin juejin.im/post/7084606535573176351
Recomendado
Clasificación