Saltar a contenido

limit-rate: módulo Lua para limitar la tasa de solicitudes para nginx-module-lua, utilizando el método "token bucket"

Instalación

Si no has configurado la suscripción al repositorio RPM, regístrate. Luego puedes proceder con los siguientes pasos.

CentOS/RHEL 7 o Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-limit-rate

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-limit-rate

Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.

Este documento describe lua-resty-limit-rate v0.1 lanzado el 25 de octubre de 2018.


http {
    lua_shared_dict my_limit_rate_store 100m;
    lua_shared_dict my_locks 100k;

    server {
        location / {
            access_by_lua_block {
                local limit_rate = require "resty.limit.rate"

                local lim, err = limit_rate.new("my_limit_rate_store", 500, 10, 3, 200, {
                    lock_enable = true, -- usar lua-resty-lock
                    locks_shdict_name = "my_locks",
                })

                if not lim then
                    ngx.log(ngx.ERR,
                            "falló al instanciar un objeto resty.limit.rate: ", err)
                    return ngx.exit(500)
                end

                -- la siguiente llamada debe ser por solicitud.
                -- aquí usamos la dirección remota (IP) como la clave de limitación
                local key = ngx.var.binary_remote_addr
                local delay, err = lim:incoming(key, true)
                -- local delay, err = lim:take(key, 1, true)
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "falló al tomar el token: ", err)
                    return ngx.exit(500)
                end

                if delay >= 0.001 then
                    -- el  valor de retorno contiene el número actual de tokens disponibles
                    -- de solicitudes para la clave especificada
                    local avail = err

                    ngx.sleep(delay)
                end
            }

            # el manejador de contenido va aquí. si es content_by_lua, entonces puedes
            # fusionar el código Lua anterior en access_by_lua en el manejador de
            # content_by_lua para ahorrar un poco de tiempo de CPU.
        }

        location /take_available {
            access_by_lua_block {
                local limit_rate = require "resty.limit.rate"

                -- global 20r/s 6000r/5m
                local lim_global = limit_rate.new("my_limit_rate_store", 100, 6000, 2, nil, {
                    lock_enable = true,
                    locks_shdict_name = "my_locks",
                })

                if not lim_global then
                    return ngx.exit(500)
                end

                -- single 2r/s 600r/5m
                local lim_single = limit_rate.new("my_limit_rate_store", 500, 600, 1, nil, {
                    locks_shdict_name = "my_locks",
                })

                if not lim_single then
                    return ngx.exit(500)
                end

                local t0, err = lim_global:take_available("__global__", 1)
                if not t0 then
                    ngx.log(ngx.ERR, "falló al tomar global: ", err)
                    return ngx.exit(500)
                end

                -- aquí usamos el userid como la clave de limitación
                local key = ngx.var.arg_userid or "__single__"

                local t1, err = lim_single:take_available(key, 1)
                if not t1 then
                    ngx.log(ngx.ERR, "falló al tomar single: ", err)
                    return ngx.exit(500)
                end

                if t0 == 1 then
                    return -- el bucket global no tiene hambre
                else
                    if t1 == 1 then
                        return -- el bucket single no tiene hambre
                    else
                        return ngx.exit(503)
                    end
                end
            }
        }
    }
}

Descripción

Este módulo proporciona APIs para ayudar a los programadores usuarios de OpenResty/ngx_lua a limitar la tasa de solicitudes utilizando el método "token bucket".

Si deseas usar múltiples instancias diferentes de esta clase a la vez o usar una instancia de esta clase con instancias de otras clases (como resty.limit.conn), entonces debes usar el módulo resty.limit.traffic para combinarlos.

La principal diferencia entre este módulo y resty.limit.req:

  • resty.limit.req limita la tasa de solicitudes utilizando el método "leaky bucket", este módulo utiliza el método "token bucket".

La principal diferencia entre este módulo y resty.limit.count:

  • resty.limit.count ofrece un modelo mental sencillo que limita la tasa de solicitudes por un número fijo de solicitudes en una ventana de tiempo dada, pero a veces puede dejar pasar el doble del número de solicitudes permitidas por minuto. Por ejemplo, si nuestro límite de tasa fuera de 10 solicitudes por minuto y un usuario hiciera 10 solicitudes a las 10:00:59, podría hacer 10 solicitudes más a las 10:01:00 porque un nuevo contador comienza al inicio de cada minuto. En este caso, este módulo puede controlar de manera más precisa y fluida.

Métodos

new

sintaxis: obj, err = class.new(shdict_name, interval, capacity, quantum?, max_wait?, opts?)

Instancia un objeto de esta clase. El valor class es devuelto por la llamada require "resty.limit.rate".

El método devuelve un nuevo token bucket que se llena a la tasa de quantum número de tokens cada interval, hasta la capacidad máxima dada capacity. El bucket está inicialmente lleno.

