草庐IT

高并发系统设计之限流

booksea 2023-03-28 原文

本文已收录至Github,推荐阅读 ? Java随想录

微信公众号:Java随想录

CSDN: 码农BookSea

这篇文章来讲讲限流,在高并发系统中限流是必不可少的,限流可以保证一部分的请求得到正常的响应,是一种自我保护的措施。限流可以保证使用有限的资源提供最大化的服务能力,按照预期流量提供服务,超过的部分将会拒绝服务、排队或等待、降级等处理。

首先,先来了解下几种限流算法。

限流算法

计数器算法

计数器算法是限流算法里最简单也是最容易实现的一种算法。

举个例子:我们规定接口A在1分钟内访问次数不能超过1000个。我们可以设置一个计数器,对固定时间窗口1分钟进行计数,每有一个请求,计数器就+1,如果请求数超过了阈值,则舍弃该请求,当时间窗口结束时,重置计数器为0。

计数器算法虽然简单,但是有一个十分致命的问题,那就是临界问题。

假设有一个用户,他在0:59时,瞬间发送了1000个请求,并且1:01又发送了1000个请求,那么其实用户在 2秒里面,瞬间发送了2000个请求。用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能利用这个漏洞卡Bug,瞬间压垮我们的应用。

缺点:没有办法防止时间范围临界点突发大流量,很可能在时间范围交界处被大量请求直接打到降级,影响后续服务

滑动窗口

滑动窗口算法解决了上诉计数器算法的缺点。计数器的时间窗口是固定的,而滑动窗口的时间窗口是动态的。

整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器,比如当一个请求在0:35秒的时候到达,那么0:30~0:39对应的计数器就会加1。

那么滑动窗口怎么解决刚才的临界问题的呢?我们可以看上图,0:59到达的1000个请求会落在灰色的格子中,而1:01到达的请求会落在橘黄色的格子中。当时间到达1:00时,我们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是2000个,超过了限定的1000个,所以此时能够检测出来触发限流。

当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确

缺点:滑动窗口无法平滑控制请求流量,仅能控制时间段内请求总量,宏观来看,时间轴上的请求数量波形可能出现较大的波动

漏桶算法

说到漏桶算法的时候,我们脑中先构思出一幅图:一个水桶,桶底下有一个小孔,水以固定的频率流出,水龙头以任意速率流入水,当水超过桶则”溢出“

漏桶算法的话保证了固定的流出速率,这是漏桶算法的优点,也可以说是缺点。始终恒定的处理速率有时候并不一定是好事情,对于突发的请求洪峰,在保证服务安全的前提下,应该尽最大努力去响应,这个时候漏桶算法显得有些呆滞,最终可能导致水位”溢出“,请求被丢弃。

缺点:无法应对突发流量,由于处理速度恒定,当大量请求到来时,用户等待时间长,用户体验差

令牌桶算法

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。

缺点:令牌桶的数量,生成的速度需要根据以往的系统性能以及用户习惯等经验的累积来判断,实际限流数难以预知

限流算法实现

Guava RateLimiter实现限流

引入依赖

<dependency>
	<groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

下面是一个使用的简单例子:

import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterTest {
    public static void main(String[] args) {
        RateLimiter rateLimiter = RateLimiter.create(1); //创建一个每秒产生一个令牌的令牌桶
        for (int i = 1; i <= 5; i++) {
            double waitTime = rateLimiter.acquire(i); //一次获取i个令牌
            System.out.println("acquire:" + i + " waitTime:" + waitTime);
        }

    }
}

结果:
    
acquire:1 waitTime:0.0
acquire:2 waitTime:0.995081
acquire:3 waitTime:1.998054
acquire:4 waitTime:2.999351
acquire:5 waitTime:3.999224

可以发现等待时间差不多间隔都是1秒。

RateLimiter是个抽象类,子类SmoothRateLimiter又做了层抽象,SmoothRateLimiter有两个子类SmoothBursty和SmoothWarmingUp。

  1. SmoothBursty: 令牌的生成速度恒定。使用 RateLimiter.create(double permitsPerSecond) 创建的是 SmoothBursty 实例。
  2. SmoothWarmingUp:令牌的生成速度持续提升,直到达到一个稳定的值。WarmingUp,顾名思义就是有一个热身的过程。使用 RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 时创建就是 SmoothWarmingUp 实例,其中 warmupPeriod 就是热身达到稳定速度的时间。

