Limitación de corriente del sistema de alta concurrencia (2)

introducir

En el artículo anterior, presentamos el método de limitación de corriente a nivel de aplicación, pero no podemos realizar una limitación de corriente completa. En este artículo, presentaremos la limitación de corriente distribuida y la limitación de corriente de la capa de acceso que puede realizar la limitación de corriente global .

Limitación de corriente de la capa de acceso

La limitación actual de la capa de acceso generalmente se refiere a la entrada de tráfico de solicitudes. Los propósitos principales son: balanceo de carga, filtrado de solicitudes ilegales, agregación de solicitudes, almacenamiento en caché, limitación actual, pruebas A/B y monitoreo de la calidad del servicio. Por lo general , se utiliza la limitación actual de la capa de acceso de Nginx (OpenResty) .

Se pueden usar dos módulos que vienen con nginx: el módulo limitador de corriente del número de conexión ngx_http_limit_conn_module y el módulo limitador de corriente de solicitud ngx_http_limit_req_module implementado por el algoritmo de cubeta con fugas .

ngx_http_limit_conn_módulo

limit_conn es para limitar el número total de conexiones de red correspondientes a una clave determinada. El número total de conexiones se puede limitar según la IP o el nombre de dominio del servicio . El contador no contará todas las conexiones de solicitud, solo contará la conexión de solicitud procesada por nginx y el encabezado de solicitud completo que se haya leído . El ejemplo de configuración es el siguiente:

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

El proceso principal de ejecución es el siguiente:

未命名文件 (34).png

limit_conn puede limitar el número total concurrente/número de solicitud de una clave, y la clave se puede cambiar según sea necesario.

ngx_http_limit_req_módulo

limit_req es la implementación del algoritmo de cubeta con fugas, que se utiliza para especificar la solicitud correspondiente a la clave para limitar la corriente.

Veamos un ejemplo de configuración:

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

El proceso de ejecución es el siguiente:

1. Solicitud de entrada Primero juzgue si la hora de la última solicitud en relación con la hora actual (la primera vez es 0) necesita limitar la corriente , si necesita limitar la corriente, vaya al paso 2, de lo contrario, vaya al paso 3

2. Si la capacidad del depósito no está configurada (ráfaga = 0), las solicitudes se procesan a una tasa fija. Si la solicitud es limitada, se devolverá directamente el código de error correspondiente (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.可以当并发量太大时降级为应用级限流 ...

总结

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

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

Supongo que te gusta

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