Java技术升华

Nginx与OpenResty接入层限流

2021-10-27  本文已影响0人  肥兔子爱豆畜子

本文来自于对nginx和openresty文档和网上文章的学习记录,非纯粹原创

一、nginx本身支持的限流功能

主要是依靠ngx_http_limit_req_modulengx_http_limit_conn_module两个模块中的,limit_req与limit_conn两组配置来实现rps与连接数两个维度的限流。

1、limit_req_zone与limit_req

示例:

limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=5r/s; #单ip每秒限制5个请求
limit_req_zone $server_name zone=perserver_rps:10m rate=3000r/s; #每个server每秒限制处理3000个请求

上面分别是按照ip和server来限流rps,zone=perip_rps:10m是设定这个limit_req_zone的名字为perid_rps,且在nginx内存里分配10m的空间来存储访问频次信息,rate=15r/s表示每秒15个请求,30r/m每分钟30次请求。

一般在http里配置好了limit_req_zone之后,就可以在server或者location里边配置limit_req了,比如:

limit_req zone=perserver_rps burst=2000 nodelay;  #server每秒请求限流
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个

limit_req模块使用的是漏桶算法。参考:http://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req

2、limit_conn_zone与limit_conn

limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
limit_conn perip_conn 10;   #每个ip最多允许10个连接

这俩一般用来控制单客户端ip可以连多少连接到nginx上。总的连接一般就直接在nginx.conf里配置worker_connections 1024;来限制住了。

附nginx.conf配置文件片段:

http {
    include       mime.types;
    default_type  application/octet-stream;
    
    #nginx限流配置
    #每秒请求数
    limit_req_log_level error;
    limit_req_status 503;
    limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=15r/s; #单ip每秒限制15个请求
    limit_req_zone $server_name zone=perserver_rps:10m rate=1500r/s; #每个server每秒限制处理1500个请求
    
    #连接数限制
    limit_conn_log_level error;
    limit_conn_status 503;
    limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
    limit_conn_zone $server_name zone=perserver_conn:10m;
    
    upstream seckillcore {
        server 127.0.0.1:8080;
    }
    
    server {
        listen       80;
        server_name  localhost;

        #开发调试模式、关闭lua代码缓存,生产环境请勿关闭
        lua_code_cache off;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
        
        limit_req zone=perserver_rps burst=10 nodelay;  #server每秒请求限流

        location / {
            root   html;
            index  index.html index.htm;
        }
        
        #预约接口
        location /seckill/rest/appointment {
            limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
            limit_conn perip_conn 10;   #每个ip最多允许10个连接
            
            default_type text/html;
            access_by_lua_file lua/wangan/seckill/appointment_check.lua;
            proxy_pass http://seckillcore;
            proxy_redirect default;
        }
       
        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }

}

参考:https://blog.csdn.net/myle69/article/details/83512617

二、openresty限流功能

lua-resty-limit-traffic库中的resty.limit.count模块、resty.limit.conn模块、resty.limit.req模块。

我们先从源码入手:

1、count.lua 限制单位时间内的请求数(请求速率)

每隔window时间在dict里放一个key、到时间自动失效、失效时间也是window。限制啥要看key按照什么去设置,比如每个ip设置一个key就是限制ip的单位时间请求数,key对应的value记的是ip1在这个时间window所允许的请求数limit,每来一个请求则-1,如果减没了就503拒绝。有点像弱化版的令牌桶算法,只不过没有按照一定速率添加令牌这个操作罢了。

关键代码:

--incoming
remaining, err = dict:incr(key, -1, limit)
ok, err = dict:expire(key, window)

2、conn.lua 限制连接数或者也可以说是并发数,标准ngx_limit_conn模块的lua增强版

key设置ip1的话,那么就是ip1来一个请求,在access阶段去调用income(),则value从0开始 +1,加到max则拒绝,同时在log阶段去调leave(),value -1。所以在某一时刻看过去,key=ip1的value里边记着的就是此时的并发连接数。

