进阶Openresty高级功能之限流

2024-04-15 15:19:14 浏览数 (3)

Openresty可以在Nginx的基础上搭配lua脚本实现更多高级功能,比如限流、缓存、非法URI拦截等功能。

限流

流量限制主要包括限频和限流:

  • 限频,限制单位时间内调用次数,关注调用速度
  • 限流,限制时间窗口内调用次数,关注调用总量

限流分为按请求量限流和连接数限流,可以在nginx.conf中配置。Nginx本身自带了限流功能,Openresty也实现了灵活的限流功能。

Nginx限流模块

limit_req_zone 是 Nginx 的模块之一,用于实现请求限流功能。它是 Nginx 自带的功能,不依赖于 OpenResty。

默认情况下,Nginx 使用 "leaky bucket"(漏桶)算法来进行请求限流。

漏桶算法是一种经典的请求限流算法,它基于一个类似于漏桶的数据结构。请求以固定的速率进入漏桶,如果漏桶已满,则请求被丢弃或延迟处理。这种算法可以平滑地限制请求的速率,防止服务器过载。

可以使用 limit_req_zone 指令来定义请求限流的区域,并使用 limit_req 指令来应用限流策略。

简单的示例如下:

代码语言:lua复制
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
    server {
        listen 8080;
        location / {
            limit_req zone=perip burst=5;
            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

上述limit_req_zone 定义了一个名为perip的存储区,大小为10MB,用于存储每个IP的请求状态。$binary_remote_addr变量表示客户端的IP地址。rate=100r/m表示限制每个IP每分钟只能发送100个请求。

代码语言:lua复制
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;

limit_req 配置应用了上面limit_req_zone 定义的限制。zone=perip指定了使用哪个存储区,burst=5表示允许短时间内的请求超过限制,最多超过5个。如果超过这个数量,那么超出的请求将会被延迟处理,直到请求速率降到限制以下。如果设置nodelay选项,那么超出burst数量的请求将会被直接拒绝。

代码语言:javascript复制
limit_req zone=perip burst=5;

当然也可以参照上述按请求数限流的方式设置按连接数限流。

代码语言:javascript复制
limit_conn_zone $binary_remote_addr zone=concurrent:10m;

limit_conn concurrent 5;

Openresty限流模块

OpenResty官方提供的几个Lua扩展:

  • resty.limit.req, 用于限制单位时间(秒)的请求数
  • resty.limit.conn, 用于限制并发连接数
  • resty.limit.count, 用于限制时间窗口内的请求数量限制,时间窗口可自定义
  • resty.limit.traffic, 用于对三者进行组合,以实现更丰富的限制策略

Openresty中的逻辑实现采用lua脚本,在location中可以使用access_by_lua_block 代码块来实现逻辑,也可以把逻辑独立到其它文件,然后通过access_by_lua_file xxx.lua 来引用即可。

限制时间窗内请求数

如下是每60秒只能请求100次的限流示例:

代码语言:lua复制
worker_processes  1;
error_log logs/error.log info;   
events {
    worker_connections 1024;
}
http {
    # 声明一个Hash缓存对象
    lua_shared_dict limit_count_store 100m;

    server {
        location / {
            access_by_lua_block {
                -- 引入请求限流模块
                local limit_count = require "resty.limit.count"
                
                -- 60秒内只能调用100次,将统计信息保存到缓存
                local lim, err = limit_count.new("limit_count_store", 100, 60)
                if not lim then
                    ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err)
                    return ngx.exit(500)
                end
                
                -- 将客户端的IP地址作为key来统计请求次数
                local key = ngx.var.binary_remote_addr
                
                local delay, err = lim:incoming(key, true)
                -- 如果请求数在限制范围内,则当前请求被处理的延迟和将被处理的请求的剩余数,delay返回0, err返回剩余数
                -- 如果请求被拒绝,delay返回nil, err返回rejected
                ngx.log(ngx.INFO, "请求剩余数:", err);
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "failed to limit count: ", err)
                    return ngx.exit(500)
                end
            }   

            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

限制IP的并发连接数

以下是限制每个客户端最大1个并发请求。

代码语言:lua复制
worker_processes  1;
error_log logs/error.log info;   
events {
    worker_connections 1024;
}
http {
    # 声明一个Hash缓存对象
    lua_shared_dict limit_count_store 100m;

    server {
        location / {
            access_by_lua_block {
               local limit_conn = require "resty.limit.conn"
               -- 限制一个 ip 客户端最大 1 个并发请求
               -- burst 设置为 0,如果超过最大的并发请求数,则直接返回503,
               -- 如果此处要允许突增的并发数,可以修改 burst 的值(漏桶的桶容量)
               -- 最后一个参数其实是你要预估这些并发(或者说单个请求)要处理多久,以便于对桶里面的请求应用漏桶算法
               -- 如果设置local lim, err = limit_conn.new("limit_conn_store", 50, 25, 0.5)
               -- 则表示限制50个并发请求,和一个25个并发额外的突发请求,  也就是一个客户端访问50  25 次之后就会抛出503
               local lim, err = limit_conn.new("limit_conn_store", 1, 0, 0.5)              
               if not lim then
                   ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", err)
                   return ngx.exit(500)
               endlocal key = ngx.var.binary_remote_addr
               -- commit 为true 代表要更新shared dict中key的值,-- false 代表只是查看当前请求要处理的延时情况和前面还未被处理的请求数local delay, err = lim:incoming(key, true)
               if not delay thenif err == "rejected" thenreturn ngx.exit(503)
                   end
                   ngx.log(ngx.ERR, "failed to limit req: ", err)
                   return ngx.exit(500)
               end-- 如果请求连接计数等信息被加到shared dict中,则在ctx中记录下,-- 因为后面要告知连接断开,以处理其他连接if lim:is_committed() thenlocal ctx = ngx.ctx
                   ctx.limit_conn = lim
                   ctx.limit_conn_key = key
                   ctx.limit_conn_delay = delay
               endlocal conn = err
               -- 其实这里的 delay 肯定是上面说的并发处理时间的整数倍,-- 举个例子,每秒处理100并发,桶容量200个,当同时来500个并发,则200个拒掉-- 100个正在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.5秒处理,-- 101-200个则应该延后0.5*2=1秒处理(0.5是上面预估的并发处理时间)-- ngx.say("delay: ", delay)if delay >= 0.001 then-- ngx.sleep(delay)end
            }

            log_by_lua_block {
               local ctx = ngx.ctx
               local lim = ctx.limit_conn
               if lim thenlocal key = ctx.limit_conn_key
                   -- 这个连接处理完后应该告知一下,更新shared dict中的值,让后续连接可以接入进来处理-- 此处可以动态更新你之前的预估时间,但是别忘了把limit_conn.new这个方法抽出去写,-- 要不每次请求进来又会重置local conn, err = lim:leaving(key, 0.5)
                   if not conn then
                       ngx.log(ngx.ERR,
                               "failed to record the connection leaving ",
                               "request: ", err)
                       returnendend
            }

            default_type text/html;
            content_by_lua_block {
                ngx.say("<p>hello, world</p>")
            }
        }
    }
}

其它的限频和限流方式也是类似的处理方式。

0 人点赞