SmoothWarmingUp可以理解为是进阶版的SmoothBursty

令牌预分配

RateLimiter 使用令牌桶算法,会进行令牌的累积,令牌会被预先分配。

public class RateLimiterTest {
    public static void main(String[] args) {
        RateLimiter r = RateLimiter.create(5);
        while (true) {
            System.out.println("get 5 tokens: " + r.acquire(5) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("end");
            /**
             * output:
             * get 5 tokens: 0.0s
             * get 1 tokens: 0.996766s 滞后效应,需要替前一个请求进行等待
             * get 1 tokens: 0.194007s
             * get 1 tokens: 0.196267s
             * end
             * get 5 tokens: 0.195756s
             * get 1 tokens: 0.995625s 滞后效应,需要替前一个请求进行等待
             * get 1 tokens: 0.194603s
             * get 1 tokens: 0.196866s
             */
        }
    }
}

RateLimiter 由于会累积令牌,所以可以应对突发流量。有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。 RateLimiter 在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。

预热限流

RateLimiter 的 SmoothWarmingUp 是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。

public class RateLimiterTest {
    public static void main(String[] args) {
        RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
        while (true) {
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");
            System.out.println("end");
            /**
             * output:
             * get 1 tokens: 0.0s
             * get 1 tokens: 1.329289s
             * get 1 tokens: 0.994375s
             * get 1 tokens: 0.662888s  上边三次获取的时间相加正好为3秒
             * end
             * get 1 tokens: 0.49764s  正常速率0.5秒一个令牌
             * get 1 tokens: 0.497828s
             * get 1 tokens: 0.49449s
             * get 1 tokens: 0.497522s
             */
        }
    }
}

创建一个平均分发令牌速率为2,预热期为3秒。令牌桶一开始并不会0.5秒发一个令牌,而是频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。

介绍几个重要的参数

abstract class SmoothRateLimiter extends RateLimiter {
	//当前存储令牌数
    double storedPermits;
    //最大存储令牌数
    double maxPermits;
    //添加令牌时间间隔
    double stableIntervalMicros;
    private long nextFreeTicketMicros;
}

通过Debug我们可以看到,SmoothBursty方法的最大令牌数被设置成了,maxBurstSeconds 乘以 permitsPerSecond,而maxBurstSeconds默认是1。

而 SmoothWarmingUp最大令牌数的计算方法要复杂的多。

Nginx 限流

对于Nginx接入层限流可以使用 Nginx自带的两个模块:连接数限流模块ngx_http _limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module

limit_conn 用来对某个key对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。limit_req用来对某个key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay)和允许突发模式(nodelay)。

limit_conn

limit_conn是对某个key对应的总的网络连接数进行限流。可以按照IP来限制IP维度的总连接数,或者按照服务域名来限制某个域名的总连接数。但是,记住不是每个请求连接都会被计数器统计,只有那些被Nginx处理的且已经读取了整个请求头的请求连接才会被计数器统计。

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn_log_level error;
    limit_conn_status 503 ;

    server {
        location /limit {
            limit_conn addr l;
        }
    }
}
  • limit_conn:要配置存放key和计数器的共享内存区域和指定key的最大连接数。此处指定的最大连接数是1,表示Nginx最多同时并发处理1个连接,addr就是限流key,对应上文 zone=addr。
  • limit_conn_zone:用来配置限流key及存放key对应信息的共享内存区域大小。此处的key是"$binary_remote_addr",表示IP地址,也可以使用server_name作为key来限制域名级别的最大连接数。
  • limit_conn_status:配置被限流后返回的状态码,默认返回503。
  • limit_conn_log_level:配置记录被限流后的日志级别,默认error级别。

limit_req

limit_req 是漏桶算法实现,用于对指定key 对应的请求进行限流,比如,按照 IP维度限制请求速率。配置示例如下:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    limit_conn_log_level error;
    limit_conn_status 503;

    server {
        location /limit {
            limit_req zone=one burst=20 nodelay;
        }
    }
}