这里要结合两个执行阶段access和log分别调用income和leave去理解,笔者一开始没绕过这个弯儿,明明conn说是限制连接数的,为啥统计的是请求,连接复用的情况下不是统计请求要比连接多?就是因为没有注意到leave,实际上每个请求结束都会减1,这样动态的来看某一时刻value里的值就是这个ip到openresty的连接数、因为这+1和-1使得每个连接在某一个时刻只会有一个请求计数。

关键代码:

--incoming
conn, err = dict:incr(key, 1, 0)

        if conn > max + self.burst then
            conn, err = dict:incr(key, -1)
            if not conn then
                return nil, err
            end
            return nil, "rejected"
        end

--leaving
local conn, err = dict:incr(key, -1)

上面为了说明方便,假设了burst=0,也没讨论根据delay(也就是>max但是<max+burst这部分连接)如何sleep进行限制连接处理,以及如何在log阶段的leaving里边自动修正delay的逻辑。代码如下:

--incoming
if conn > max then  --conn介于max和max + burst之间
        -- make the excessive connections wait
        -- unit_delay相当于是个预估的请求处理时长的基准值
        return self.unit_delay * floor((conn - 1) / max), conn
end

--leaving
-- req_latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
-- 即实际请求处理时长 - limit:incoming(key, true)
function _M.leaving(self, key, req_latency)
    assert(key)
    local dict = self.dict

    local conn, err = dict:incr(key, -1)
    if not conn then
        return nil, err
    end

    if req_latency then
        local unit_delay = self.unit_delay
        self.unit_delay = (req_latency + unit_delay) / 2
    end

    return conn
end

unit_delay = (req_latency + unit_delay) / 2 修正基准时间

req_latency = request_time - limit_conn_delay 实际处理时间与sleep时间的差值

limit_conn_delay = unit_delay * floor((conn - 1) / max) 需要sleep的时间

参考:https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/lua-limit.html

3、req.lua 标准ngx_limit_req模块的lua接口

使用这个模块可以实现使用漏桶和令牌桶算法来平滑的限制请求rps

参考:https://segmentfault.com/a/1190000022585978 《接入层限流之OpenResty提供的Lua限流模块lua-resty-limit-traffic》

req.lua里边比上面的conn和count要复杂一些,核心代码如下:

ffi.cdef[[
    struct lua_resty_limit_req_rec {
        unsigned long        excess;
        uint64_t             last;  /* time in milliseconds */
        /* integer value, 1 corresponds to 0.001 r/s */
    };
]]
local const_rec_ptr_type = ffi.typeof("const struct lua_resty_limit_req_rec*")
local rec_size = ffi.sizeof("struct lua_resty_limit_req_rec")

-- we can share the cdata here since we only need it temporarily for
-- serialization inside the shared dict:
local rec_cdata = ffi.new("struct lua_resty_limit_req_rec")

function _M.new(dict_name, rate, burst)
    local dict = ngx_shared[dict_name]
    if not dict then
        return nil, "shared dict not found"
    end

    assert(rate > 0 and burst >= 0)

    local self = {
        dict = dict,
        rate = rate * 1000,
        burst = burst * 1000,
    }

    return setmetatable(self, mt)
end

