Nginx 限流算法大揭秘

简介: Nginx 有多种限流算法....

博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌

Java知识图谱点击链接:体系化学习Java(Java面试专题)

💕💕 感兴趣的同学可以收藏关注下不然下次找不到哟💕💕

image.png

1、什么是 Nginx 限流算法

Nginx 有多种限流算法,以下是其中几种:

  1. 漏桶算法
    漏桶算法是一种比较简单的限流算法。它的原理是将请求放入一个固定容量的漏桶中,然后以固定的速率流出。当请求到来时,如果漏桶已满,则请求被拒绝。漏桶算法可以平滑限制请求的流量,但是对于突发流量无法应对。

  2. 令牌桶算法
    令牌桶算法也是一种常用的限流算法。它的原理是维护一个固定容量的令牌桶,每个请求需要消耗一个令牌才能被处理。当令牌桶中没有足够的令牌时,请求被拒绝。令牌桶算法可以应对突发流量,并且可以设置令牌桶的填充速率和最大容量。

  3. 计数器算法
    计数器算法是一种比较简单的限流算法。它的原理是在一定时间内统计请求的数量,如果超过了设定的阈值,则拒绝请求。计数器算法可以快速应对突发流量,但是对于长时间的流量控制不够精确。

  4. 滑动窗口算法
    滑动窗口算法是一种比较常用的限流算法。它的原理是维护一个固定大小的窗口,记录在窗口时间内的请求数量。当请求到来时,将其加入窗口,并计算窗口内的请求总数。如果请求总数超过了设定的阈值,则拒绝请求。滑动窗口算法可以平滑限制请求的流量,并且可以根据实际情况动态调整窗口大小。

2、漏桶算法

漏桶算法是一种常用的限流算法,它的原理是将请求放入一个固定容量的漏桶中,然后以固定的速率流出。当请求到来时,如果漏桶已满,则请求被拒绝。漏桶算法可以平滑限制请求的流量,但是对于突发流量无法应对。

漏桶算法的实现方式为,将请求放入一个固定容量的漏桶中,然后以固定的速率流出。当请求到来时,如果漏桶未满,则将请求放入漏桶中,否则拒绝请求。漏桶中的请求以固定的速率流出,可以平滑限制请求的流量。漏桶算法可以应对流量波动较小的情况,但是对于突发流量无法应对。

我们接下来用 java 实现一个漏桶算法,代码如下:

package com.pany.camp.limiting;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @description: 漏桶算法
 * @copyright: @Copyright (c) 2022
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0
 * @createTime: 2023-06-24 20:38
 */
public class LeakyBucket {
   
   
    private final long capacity; // 漏桶容量
    private final long rate;     // 漏水速率,单位:个/秒
    private AtomicLong water;    // 当前水量
    private long lastTime;       // 上一次漏水时间

    public LeakyBucket(long capacity, long rate) {
   
   
        this.capacity = capacity;
        this.rate = rate;
        this.water = new AtomicLong(0);
        this.lastTime = System.currentTimeMillis();
    }

    // 判断桶中是否能够容纳指定数量的水
    public synchronized boolean tryConsume(long num) {
   
   
        // 先漏水
        long now = System.currentTimeMillis();
        long leaked = (now - lastTime) * rate / 1000;
        if (leaked > 0) {
   
   
            water.set(Math.max(0, water.get() - leaked));
            lastTime = now;
        }
        // 再加水
        if (num <= capacity - water.get()) {
   
   
            water.addAndGet(num);
            return true;
        } else {
   
   
            return false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
   
   
        LeakyBucket bucket = new LeakyBucket(10, 2); // 漏桶容量为10,漏水速率为2个/秒
        for (int i = 0; i < 200; i++) {
   
    // 模拟200个请求
            if (bucket.tryConsume(1)) {
   
   
                System.out.println("处理请求 " + i);
            } else {
   
   
                System.out.println("请求 " + i + " 被限流");
            }
            TimeUnit.MILLISECONDS.sleep(100); // 每个请求间隔100毫秒
        }
    }
}

以下是上面的输出:

Connected to the target VM, address: '127.0.0.1:61851', transport: 'socket'
处理请求 0
处理请求 1
处理请求 2
处理请求 3
处理请求 4
处理请求 5
处理请求 6
处理请求 7
处理请求 8
处理请求 9
处理请求 10
处理请求 11
请求 12 被限流
请求 13 被限流
请求 14 被限流
处理请求 15
请求 16 被限流
请求 17 被限流
请求 18 被限流
请求 19 被限流
处理请求 20
.....

理解了漏桶算法之后呢,我们学习一下如何配置。
在 Nginx 中配置漏桶算法可以通过 ngx_http_limit_req_module模块实现。该模块可以限制并发请求的数量和请求的速率,从而防止服务器被过多的请求压垮。

以下是一个使用漏桶算法限制请求速率的 Nginx 配置示例:

http {
   
   
    limit_req_zone $binary_remote_addr zone=limit:10m rate=2r/s;
    server {
   
   
        listen 80;
        server_name example.com;
        location / {
   
   
            limit_req zone=limit burst=5;
            # 其他配置
        }
    }
}

在上面的配置中, limit_req_zone 指令用于定义一个名为 limit 的内存共享区域,用于存储客户端 IP 地址的请求信息。该指令的参数说明如下:

  • $binary_remote_addr :使用客户端 IP 地址作为键值。
  • zone=limit:10m :定义名为 limit 的内存共享区域,大小为 10MB。
  • rate=2r/s :设置漏桶的速率为 2 个请求/秒。

在 server 块中, listen 指令用于指定监听的端口和主机名, server_name 指令用于指定服务器的域名。 location 块用于指定请求的处理方式,其中的 limit_req 指令用于限制请求速率。该指令的参数说明如下:

  • zone=limit :使用名为 limit 的内存共享区域存储客户端请求信息。
  • burst=5 :设置漏桶的容量为 5 个请求。

在上面的配置中,如果客户端发送的请求速率超过了 2 个请求/秒,则会被限制,直到漏桶中的请求数量小于等于 5。

3、令牌桶算法

令牌桶算法是一种常用的流量控制算法,可以用于限制请求的速率。它的原理是通过令牌桶来控制请求的访问速率,每个请求需要消耗一个令牌,而令牌桶以一定的速率生成令牌,当令牌桶中没有令牌时,请求将被限制。

令牌桶算法中,令牌桶是一个固定容量的桶,其中包含一定数量的令牌。每个令牌代表一个请求的访问权限。令牌桶以一定的速率生成令牌,当请求到来时,如果令牌桶中有令牌,则允许请求访问,并消耗一个令牌;如果令牌桶中没有令牌,则请求被限制。

令牌桶算法可以用于限制请求的速率,防止服务器被过多的请求压垮。它也可以用于限制用户的访问频率,防止恶意用户对服务器进行攻击。

我们接下来用 java 实现一个令牌桶算法,代码如下:

package com.pany.camp.limiting;

import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * @description:  令牌桶算法
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-24 20:45
 */
public class TokenBucket {
   
   

    private final int capacity; // 令牌桶的容量
    private final int rate; // 令牌生成速率
    private final AtomicInteger tokens; // 当前令牌数
    private long timestamp; // 上次更新时间

    public TokenBucket(int capacity, int rate) {
   
   
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = new AtomicInteger(capacity);
        this.timestamp = System.currentTimeMillis();
    }