limit_req 和 limit_conn 的配置类似。

  • limit_req:配置限流区域,上面的参数会让nginx 每个IP一秒钟只处理一个请求。
  • burst: burst 参数定义了超出 zone 指定速率的情况下,客户端还能发起多少请求,超出速率的请求将会被放入队列,我们将队列大小设置为20。这意味着,如果从一个给定 IP 地址发送 21 个请求,Nginx 会立即将第一个请求发送到上游服务器群,然后将余下 20 个请求放在队列中。然后每1秒转发一个排队的请求,只有当传入请求使队列中排队的请求数超过 20 时,Nginx 才会向客户端返回 503。
  • nodelay:配置 burst 参数将会使通讯更流畅,但是可能会不太实用,因为该配置会使站点看起来很慢。在上面的示例中,队列中的第 20 个包需要等待 20 秒才能被转发,此时返回给客户端的响应可能不再有用。要解决这个情况,可以在 burst 参数后添加 nodelay 参数。使用 nodelay 参数,当一个请求到达“太早”时,只要在队列中能分配位置,Nginx 将立即转发这个请求。将队列中的该位置标为”taken”(占据),并且不会被释放以供另一个请求使用,直到一段时间后才会被释放。假设如前所述,队列中有 20 个空位,从给定的 IP 地址发出的 21 个请求同时到达。Ngin x会立即转发这个 21 个请求,并且标记队列中占据的 20 个位置,然后每 1秒释放一个位置。如果是25个请求同时到达,Nginx 将会立即转发其中的 21 个请求,标记队列中占据的 20 个位置,并且返回 503 状态码来拒绝剩下的 4 个请求。如果希望不限制两个请求间允许间隔的情况下实施“流量限制”,nodelay 参数是很实用的。
  • limit_req_zone:配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处指定的key是“$binary_remote_addr”,表示IP地址。10m表示共享内存的大小,16000 个 IP 地址的状态信息,大约需要 1MB,所以示例中区域可以存储 160000 个 IP 地址
  • limit_conn_status:配置被限流后返回的状态码,默认返回503。
  • limit_conn_log_level:配置记录被限流后的日志级别,默认级别为error。

黑白名单限流

geo $limit {
    default         1;
    10.0.0.0/8      0;
    192.168.0.0/64  0;
}
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;
    }
}

geo 指令将给在白名单中的 IP 地址对应的"$limit" 变量分配一个值 0,给其它不在白名单中的分配一个值 1。然后我们使用一个映射将这些值转为 key。

白名单内 IP 地址的"$limit_key"变量被赋值为空字符串,不在白名单内的被赋值为客户端的 IP 地址。当limit_req_zone后的第一个参数是空字符串时,不会应用“流量限制”,所以白名单内的 IP 地址不会被限制。其它所有 IP 地址都会被限制到每秒 5 个请求。

而要做出网站黑名单,就有可能要屏蔽一堆ip,但是如果将其放在nginx.conf文件夹下,既不美观,也不利于管理,因此需要单独写出一个conf文件,然后在nginx.conf中使用 include标签引用它。

如果我们不是要限流,而是要直接实现黑名单禁止访问网站的话。可以使用allowdeny标签。

server{
    listen: 80;
    server_name www.baidu.com;
    allow all; #允许访问所有的ip
    deny 172.0.0.1; #禁止 172.0.0.1 访问
}

可以配合shell脚本,然后把脚本加入crontab定时任务就可以实现动态添加黑名单。

#!/bin/bash
#取最近5w条数据
tail -n50000 /usr/local/nginx/logs/access.log \
#过滤需要的信息行ip等
|awk '{print $1,$12}' \
#过滤爬虫
|grep -i -v -E "google|yahoo|baidu|msnbot|FeedSky|sogou|360|bing|soso|403|admin" \
#统计
|awk '{print $1}'|sort|uniq -c|sort -rn \
#超过1000加入黑名单
|awk '{if($1>1000)print "deny "$2";"}' >> /usr/local/nginx/conf/blockip.conf
#重启nginx生效
/usr/local/nginx/sbin/nginx -s reload

本篇文章就到这里,感谢阅读,如果本篇博客有任何错误和建议,欢迎给我留言指正。文章持续更新,可以关注公众号第一时间阅读。

有关高并发系统设计之限流的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  3. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  6. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  7. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  8. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  9. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  10. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

随机推荐