zoukankan      html  css  js  c++  java
  • 浅谈限流(下)实战

    常见的应用限流手段

    应用开发中常见的限流的都有哪些呢?其实常用的限流手段都比较简单,关键都是限流服务的高并发。为了在LB上实现高效且有效的限流,普遍的做法都是Nginx+Lua或者Nginx+Redis去实现服务服务限流,所以市面上比较常用的waf框架都是基于Openresty去实现的。我们看下比较常用的几个限流方式。

    Openresty+共享内存实现的计数限流

    先看下代码限流代码

    lua_shared_dict limit_counter 10m;
    server {
    listen 80;
    server_name www.test.com;
    location / {
    root html;
    index index.html index.htm;
    }
    
    location /test {
    access_by_lua_block {
    local function countLimit()
    local limit_counter =ngx.shared.limit_counter
    local key = ngx.var.remote_addr .. ngx.var.http_user_agent .. ngx.var.uri .. ngx.var.host
    local md5Key = ngx.md5(key)
    local limit = 10
    local exp = 300
    local current =limit_counter:get(key)
    if current ~= nil and current + 1> limit then
    return 1
    end
    if current == nil then
    limit_counter:add(key, 1, exp)
    else
    limit_counter:incr(key, 1)
    end
    return 0
    end
    
    local ret = countLimit()
    if ret > 0 then
    ngx.exit(405)
    end
    }
    content_by_lua 'ngx.say(111)';
    }
    }
    

    解释下上面这段简单的代码,对于相同的IP UA HOST URI组合的唯一KEY,就是同一个URI每个用户在5分钟内只允许有10次请求,如果超过10次请求,就返回405的状态码,如果小于10次,就继续执行后面的处理阶段。
    看下访问结果

    curlhttp://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    <html>
    <head><title>405 Not Allowed</title></head>
    <body bgcolor="white">
    <center><h1>405 Not Allowed</h1></center>
    <hr><center>openresty/1.13.6.2</center>
    </body>
    </html>
    

    这就是一个简单的计数限流的例子

    Openresty 限制连接数和请求数的模块

    限制连接数和请求数的模块是 lua-resty-limit-traffic。它的限速实现基于以前说过的漏桶原理。
    蓄水池一边注水一边放水的问题。 这里注水的速度是新增请求/连接的速度,而放水的速度则是配置的限制速度。 当注水速度快于放水速度(表现为池中出现蓄水),则返回一个数值 delay。调用者通过 ngx.sleep(delay) 来减慢注水的速度。 当蓄水池满时(表现为当前请求/连接数超过设置的 burst 值),则返回错误信息 rejected。调用者需要丢掉溢出来的这部份。
    看下配置代码

    http {
    lua_shared_dict my_req_store 100m;
    lua_shared_dict my_conn_store 100m;
    
    server {
    location / {
    access_by_lua_block {
    local limit_conn = require "resty.limit.conn"
    local limit_req = require "resty.limit.req"
    local limit_traffic = require "resty.limit.traffic"
    
    local lim1, err = limit_req.new("my_req_store", 300, 150)
    --300r/s的频率,大于300小于450就延迟大概0.5秒,超过450的请求就返回503错误码
    local lim2, err = limit_req.new("my_req_store", 200, 100)
    local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
    --1000c/s的频率,大于1000小于2000就延迟大概1s,超过2000的连接就返回503的错误码,估算每个连接的时间大概是0.5秒,
    local limiters = {lim1, lim2, lim3}
    
    local host = ngx.var.host
    local client = ngx.var.binary_remote_addr
    local keys = {host, client, client}
    
    local states = {}
    local delay, err = limit_traffic.combine(limiters, keys, states)
    if not delay then
    if err == "rejected" then
    return ngx.exit(503)
    end
    ngx.log(ngx.ERR, "failed to limit traffic: ", err)
    return ngx.exit(500)
    end
    
    if lim3:is_committed() then
    local ctx = ngx.ctx
    ctx.limit_conn = lim3
    ctx.limit_conn_key = keys[3]
    end
    
    print("sleeping ", delay, " sec, states: ",
    table.concat(states, ", "))
    
    if delay >= 0.001 then
    ngx.sleep(delay)
    end
    }
    log_by_lua_block {
    local ctx = ngx.ctx
    local lim = ctx.limit_conn
    if lim then
    local latency = tonumber(ngx.var.request_time)
    local key = ctx.limit_conn_key
    local conn, err = lim:leaving(key, latency)
    if not conn then
    ngx.log(ngx.ERR,
    "failed to record the connection leaving ",
    "request: ", err)
    return
    end
    end
    }
    }
    }
    }
    

    简单的注释可以介绍它大概的参数说明了。具体的可以参看下官方文档
    https://github.com/openresty/lua-resty-limit-traffic
    注意下,连接数限流在log阶段有个leaving()的调用来动态调整请求时间。不要忘记leaving的调用
    用了这么长时间了没感觉有啥需要注意的坑。就是测试的时候,要测出效果,需要ngx.sleep下,否则,简单的程序,没任何压力,Nginx都能执行完,不会有延迟。所以需要测试延迟的时候 content阶段做下sleep,就能测到效果了。

    Openresty 共享内存 动态限流

    我们的使用的过程中发现,攻击或者流量打过来的时候我通常的流程都是:先通过日志服务发现有流量,然后在查询攻击的IP 或者UID,最后再封禁这些IP或者UID。一直是滞后的。我们应该做的是,在流量进来的时候通过动态分析直接拦截,而不是滞后拦截,滞后拦截有可能服务都被流量打死了。
    动态限流是基于前面的技术限流的。

    lua_shared_dict limit_counter 10m;
    server {
    listen 80;
    server_name www.test.com;
    
     
    
    location / {
    root html;
    index index.html index.htm;
    }
    
    location /test {
    access_by_lua_block {
    local function countLimit()
    local limit_counter =ngx.shared.limit_counter
    local key = ngx.var.remote_addr .. ngx.var.http_user_agent .. ngx.var.uri .. ngx.var.host
    local md5Key = ngx.md5(key)
    local limit = 5
    local exp = 120
    local disable = 7200
    local disableKey = md5Key .. ":disable"
    local disableRt = limit_counter:get(disableKey)
    if disableRt then
    return 1
    end
    local current =limit_counter:get(key)
    if current ~= nil and current + 1> limit then
    dict:set(disableKey, 1, disable)
    return 1
    end
    if current == nil then
    limit_counter:add(key, 1, exp)
    else
    limit_counter:incr(key, 1)
    end
    return 0
    end
    
    local ret = countLimit()
    if ret > 0 then
    ngx.exit(405)
    end
    }
    content_by_lua 'ngx.say(111)';
    }
    }
    

    看下这行结果

    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    111
    curl http://www.test.com/test
    <html>
    <head><title>500 Internal Server Error</title></head>
    <body bgcolor="white">
    <center><h1>500 Internal Server Error</h1></center>
    <hr><center>openresty/1.13.6.2</center>
    </body>
    </html>
    

    大致的思路比较简单,一旦发现请求触发阀值(2分钟5次),直接将请求的唯一值放到黑名单2个小时,以后的请求一旦发现在黑名单里面,就直接返回503。如果没有触发阀值,那就给请求的唯一值加1,这个计数器的过期时间是2分钟,过了两分钟就会重新计数。基本满足了我们目前当前的动态限流。

    最后

    我目前工作中比较常见的限流方式就上面三种,第二种是oenresty官方的模块,已经能够满足绝大多数限流需求,达到保护服务的目的。简单的限流控制利用openresty+shared.DICT很容易实现,把shared.DICT换成Redis就可以实现分布式限流。当然了,市场上已经有了很多特别优秀的开源的网关服务框架包含了waf的功能,使用比较多的比如kong、orange,已经有很多巨头公司在使用了,最近比较热门的apisix等等。如果有这方面需求的话可以关注下。

    浅谈限流(上)

  • 相关阅读:
    BZOJ.1468.Tree(点分治)
    BZOJ.1935.[SHOI2007]Tree园丁的烦恼(CDQ分治 三维偏序)
    BZOJ.4319.[cerc2008]Suffix reconstruction(后缀数组 构造 贪心)
    BZOJ.3262.陌上花开([模板]CDQ分治 三维偏序)
    洛谷.3374.[模板]树状数组1(CDQ分治)
    BZOJ.4566.[HAOI2016]找相同字符(后缀数组 单调栈)
    POJ.3145.Common Substrings(后缀数组 倍增 单调栈)
    POJ.2774.Long Long Message/SPOJ.1811.LCS(后缀数组 倍增)
    POJ.1743.Musical Theme(后缀数组 倍增 二分 / 后缀自动机)
    UOJ.35.[模板]后缀排序(后缀数组 倍增)
  • 原文地址:https://www.cnblogs.com/feixiangmanon/p/11495314.html
Copyright © 2011-2022 走看看