8.Nginx的请求限制( limit_conn_zone、 limit_conn、limit_req_zone、limit_req zone)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8.Nginx的请求限制( limit_conn_zone、 limit_conn、limit_req_zone、limit_req zone)相关的知识,希望对你有一定的参考价值。

参考技术A 在配置nginx的过程中我们需要考虑受到攻击或恶意请求的情况,比如单用户恶意发起大量请求,这时Nginx的请求限制可以帮助我们对其进行限制。

连接频率限制 : limit_conn_module

请求频率限制 : limit_req_module

理解:连接频率限制和请求频率限制都可以实现Nginx的请求限制 , 但是他们的实现原理是不一样的 , 区别就在于连接和请求上 , http协议的链接与请求 , http协议是建立在tcp协议之上的,要完成一次http的请求,先要进行tcp的3次握手建立http的连接 , 然后才进行http的request和response(请求和响应) , 现在http1.1以上的版本已经可以实现一次建立http的连接进行多次的http的request和response(请求和响应) ,最后客户端和服务端不断的来发送FIN包和ACK包来保持HTTP的连接 。

如果面对抢购和秒杀需求来限制 , 个人觉得连接频率限制和请求频率限制应该配合使用 , 使用连接频率限制同一IP同时只能有3个连接, 再使用请求频率限制对于同一ip的请求,限制平均速率为5个请求/秒 , 这样是不是比单独只使用一种限制要好很多?

比如只使用连接频率限制 , 由于一次建立http的连接可以进行多次的请求和响应 , 我们无法精确的限制同一ip同时发起多少次的http请求 ;

比如只使用请求频率限制 , 可以精确的限制同一ip1秒只能发起5次的http请求 , 假如同一ip1秒内发起了100000次请求 , 虽然限制了只有5次成功响应 , 但是其他的99995次的请求TCP握手建立http连接是不是会消耗服务器资源?

所以,个人觉得连接频率限制和请求频率限制应该配合使用!

HTTP请求建立在一次TCP连接基础上

一次TCP连接至少产生一次HTTP请求

ngx_http_limit_conn_module模块用于限制每个定义键的连接数,特别是来自单个IP地址的连接数。