function _M.incoming(self, key, commit)
    local dict = self.dict
    local rate = self.rate
    local now = ngx_now() * 1000 --时间戳,ms

    local excess

    -- it's important to anchor the string value for the read-only pointer
    -- cdata:
    local v = dict:get(key)
    if v then
        if type(v) ~= "string" or #v ~= rec_size then
            return nil, "shdict abused by other users"
        end
        local rec = ffi_cast(const_rec_ptr_type, v)
        local elapsed = now - tonumber(rec.last)  --过了多少ms了

        -- print("elapsed: ", elapsed, "ms")

        -- we do not handle changing rate values specifically. the excess value
        -- can get automatically adjusted by the following formula with new rate
        -- values rather quickly anyway.
        --[[
            我们不专门处理变化的速率值rate。因为剩余值excess可以通过以下公式自动调整,并使用新的速率值,调整速度相当快。
            上一次剩余值 - 这段时间可以处理的数量 + 1000
        ]]
        excess = max( tonumber(rec.excess) - rate * abs(elapsed) / 1000 + 1000,
                     0 )

        -- print("excess: ", excess)

        if excess > self.burst then
            return nil, "rejected"
        end

    else
        excess = 0
    end

    if commit then
        rec_cdata.excess = excess
        rec_cdata.last = now
        dict:set(key, ffi_str(rec_cdata, rec_size))
    end

    -- return the delay in seconds, as well as excess
    -- 剩余除以速率就是延迟时间
    return excess / rate, excess / 1000
end

大致思路就是逐个请求去判断,根据至上一个请求到此刻经过的时间和rate,可以计算出这个时间段允许通过的请求。如果小于burst则返回延迟,否则拒绝。

设置burst = 0,漏桶容量0,漏不过去的直接拒绝

local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 2, 0) -- rate = 2r/s, burst = 0
local delay, err = lim:incoming(key, true)

漏桶算法:设置burst = 100,漏桶容量100, 超过容量的拒绝,没超过的就计算一下延迟,然后ngx.sleep(delay)控制一下请求的流入速度。

local lim, err = limit_req.new("my_limit_req_store", 2, 60)
local delay, err = lim:incoming(key, true)

if delay >= 0.001 then
   ngx.sleep(delay)
end

令牌桶算法:设置burst=100,桶容量100,超过容量拒绝,没超过的话可以一次放过去,nodelay,也就是允许一定的突发流量。这个时候就是令牌桶算法的思路:桶里100个令牌,来的请求拿1个令牌通过,没令牌拿了则拒绝。

local lim, err = limit_req.new("my_limit_req_store", 2, 100)
local delay, err = lim:incoming(key, true)

if delay >= 0.001 then
   -- 令牌桶就这里直接放到后端服务器,不做sleep延迟处理了
   -- ngx.sleep(delay) 
end

其实nginx的ngx_http_limit_req_module 这个模块中的delay和nodelay也就是类似此处对桶中请求是否做延迟处理的两种方案,也就是分别对应的漏桶和令牌桶两种算法。

三、总结:

我们需要:

  1. 单ip需要限制连接数、以及rps ,防止恶意请求脚本来刷服务器。
#http
limit_req_zone $binary_remote_addr zone=perip_rps:10m rate=5r/s; #单ip每秒限制5个请求
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
#location
limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
limit_conn perip_conn 5;    #每个ip最多允许同时5个连接
  1. 重点location接口使用漏桶或令牌桶平滑限制rps,保护后端的核心服务。
#http
limit_req_zone $server_name zone=perserver_rps:10m rate=1500r/s; #每个server每秒限制处理1500个请求
#server
limit_req zone=perserver_rps burst=100 nodelay;  #server每秒请求限流

或者使用openresty:

location /seckill/rest/appointment {
    limit_req zone=perip_rps burst=10 nodelay; #每个ip每秒请求如果超过limit_req_zone的配置,最多可以缓冲10个
    limit_conn perip_conn 5;    #每个ip最多允许5个连接
            
    default_type text/html;
    # 在access阶段使用resty.limit.req做令牌桶或者漏桶限流
    access_by_lua_file lua/wangan/seckill/appointment_check.lua;
    proxy_pass http://seckillcore;
    proxy_redirect default;
    # log_by_lua_file src/log.lua; # 如果是漏桶那么在log阶段ngx.sleep(delay),如果是令牌桶则不需要
}

ps:学习了openresty限流之后对nginx的限流原理也理解更深入了,但是笔者没认识到什么场景非用openresty限流替换nginx限流不可。。。

上一篇下一篇

猜你喜欢

热点阅读