如何利用nginx+lua限流

一、限流是什么

举个例子

奶茶店A开在大街上,没有门店,于是想喝奶茶的人把这家奶茶店围得水泄不通,顾客明明下了单,但是依然拿不到货,奶茶店制造奶茶的速度远远不能满足顾客的需要,于是愤怒的顾客把奶茶店挤爆了。

奶茶店B开在大街上,但是有门店,顾客只能从入口进来,所以奶茶店在有序的接待顾客,双方一手交钱一手交货,有序和谐。

在上述例子中,门店大门就是限流器。

在web服务器中,限流就是是限制服务器资源被外界连接访问

二、为什么要限流

服务器同时能处理的请求数有限,如果请求量特别大,系统资源不足以满足所有请求时,我们为了保证资源能够正常服务,就需要按照预设的规则进行流量限制或功能限制。

三、限流工具

工具: 采用OpenResty® + Redis

语言: Lua

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

我们可以在openResty中编写Lua脚本来控制流量

四、限流算法

1、计数器算法

假如限流qps为100,从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。

这样会有产生在0.99秒的时候突然来了100个请求,然后在1秒结束的时候计数归零,在1.01秒的时候又来了100个请求,这样会造成在短时间内qps超出预期的情况,可能会造成宕机。

2、漏桶算法

漏桶可以看作是一个带有常量服务时间的单服务器队列,请求存放在桶中,如果漏桶(包缓存)溢出,那么数据包会被丢弃。

在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。

3、令牌桶算法

令牌桶的桶是用来存放token的,token会以固定的速度产生,加入桶中满了,则不再产生。当请求到达时,消耗令牌,当桶中没有令牌时则讲请求存放在队列中等待处理或者抛弃

令牌桶算法是对漏桶算法的一种改进,令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。

五、openResty + Lua使用令牌桶算法实现限流

在openResty中,可以运行Lua脚本,,通过Lua脚本来编写算法达到限流目的
先来说一下Nginx lua 模块指令,Nginx共11个处理阶段,而相应的处理阶段是可以做插入式处理,即可插拔式架构;另外指令可以在http、server、location几个范围进行配置:

指令 所处处理阶段 使用范围 解释
init_by_lua loading-config http nginx Master进程加载配置时执行;通常用于初始化全局配置/预加载Lua模块
init_by_lua_file loading-config http nginx Master进程加载配置时执行;通常用于初始化全局配置/预加载Lua模块
init_worker_by_lua starting-worker http 每个Nginx Worker进程启动时调用的计时器,如果Master进程不允许则只会在init_by_lua之后调用;通常用于定时拉取配置/数据,或者后端服务的健康检查
init_worker_by_lua_file starting-worker http 每个Nginx Worker进程启动时调用的计时器,如果Master进程不允许则只会在init_by_lua之后调用;通常用于定时拉取配置/数据,或者后端服务的健康检查
set_by_lua rewrite server,location 设置nginx变量,可以实现复杂的赋值逻辑;此处是阻塞的,Lua代码要做到非常快
set_by_lua_file rewrite server,location 设置nginx变量,可以实现复杂的赋值逻辑;此处是阻塞的,Lua代码要做到非常快
rewrite_by_lua rewrite tail http,server,location rewrite阶段处理,可以实现复杂的转发/重定向逻辑
rewrite_by_lua_file rewrite tail http,server,location rewrite阶段处理,可以实现复杂的转发/重定向逻辑
access_by_lua access tail http,server,location 请求访问阶段处理,用于访问控制
access_by_lua_file access tail http,server,location 请求访问阶段处理,用于访问控制
content_by_lua content location 内容处理器,接收请求处理并输出响应
content_by_lua_file content location 内容处理器,接收请求处理并输出响应
header_filter_by_lua output-header-filter http,server,location 设置header和cookie
header_filter_by_lua_file output-header-filter http,server,location 设置header和cookie
body_filter_by_lua output-body-filter http,server,location 对响应数据进行过滤,比如截断、替换。
body_filter_by_lua_file output-body-filter http,server,location 对响应数据进行过滤,比如截断、替换。
log_by_lua log http,server,location log阶段处理,比如记录访问量/统计平均响应时间
log_by_lua_file log http,server,location log阶段处理,比如记录访问量/统计平均响应时间

在http代码块中引入Lua脚本

// nginx.conf
http {
    ...
    init_worker_by_lua_file lua/generate_token.lua;
    access_by_lua_file lua/check_token.lua;
    ...
}

定时生成令牌,当

// generate_token.lua
if ngx.worker.id() == 0 then -- 只在第一个woker里自动生成,防止重复生成令牌
    local delay = 1    -- 延迟时间
    local redis = require "resty.redis"
    local handler

    handler = function()
        local red = redis:new()
        red:set_timeout(1000) -- 1秒
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "Failed to connect: ", err)·
            return
        end
        local num = tonumber(red:get("token"))
        if num == nil then
            red:set("token", 50)
        end
        if num ~= nil and num < 50 then
            red:set("token", 50)
        end
    end

    local ok, err = ngx.timer.every(delay, handler)
    if not ok then
        ngx.log(ngx.ERR, "failed to create the timer:", err)
        return
    end
end

当请求到达时判断桶里有没有令牌

local request_uri = ngx.var.request_uri
if request_uri ~= '/favicon.ico' then -- 网页图标会消耗一次令牌,需要避免
    local ip = ngx.req.get_headers()["X-Real-IP"]
    if ip == nil then
        ip = ngx.req.get_headers()["x_forwarded_for"]
    end
    if ip == nil then
        ip = ngx.var.remote_addr
    end

    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000) -- 一秒

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "Failed to connect: ", err)
        return
    end

    local token_num = tonumber(red:get("token")) -- 获取令牌

    if token_num ~= nil and token_num == 0 then -- 判断令牌是否足够
        ngx.log(ngx.ERR, "Not token")
        return
    end

    local freq = "freq:" .. ip -- 采用ip作为身份标识

    local freq_num = tonumber(red:get(freq))

    if freq_num == nil then
        red:set(freq, 0)
        red:expire(freq, 3)
        red:decr("token")
        return
    end

    if freq_num ~= nil and freq_num > 10 then
        ngx.log(ngx.ERR, "The most frequently visited: ", freq_num)
        return
    end

    red:incr(freq)
    red:decr("token")
end

Comments

No Data
Total 0
  • 1