未开启 连接限制 时做个压力测试 (不懂ab的可以看看https://www.cnblogs.com/TingJie/articles/4974885.html这个文章 , 很简单明了)

这里模拟了10万个请求 , 1000个并发 , 78个请求失败,打开nginx的错误日志查看都是打开文件失败的错误 ( open() "/usr/share/nginx/html/50x.html" failed (24: Too many open files))! 想象一下 , 如果我们是一个商城程序的API接口 , 正常情况下 , 同一个IP下10万个请求1000个并发 , 算不算恶意攻击?那么就需要做一下连接限制了噻 , 具体怎么限制根绝具体的逻辑去处理 , 我们这里简单的限制一下(启用配置示例的连接限制)再次做个压力测试:

开启连接限制对单个IP限制同时只能存在一个连接,这里模拟了10万个请求 , 1000个并发 , 43616个请求失败,打开nginx的错误日志查看下错误全是连接限制的作用(limiting connections by zone "conn_zone") , 我们知道"现在http1.1以上的版本已经可以实现一次建立http的连接进行多次的http的request和response(请求和响应) " , 大家想想 , 如果我们需要做一个抢购和秒杀 , 是不是需要对单个抢购和秒杀限制连接单个IP同时只能存在一个或者多个连接的限制?不然人家写个脚本程序运行在十台八台的机器上疯狂的请求怎么办?当然这只是一个比较简单的应用场景 , 更多的还是需要自己思考与摸索.

ngx_http_limit_req_module模块用于限制请求的处理速率,特别是单一的IP地址的请求的处理速率。使用“漏桶”方法进行限制。

语法: limit_req_zone  key zone=name:size rate=rate;

只能在 http 块中使用

此指令用于声明请求限制 zone , zone 可以保存各种 key 的状态, name 是 zone 的唯一标识, size 代表 zone 的内存大小, rate 指定速率限制。

参数详解:

1.key ,

若客户的请求匹配了key,则进入 zone 。可以是文本、变量,通常为Nginx变量。

如 $binary_remote_addr (客户的ip), $uri( 不带参数的请求地址 ) , $request_uri( 带参数的请求地址 ) , $server_name (服务器名称) 。

支持组合使用,使用空格隔开。

2.zone

使用zone=test,指定此zone的名字为test。

3.size

在zone=name后面紧跟 :size ,指定此 zone 的内存大小。如zone=name: 10m ,代表name的共享内存大小为10m。通常情况下,1m可以保存16000个状态。

4.rate

使用rate=1r/s,限制 平均 1秒不超过1个请求。使用rate=1r/m,限制 平均 1分钟不超过1个请求。

例子:

同一ip 不同 请求地址,进入名为 one 的zone,限制速率为 5请求/秒 。

同一ip 同一 请求地址,进入名为 two 的zone,限制速率为 1请求/秒 。

语法: limit_req  zone=name [burst=number] [nodelay];

可在 http, server, location 块中使用

此指令用于设置共享的内存 zone 和最大的突发请求大小。

若请求速率超过了 limit_req_zone 中指定的 rate 但小于 limit_req 中的 burst ,则进行延迟处理,若再超过 burst ,就可以通过设置nodelay对其进行丢弃处理。

参数详解:

1.zone

使用 zone=name 指定使用名为 name 的 zone ,这个 zone 之前使用 limit_req_zone 声明过。

2.burst(可选)

burst用于指定最大突发请求数。许多场景下,单一地限制rate并不能满足需求,设置 burst ,可以延迟处理超过rate限制的请求。

3.nodelay(可选)

如果设置了 nodelay ,在突发请求数大于 burst 时,会丢弃掉这部分请求。因为如果只是延迟处理,就像” 漏斗 “,一旦上面加得快( 请求 ),下面漏的慢( 处理速度 ),” 漏斗 “总会有溢出的时候。这时,丢弃掉溢出的部分就显得很有意义了。

单客户分为三种情况:

请求速率 < rate(1r/s) ,正常处理

rate(1r/s) < 请求速率 < burst(5r/s) ,大于rate部分延迟

burst(5r/s)  < 请求速率 ,大于burst部分丢弃(返回503服务暂时不可用)

未开启请求限制时做个压力测试 (不懂ab的可以看看https://www.cnblogs.com/TingJie/articles/4974885.html这个文章 , 很简单明了)

这里模拟了10万个请求 , 1000个并发 , 全部请求成功! 想想一下 , 如果我们是一个商城程序的API接口 , 正常情况下,同一个IP下10万个请求算不算恶意攻击?那么就需要做一下请求限制了噻 , 具体怎么限制根绝具体的逻辑去处理 , 我们这里简单的限制一下:

同一ip请求,进入名为req_zone的zone,限制速率为20次请求/秒,

超过部分进行延迟处理,若超过10个请求/秒,丢弃超过部分。

修改了配置之后平滑重启一下nginx -s reload , 再次使用之前的参数做个压力测试看看

同一IP发起了请求10万次, nginx只接受处理了100次,是不是nginx的压力一下子就小了

简易先进先出队列-自用

简易先进先出队列-自用

/**
 * _______________*********_______________________
 * ______________************_____________________
 * ______________*************____________________
 * _____________**__***********___________________
 * ____________***__******_*****__________________
 * ____________***_*******___****_________________
 * ___________***__**********_****________________
 * __________****__***********_****_______________
 * ________*****___***********__*****_____________
 * _______******___***_********___*****___________
 * _______*****___***___********___******_________
 * ______******___***__***********___******_______
 * _____******___****_**************__******______
 * ____*******__*********************_*******_____
 * ____*******__******************************____
 * ___*******__******_*****************_*******___
 * ___*******__******_******_*********___******___
 * ___*******____**__******___******_____******___
 * ___*******________******____*****_____*****____
 * ____******________*****_____*****_____****_____
 * _____*****________****______*****_____***______
 * ______*****______;***________***______*________
 * ________**_______****________****______________
 *
 * @author 闫影 - yanying876@gmail.com
 * @Package user
 * @date 2020/6/1916:53
 */
public class QueueY<T> {

    // 队列最多容纳数量 初始化的时候可以根据自己时间情况设置的相对大一些 总归是空间换时间
    // 考虑 队列慢的情况下可以使用线程让其代替等待
    private int maxSize;   
    private Object[] queueArray;
    // 队头
    private int front;  
    // 队尾
    private int rear;	
    private int size;

    public QueueY(int length) {
        maxSize = length;
        queueArray = new Object[maxSize];
        front = 0;
        rear = -1;
        size = 0;
    }

    /**	入队: 先将rear(队为指针) 加1, 后将数据项存入rear的位置。
     *	当rear 指向maxSize -1 的位置时,将rear 设置为-1(循环队列),加1 后存入数据项。
     */
    public void enQueue(T str){

        // 入队之前先检查队列是否已满,已满则抛出异常。
        if(isFull()){    
            //这里是抛出了一个异常 根据时间使用情况这里可以另做处理 
            throw new RuntimeException("队列已满," + str + " 不能入队!");
        }

        if(rear == maxSize -1){
            rear = -1;
        }
        queueArray[++rear] = str;  // 先将 rear 加1,后取值
        size++;
    }


    /**出队: 先取出front 的值,然后将front 减1
     * 如果 front 超过了数组的顶端,将 front 设置为 0(循环队列)
     */
    @SuppressWarnings("unchecked")
    public T deQueue(){
		// 出队之前先检查队列是否为空。
        // 这里根据实际情况可做另外处理 比如等待 返回特定值
        if(isEmpty()){   
            System.out.printf("队列为空,不能出队!");
            return null;
        }

        T str = (T) queueArray[front++];  // 先去 queueArray[front] 的值,后将front 加1
        if(front == maxSize){
            front = 0;
        }
        size--;
        return str;
    }

    /**查看对头数据项
     */
    @SuppressWarnings("unchecked")
    public T peek(){
        // 查看队头时,判断是否为空, 为空则抛出异常。
        // 此处根据实际情况可以单独处理
        if(isEmpty()){   
            throw new RuntimeException("队列为空!");
        }
        return (T) queueArray[front];
    }

    /** 判断队列是否为空。队空: rear + 1 = front 或 front + maxSize -1 = rear
     *  通过数组容量比队列数据项的最大值大一,来区分对空和对满。
     */
    public boolean isEmpty(){
        return (rear + 1 == front || front + maxSize -1 == rear);
    }

    /**判断队列是否为满。 队满: rear + 2 = front 或 front + maxSize -2 = rear
     * 通过数组容量比队列数据项的最大值大一,来区分对空和对满。
     */
    public boolean isFull(){
        return (rear + 2 == front || front + maxSize -2 == rear);
    }

    /** 获取队列的大小
     */
    public int queueSize(){
	/* 可以通过队头队尾计算出队列的大小,也可以通过一个计数器,当入队是加1,出队是减1.
	 if(rear >= front){
			return rear - front +1;
		}else {
			return maxSize - front + (rear + 1);
		}
	*/
        return size;
    }

//    public static void main(String[] args) {
//        Queue<String> queue = new Queue<>(5);
//        queue.enQueue("a");
//        queue.enQueue("b");
//        queue.enQueue("c");
//        queue.enQueue("d");
//        queue.deQueue();
//        queue.deQueue();
//
//        System.out.println("队列是否为空: " + queue.isEmpty() + "  队列是否满: " + queue.isFull());
//        System.out.println("队列大小:" + queue.queueSize());
//
//        int size = queue.queueSize();
//        for(int i = 0; i < size; i++){
//            String str = queue.deQueue();
//            System.out.print(str + " ");
//        }
//
//    }

以上是关于8.Nginx的请求限制( limit_conn_zone、 limit_conn、limit_req_zone、limit_req zone)的主要内容,如果未能解决你的问题,请参考以下文章

Nginx限制并发连接数

nginx的preaccess 阶段的limit_req模块与limit_conn模块

nginx v1.1.8新语法 limit_conn_zone 替换和 limit_conn 用法

nginx重新整理——————http请求的11个阶段中的preaccess[十四]

关于limit_req和limit_conn的区别

Nginx常见配置说明