Este método toma los siguientes argumentos y una tabla de opciones opcional opts:

  • shdict_name es el nombre de la zona shm lua_shared_dict.

    Es una buena práctica usar zonas shm separadas para diferentes tipos de limitadores.

  • interval es el tiempo que pasa entre la adición de tokens, en milisegundos.

  • capacity es el número máximo de tokens que se pueden mantener en el bucket.

  • quantum es el número de tokens que se agregarán al bucket en un intervalo, este argumento es opcional, por defecto 1.

  • max_wait es el tiempo máximo que esperaríamos para que se agreguen suficientes tokens, en milisegundos, este argumento es opcional, por defecto nil, lo que significa infinito.

La tabla de opciones acepta las siguientes opciones:

  • lock_enable Cuando está habilitado, la actualización del estado de shdict a través de múltiples procesos de trabajo de nginx es atómica; de lo contrario, habrá una ventana de condición de carrera (pequeña) entre el comportamiento de "leer-y-luego-escribir", por defecto false. Consulta lua-resty-lock para más detalles.

  • locks_shdict_name Especifica el nombre del diccionario compartido (creado por lua_shared_dict) para el bloqueo, por defecto locks.

En caso de fallo, este método devuelve nil y una cadena que describe el error (como un nombre de lua_shared_dict incorrecto).

incoming

sintaxis: delay, err = obj:take(key, commit)

Dispara un nuevo evento de solicitud entrante y calcula el retraso necesario (si lo hay) para la solicitud actual sobre la clave especificada o si el usuario debería rechazarla inmediatamente.

Similar al método take, pero este método solo toma un token del bucket a la vez.

Este método acepta los siguientes argumentos:

  • key es la clave especificada por el usuario para limitar la tasa.

    Ten en cuenta que este módulo no antepone ni añade sufijos a la clave del usuario, por lo que es responsabilidad del usuario asegurarse de que la clave sea única en la zona shm de lua_shared_dict.

  • commit es un valor booleano. Si se establece en true, el objeto realmente registrará el evento en la zona shm que respalda el objeto actual; de lo contrario, solo sería una "prueba" (que es el valor predeterminado).

set_max_wait

sintaxis: obj:set_max_wait(max_wait?)

Sobrescribe el umbral max_wait como se especificó en el método new.

take

sintaxis: delay, err = obj:take(key, count, commit)

El método toma count tokens del bucket sin bloquear.

Este método acepta los siguientes argumentos:

  • key es la clave especificada por el usuario para limitar la tasa.

    Ten en cuenta que este módulo no antepone ni añade sufijos a la clave del usuario, por lo que es responsabilidad del usuario asegurarse de que la clave sea única en la zona shm de lua_shared_dict.

  • count es el número de tokens a eliminar.

  • commit es un valor booleano. Si se establece en true, el objeto realmente registrará el evento en la zona shm que respalda el objeto actual; de lo contrario, solo sería una "prueba" (que es el valor predeterminado).

Los valores de retorno dependen de los siguientes casos:

  1. Si el valor max_wait especificado en el método new o set_max_wait, el método solo tomará tokens del bucket si el tiempo de espera para los tokens no es mayor que max_wait, y devuelve el tiempo que el llamador debería esperar hasta que los tokens estén realmente disponibles, de lo contrario, devuelve nil y la cadena de error "rejected".

  2. Si el valor max_wait es nil, devuelve el tiempo que el llamador debería esperar hasta que los tokens estén realmente disponibles.

Además, este método también devuelve un segundo valor de retorno que indica el número de tokens disponibles en este momento.

Si ocurrió un error (como fallos al acceder a la zona shm de lua_shared_dict que respalda el objeto actual), entonces este método devuelve nil y una cadena que describe el error.

Este método nunca duerme por sí mismo. Simplemente devuelve un retraso si es necesario y requiere que el llamador invoque más tarde el método ngx.sleep para dormir.

take_available

sintaxis: count, err = obj:take_available(key, count)

El método toma hasta count tokens disponibles inmediatamente del bucket. Devuelve el número de tokens eliminados, o cero si no hay tokens disponibles. No bloquea.

Este método acepta los siguientes argumentos:

  • key es la clave especificada por el usuario para limitar la tasa.

    Ten en cuenta que este módulo no antepone ni añade sufijos a la clave del usuario, por lo que es responsabilidad del usuario asegurarse de que la clave sea única en la zona shm de lua_shared_dict.

  • count es el número de tokens a eliminar.

Si ocurrió un error (como fallos al acceder a la zona shm de lua_shared_dict que respalda el objeto actual), entonces este método devuelve nil y una cadena que describe el error.

uncommit

sintaxis: ok, err = obj:uncommit(key)

Esto intenta deshacer el compromiso de la llamada incoming. Esto es simplemente una aproximación y debe usarse con cuidado. Este método se utiliza principalmente en el módulo Lua resty.limit.traffic al combinar múltiples limitadores al mismo tiempo.

Granularidad de Limitación

La limitación funciona en la granularidad de una instancia individual de servidor NGINX (incluyendo todos sus procesos de trabajo). Gracias al mecanismo shm; podemos compartir el estado de manera económica entre todos los trabajadores en una sola instancia de servidor NGINX.

Ver También

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-limit-rate.