    public synchronized boolean acquire() {
   
   
        // 计算时间间隔
        long now = System.currentTimeMillis();
        long interval = now - timestamp;
        // 计算新生成的令牌数
        int newTokens = (int) (interval * rate / 1000);
        if (newTokens > 0) {
   
   
            tokens.set(Math.min(capacity, tokens.get() + newTokens));
            timestamp = now;
        }
        // 判断是否有足够的令牌
        if (tokens.get() > 0) {
   
   
            tokens.decrementAndGet();
            return true;
        } else {
   
   
            return false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
   
   
        TokenBucket bucket = new TokenBucket(10, 2); // 令牌桶容量为 10,令牌生成速率为 2 个/秒
        for (int i = 0; i < 200; i++) {
   
   
            Thread.sleep(100); // 每隔 100 毫秒发送一个请求
            if (bucket.acquire()) {
   
   
                System.out.println("请求 " + i + " 被处理");
            } else {
   
   
                System.out.println("请求 " + i + " 被限流");
            }
        }
    }
}

在上面的代码中, TokenBucket 类表示一个令牌桶,包含令牌桶的容量、令牌生成速率、当前令牌数和上次更新时间等属性。 acquire 方法用于获取令牌,如果令牌桶中有足够的令牌,则返回 true ,否则返回 false 。

在 acquire 方法中,首先计算时间间隔 interval ,然后根据时间间隔和令牌生成速率计算新生成的令牌数 newTokens 。如果新生成的令牌数大于 0,则更新令牌桶中的令牌数和上次更新时间。最后,判断令牌桶中是否有足够的令牌,如果有,则减少一个令牌并返回 true ,否则返回 false 。

以下是上面的输出:

Connected to the target VM, address: '127.0.0.1:62453', transport: 'socket'
请求 0 被处理
请求 1 被处理
请求 2 被处理
请求 3 被处理
请求 4 被处理
请求 5 被处理
请求 6 被处理
请求 7 被处理
请求 8 被处理
请求 9 被处理
请求 10 被处理
请求 11 被处理
请求 12 被限流
请求 13 被限流
请求 14 被处理
请求 15 被限流
请求 16 被限流
请求 17 被限流
请求 18 被限流
请求 19 被处理
请求 20 被限流
......

理解了漏桶算法之后呢,我们学习一下如何配置。

在 Nginx 中配置令牌桶算法可以使用 ngx_http_limit_req_module 模块。该模块提供了限制请求速率的功能,包括基于令牌桶算法的限速。

以下是一个简单的 Nginx 配置示例,使用令牌桶算法限制每个 IP 地址的请求速率为 10 个/秒:

http {
   
   
    limit_req_zone $binary_remote_addr zone=rate-limit:10m rate=10r/s;
    server {
   
   
        listen 80;
        server_name example.com;
        location / {
   
   
            limit_req zone=rate-limit burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

Nginx 中的令牌桶算法和漏桶算法在配置上非常相似,这是因为它们都是基于限速的算法,本质上都是通过控制请求的速率来保护服务器或应用程序。虽然它们在实现细节上有所不同,但在 Nginx 的实现中,它们都使用了 ngx_http_limit_req_module 模块,因此配置方式类似。

具体来说,两种算法的区别在于令牌桶算法是按照固定速率生成令牌,然后根据令牌数量来控制请求的速率;而漏桶算法是按照固定速率处理请求,然后根据漏桶中的水量来控制请求的速率。在 Nginx 中, ngx_http_limit_req_module 模块提供了 limit_req_zone 和 limit_req 两个指令,它们可以同时支持令牌桶算法和漏桶算法,只需要在配置中指定不同的参数即可。

4、计数器算法

计数器算法是一种常用的限速算法,它通过计数器来记录请求的数量,然后根据计数器的值来控制请求的速率。计数器算法的实现方式有很多种,其中比较常见的是以下两种:

  • 固定窗口计数器算法:将请求的数量累加到计数器中,然后每隔固定的时间窗口,将计数器清零。在每个时间窗口内,只允许处理一定数量的请求,超出限制的请求会被丢弃或延迟处理。
  • 滑动窗口计数器算法:将时间分为若干个时间段,然后分别记录每个时间段内的请求数量。在每个时间段内,只允许处理一定数量的请求,超出限制的请求会被丢弃或延迟处理。与固定窗口计数器算法相比,滑动窗口计数器算法可以更精确地控制请求的速率,但同时也需要更多的计算和存储资源。
    计数器算法可以应用于各种场景,例如限制 API 接口的请求速率、限制爬虫的访问频率等。它的实现简单,效率高,因此被广泛应用。

我们接下来用 java 实现一个计数器算法,代码如下:

package com.pany.camp.limiting;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @description:  固定窗口计数器算法
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-24 21:10
 */
public class FixedWindowCounter {
   
   
    private final int limit; // 限制数量
    private final long windowSize; // 窗口时间
    private final AtomicInteger counter; // 计数器
    private final Lock lock; // 锁对象
    private long lastUpdateTime; // 上次更新时间

    public FixedWindowCounter(int limit, long windowSize) {
   
   
        this.limit = limit;
        this.windowSize = windowSize;
        this.counter = new AtomicInteger(0);
        this.lock = new ReentrantLock();
        this.lastUpdateTime = System.currentTimeMillis();
    }

    public boolean tryAcquire() {
   
   
        lock.lock();
        try {
   
   
            long now = System.currentTimeMillis();
            if (now - lastUpdateTime >= windowSize) {
   
    // 窗口时间已过,重置计数器
                counter.set(0);
                lastUpdateTime = now;
            }
            if (counter.incrementAndGet() > limit) {
   
    // 超过限制数量,返回 false
                return false;
            }
            return true;
        } finally {
   
   
            lock.unlock();
        }
    }

    public static void main(String[] args) {
   
   
        FixedWindowCounter counter = new FixedWindowCounter(5, 1000);

        // 模拟在 1 秒内进行 10 次请求
        for (int i = 0; i < 10; i++) {
   
   
            if (counter.tryAcquire()) {
   
   
                System.out.println("请求 " + (i + 1) + " 被处理");
            } else {
   
   
                System.out.println("请求 " + (i + 1) + " 被限流");
            }
        }
    }
}

以下是上面的输出:

请求 1 被处理
请求 2 被处理
请求 3 被处理
请求 4 被处理
请求 5 被处理
请求 6 被限流
请求 7 被限流
请求 8 被限流
请求 9 被限流
请求 10 被限流

Process finished with exit code 0

理解了漏桶算法之后呢,我们学习一下如何配置。

  1. 在 http 块中使用 limit_req_zone 指令来定义限制区域,例如:
http {
   
   
    limit_req_zone $binary_remote_addr zone=rate-limit:10m rate=10r/s;
}
  1. 在 server 或 location 块中使用 limit_req 指令来应用 FixedWindowCounter 算法,例如:
server {
   
   
    listen 80;
    server_name example.com;
    location / {
   
   
        limit_req zone=rate-limit burst=20;
        proxy_pass http://backend;
    }
}

其中, rate-limit 表示应用的限制区域, burst 表示允许的突发请求数量。

这样配置后,Nginx 将会对每个 IP 地址在每秒钟最多处理 10 个请求,超过这个数量的请求将会被延迟或拒绝。如果在固定时间窗口内请求量超过了 burst 数量,则会触发限流,直到时间窗口结束。

需要注意的是,FixedWindowCounter 算法的时间窗口是固定的,如果请求量不均匀分布,则可能会出现一些请求在时间窗口开始时被限流,而在时间窗口结束时可以得到处理的情况。因此,如果需要更精确的限流算法,可以考虑使用 RollingWindowCounter 算法。

5、滑动窗口算法

滑动窗口算法是一种用于解决数据流或连续数据的问题的算法。它通常用于计算滑动时间窗口内的数据总和、平均值、最大值、最小值等统计信息,或者在滑动窗口内查找特定的数据子序列。滑动窗口算法的基本思想是维护一个固定大小的滑动窗口,该窗口在数据流中向右滑动,并在每个时间点计算窗口内的数据。具体来说,滑动窗口算法通常使用双指针来维护窗口的起始位置和结束位置,同时使用一个数据结构(如队列、栈、哈希表等)来维护窗口内的数据。在每个时间点,我们将窗口向右滑动一个单位,同时添加新的数据,并从窗口左侧删除旧的数据。这样,我们就可以在滑动窗口内快速地计算各种统计信息或查找特定的数据子序列。

滑动窗口算法通常用于处理实时数据流、日志数据、网络流量等场景,它具有时间复杂度低、空间复杂度小、实现简单等优点。常见的滑动窗口算法包括滑动时间窗口算法、滑动计数窗口算法、滑动平均窗口算法等。

我们接下来用 java 实现一个滑动窗口限流算法,代码如下:

package com.pany.camp.limiting;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 滑动窗口限流算法
 * @copyright: @Copyright (c) 2022
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0
 * @createTime: 2023-06-24 21:20
 */
public class SlidingWindow {
   
   
    private int windowSize; // 窗口大小
    private int threshold; // 阈值
    private int[] window; // 窗口数组
    private int index; // 窗口指针
    private int count; // 窗口内请求数量

    public SlidingWindow(int windowSize, int threshold) {
   
   
        this.windowSize = windowSize;
        this.threshold = threshold;
        this.window = new int[windowSize];
        this.index = 0;
        this.count = 0;
    }

    public synchronized boolean allowRequest() {
   
   
        long now = System.currentTimeMillis();
        int currentIndex = (int) (now / 1000 % windowSize);
        if (currentIndex != index) {
   
   
            count -= window[currentIndex];
            window[currentIndex] = 0;
            index = currentIndex;
        }
        if (count < threshold) {
   
   
            window[index]++;
            count++;
            return true;
        } else {
   
   
            return false;
        }
    }

    public static void main(String[] args) {
   
   
        SlidingWindow slidingWindow = new SlidingWindow(10, 5);
        for (int i = 0; i < 20; i++) {
   
   
            if (slidingWindow.allowRequest()) {
   
   
                System.out.println("请求 " + i + " 被处理");
            } else {
   
   
                System.out.println("请求 " + i + " 被限制");
            }
            try {
   
   
                Thread.sleep(100);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        }
    }
}

上面的代码中, SlidingWindow 类表示一个滑动窗口限流器, windowSize 表示窗口大小, threshold 表示阈值, window 表示窗口数组, index 表示窗口指针, count 表示窗口内请求数量。 allowRequest 方法用于判断是否允许进行新的请求,如果窗口内的请求数量小于阈值,就允许请求,并更新窗口内的请求数量和窗口指针;否则就拒绝请求。 main 方法是一个简单的测试方法,循环进行 20 次请求,并输出每个请求是否被允许。

以下是上面的输出:

请求 0 被处理
请求 1 被处理
请求 2 被处理
请求 3 被处理
请求 4 被处理
请求 5 被限制
请求 6 被限制
请求 7 被限制
请求 8 被限制
请求 9 被限制
请求 10 被限制
请求 11 被限制
请求 12 被限制
请求 13 被限制
请求 14 被限制
请求 15 被限制
请求 16 被限制
请求 17 被限制
请求 18 被限制
请求 19 被限制

Process finished with exit code 0

理解了漏桶算法之后呢,我们学习一下如何配置。

在 Nginx 中,可以使用 ngx_http_limit_req_module模块来实现滑动窗口限流算法。该模块可以基于客户端 IP 或请求参数等进行限流,并支持自定义限流阈值和窗口大小等参数。下面是一个简单的滑动窗口限流算法的 Nginx 配置示例,您可以参考一下:


http {
   
   
    limit_req_zone $binary_remote_addr zone=rate-limit:10m rate=10r/s;
    server {
   
   
        listen 80;
        server_name example.com;
        location / {
   
   
            limit_req zone=rate-limit burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

上面的配置中, limit_req_zone 指令用于定义限流区域,其中 $binary_remote_addr 表示使用客户端 IP 作为限流键, rate-limit 表示限流区域的名称, 10m 表示限流区域的大小为 10MB, 10r/s 表示限流速率为每秒 10 个请求。 limit_req 指令用于应用限流规则,其中 zone=rate-limit 表示使用名为 rate-limit 的限流区域, burst=20 表示突发请求的数量为 20 个, nodelay 表示不延迟请求,即直接拒绝超出限流阈值的请求。 proxy_pass 指令用于将请求转发到后端服务器。

需要注意的是,上面的示例中使用的是基于客户端 IP 的限流,如果您需要基于其他参数进行限流,可以将 $binary_remote_addr 替换为其他参数,例如 $http_user_agent 表示使用 User-Agent 作为限流键。另外,Nginx 的限流是针对整个服务器的,如果您需要对某个具体的 API 或接口进行限流,可以使用 location 指令来指定。

image.png

💕💕 本文由激流丶创作,原创不易,感谢支持!
💕💕喜欢的话记得点赞收藏啊!

目录
相关文章
|
2月前
|
负载均衡 算法 搜索推荐
Nginx 常用的负载均衡算法
【10月更文挑战第17天】在实际应用中,我们需要根据具体的情况来选择合适的负载均衡算法。同时,还可以结合其他的优化措施,如服务器健康检查、动态调整权重等,来进一步提高负载均衡的效果和系统的稳定性。
134 59
|
13天前
|
存储 应用服务中间件 nginx
Nginx限流怎么做
Nginx 通过 limit_req 模块实现限流,保护后端服务器。配置示例中,定义了限流区域 `limit`,每秒允许 10 个请求,客户端 IP 超过限流后允许 20 个突发请求,超出则立即返回 503 错误。
37 6
|
1月前
|
负载均衡 算法 应用服务中间件
Nginx 常用的负载均衡算法
【10月更文挑战第22天】不同的负载均衡算法各有特点和适用场景。在实际应用中,需要根据具体的业务需求、服务器性能和网络环境等因素来选择合适的算法。
61 3
|
2月前
|
缓存 负载均衡 算法
nginx学习:配置文件详解,负载均衡三种算法学习,上接nginx实操篇
Nginx 是一款高性能的 HTTP 和反向代理服务器,也是一个通用的 TCP/UDP 代理服务器,以及一个邮件代理服务器和通用的 HTTP 缓存服务器。
115 0
nginx学习:配置文件详解,负载均衡三种算法学习,上接nginx实操篇
|
4月前
|
算法 NoSQL Java
spring cloud的限流算法有哪些?
【8月更文挑战第18天】spring cloud的限流算法有哪些?
99 3
|
4月前
|
应用服务中间件 Linux nginx
高并发下Nginx配置限流
【8月更文挑战第16天】
81 1
|
5月前
|
监控 应用服务中间件 nginx
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
|
5月前
|
应用服务中间件 nginx 缓存
高并发架构设计三大利器:缓存、限流和降级问题之Nginx作为前置网关进行限流问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Nginx作为前置网关进行限流问题如何解决
|
5月前
|
存储 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
|
5月前
|
算法 Java 调度
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决