美文网首页Java技术升华
Nginx与OpenResty接入层限流

Nginx与OpenResty接入层限流

作者: 肥兔子爱豆畜子 | 来源:发表于2021-10-27 14:56 被阅读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),如果是令牌桶则不需要
    }
    
    • nginx自带的限流功能不需要代码开发,只在nginx.conf配置就可以了。可以做到按ip或按server来限流rps和连接数。

    • openresty使用resty.limit库进行lua开发,也可以按ip和server来限流rps、连接数。

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

    相关文章

      网友评论

        本文标题:Nginx与OpenResty接入层限流

        本文链接:https://www.haomeiwen.com/subject/flcpaltx.html