面试官 | 讲一下如何给高并发系统做限流?


作者 | nick hao

来源 | uee.me/cDuRD

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。本文结合作者的一些经验介绍限流的相关概念、算法和常规的实现方式。

缓存

缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。

在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。

降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。

根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。

限流

限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。

比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

限流的算法

常见的限流算法有:计数器、漏桶和令牌桶算法。

计数器

计数器是最简单粗暴的算法。比如某个服务最多只能每秒钟处理100个请求。我们可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。

内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。

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

示例代码如下:

//服务访问次数,可以放在Redis中,实现分布式系统的访问计数
Long counter = 0L;
//使用LinkedList来记录滑动窗口的10个格子。
LinkedList<Long> ll = new LinkedList<Long>();public static void main(String[] args)
{Counter counter = new Counter();counter.doCheck();
}private void doCheck()
{while (true){ll.addLast(counter);if (ll.size() > 10){ll.removeFirst();}//比较最后一个和第一个,两者相差一秒if ((ll.peekLast() - ll.peekFirst()) > 100){//To limit rate}Thread.sleep(100);}
}

漏桶算法

漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。贴了一张维基百科上示意图帮助大家理解:

漏桶算法的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,则不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

漏桶算法比较好实现,在单机系统中可以使用队列来实现(.Net中TPL DataFlow可以较好的处理类似的问题,你可以在这里找到相关的介绍),在分布式环境中消息中间件或者Redis都是可选的方案。

令牌桶算法

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。

  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。

  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。

  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

如下图:

令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的to network的速率。to network我们可以理解为消息的处理程序,执行某段业务或者调用某个RPC。

漏桶和令牌桶的比较

令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。更多算法相关:算法聚合

整体而言,令牌桶算法更优,但是实现更为复杂一些。

限流算法实现

Guava

Guava是一个Google开源项目,包含了若干被Google的Java项目广泛依赖的核心库,其中的RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

1. 常规速率:

创建一个限流器,设置每秒放置的令牌数:2个。返回的RateLimiter对象可以保证1秒内不会给超过2个令牌,并且是固定速率的放置。达到平滑输出的效果

public void test()
{/*** 创建一个限流器,设置每秒放置的令牌数:2个。速率是每秒可以2个的消息。* 返回的RateLimiter对象可以保证1秒内不会给超过2个令牌,并且是固定速率的放置。达到平滑输出的效果*/RateLimiter r = RateLimiter.create(2);while (true){/*** acquire()获取一个令牌,并且返回这个获取这个令牌所需要的时间。如果桶里没有令牌则等待,直到有令牌。* acquire(N)可以获取多个令牌。*/System.out.println(r.acquire());}
}

上面代码执行的结果如下图,基本是0.5秒一个数据。拿到令牌后才能处理数据,达到输出数据或者调用接口的平滑效果。acquire()的返回值是等待令牌的时间,如果需要对某些突发的流量进行处理的话,可以对这个返回值设置一个阈值,根据不同的情况进行处理,比如过期丢弃。

2. 突发流量:

突发流量可以是突发的多,也可以是突发的少。首先来看个突发多的例子。还是上面例子的流量,每秒2个数据令牌。如下代码使用acquire方法,指定参数。

System.out.println(r.acquire(2));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));

得到如下类似的输出。

如果要一次新处理更多的数据,则需要更多的令牌。代码首先获取2个令牌,那么下一个令牌就不是0.5秒之后获得了,还是1秒以后,之后又恢复常规速度。这是一个突发多的例子,如果是突发没有流量,如下代码:

System.out.println(r.acquire(1));
Thread.sleep(2000);
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));

得到如下类似的结果:

等了两秒钟之后,令牌桶里面就积累了3个令牌,可以连续不花时间的获取出来。处理突发其实也就是在单位时间内输出恒定。这两种方式都是使用的RateLimiter的子类SmoothBursty。另一个子类是SmoothWarmingUp,它提供的有一定缓冲的流量输出方案。

/**
* 创建一个限流器,设置每秒放置的令牌数:2个。速率是每秒可以210的消息。
* 返回的RateLimiter对象可以保证1秒内不会给超过2个令牌,并且是固定速率的放置。达到平滑输出的效果
* 设置缓冲时间为3秒
*/
RateLimiter r = RateLimiter.create(2,3,TimeUnit.SECONDS);while (true) {/*** acquire()获取一个令牌,并且返回这个获取这个令牌所需要的时间。如果桶里没有令牌则等待,直到有令牌。* acquire(N)可以获取多个令牌。*/System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1));
}

输出结果如下图,由于设置了缓冲的时间是3秒,令牌桶一开始并不会0.5秒给一个消息,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。

图中红线圈出来的3次累加起来正好是3秒左右。这种功能适合系统刚启动需要一点时间来“热身”的场景。

Nginx

对于Nginx接入层限流可以使用Nginx自带了两个模块:

  • 连接数限流模块ngx_http_limit_conn_module

  • 漏桶算法实现的请求限流模块ngx_http_limit_req_module

1. ngx_http_limit_conn_module

我们经常会遇到这种情况,服务器流量异常,负载过大等等。对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个ip的连接数,并发数进行限制。

ngx_http_limit_conn_module 模块来实现该需求。该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数。并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。

我们可以在nginx_conf的http{}中加上如下配置实现限制:

#限制每个用户的并发连接数,取名one
limit_conn_zone $binary_remote_addr zone=one:10m;#配置记录被限流后的日志级别,默认error级别
limit_conn_log_level error;
#配置被限流后返回的状态码,默认返回503
limit_conn_status 503;

然后在server{}里加上如下代码:

#限制用户并发连接数为1
limit_conn one 1;

然后我们是使用ab测试来模拟并发请求:

ab -n 5 -c 5 http://10.23.22.239/index.html

得到下面的结果,很明显并发被限制住了,超过阈值的都显示503:

另外刚才是配置针对单个IP的并发限制,还是可以针对域名进行并发限制,配置和客户端IP类似。

#http{}段配置
limit_conn_zone $ server_name zone=perserver:10m;
#server{}段配置
limit_conn perserver 1;

2. ngx_http_limit_req_module

上面我们使用到了ngx_http_limit_conn_module 模块,来限制连接数。那么请求数的限制该怎么做呢?这就需要通过ngx_http_limit_req_module 模块来实现,该模块可以通过定义的键值来限制请求处理的频率。

特别的,可以限制来自单个IP地址的请求处理频率。限制的方法是使用了漏斗算法,每秒固定处理请求数,推迟过多请求。如果请求的频率超过了限制域配置的值,请求处理会被延迟或被丢弃,所以所有的请求都是以定义的频率被处理的。

在http{}中配置

#区域名称为one,大小为10m,平均处理的请求频率不能超过每秒一次。limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

在server{}中配置

#设置每个IP桶的数量为5
limit_req zone=one burst=5;

上面设置定义了每个IP的请求处理只能限制在每秒1个。并且服务端可以为每个IP缓存5个请求,如果操作了5个请求,请求就会被丢弃。

使用ab测试模拟客户端连续访问10次:

ab -n 10 -c 10 http://10.23.22.239/index.html

如下图,设置了通的个数为5个。一共10个请求,第一个请求马上被处理。第2-6个被存放在桶中。由于桶满了,没有设置nodelay因此,余下的4个请求被丢弃。

推荐阅读:Java面试题汇总(208道)【END】
关注下方二维码,订阅更多精彩内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546463.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python利用multiprocessing实现多进程,Pyinstaller打包python多进程程序出现多个窗口

一、为什么需要采用multiprocessing多线程技术 自己在做文件Hash校验工具V1.0小工具软件时,需要读取文件,计算文件的MD5、SHA1、SHA256和CRC32这些Hash值,对于小文件能够很快计算出hash值,但是对于大文件需要花费一些时间,不知道进度如何?使用进度条指示也无法正确显示进…

面试官 | 说一下数据库如何分库分表?

作者 | butterfly100来源 | cnblogs.com/butterfly100/p/9034281.html一. 数据切分关系型数据库本身比较容易成为系统瓶颈&#xff0c;单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后&#xff0c;由于查询维度较多&#xff0c;即使添加从库、优化索…

面试官 | JVM 为什么使用元空间替换了永久代?

7:40到11:40历时4个小时完成了该文&#xff0c;看到电脑中左边的便签了么&#xff0c;我也是拼了。在Java8和以后版本中JVM的内存结构慢慢发生了变化。作为面试官如果你还不知道&#xff0c;那么面试过程中是不是有些露怯&#xff1f;作为面试者&#xff0c;如果知晓这些变化&a…

Typora颠覆写作体验的极简好用 Markdown 编辑器基本设置教程

Typora是一款Markdown编辑器。 无论你是建网站写博客、每天写日记、自媒体写稿、办公、程序员写代码文档等等&#xff0c;Typora 都能满足你的要求。 Typora基本设置教程 1.“通用”项设置 打开“文件”下的“偏好设置”选项&#xff0c;在“通用”这项下&#xff0c;设置自…

面试官问:一个Java字符串中到底能有多少个字符?

作者 | 鸟窝来源 | urlify.cn/qYNR3q依照Java的文档&#xff0c; Java中的字符内部是以UTF-16编码方式表示的&#xff0c;最小值是 \u0000 (0),最大值是\uffff(65535)&#xff0c; 也就是一个字符以2个字节来表示&#xff0c;难道Java最多只能表示 65535个字符&#xff1f;char…

PHP多进程处理并行处理任务实例

2019独角兽企业重金招聘Python工程师标准>>> 本文目的 本文通过例子讲解linux环境下&#xff0c;使用php进行并发任务处理&#xff0c;以及如何通过pipe用于进程间的数据同步。写得比较简单&#xff0c;作为备忘录。 PHP多进程 通过pcntl_XXX系列函数使用多进程功能…

Python PyCharm利用PyQt5使QPlainTextEdit支持拖放文件,类提升,重写QPlainTextEdit类

一、利用PyCharm新建基于PyQt5对话框工程MyMainTest,添加QPlainTextEdit控件,保存主窗口MyQTMainForm.ui文件运行如下: 二、新建myqplaintextedit.py文件,创建MyQPlainTextEdit类继承于QPlainTextEdit,只允许excel(.xls或.xlsx)文件拖放,及信号发射处理。代码如下: #…

经典面试题|ConcurrentHashMap 读操作为什么不需要加锁?

作者 | 上帝爱吃苹果来源 | cnblogs.com/keeya/p/9632958.html我们知道&#xff0c;ConcurrentHashmap(1.8)这个并发集合框架是线程安全的&#xff0c;当你看到源码的get操作时&#xff0c;会发现get操作全程是没有加任何锁的&#xff0c;这也是这篇博文讨论的问题——为什么它…

正能量

2019独角兽企业重金招聘Python工程师标准>>> 对别人&#xff0c;永远把最好的方面表现出来&#xff0c;这样别人都会为你传递正能量&#xff0c;你就能够得到能量累加。 对自己&#xff0c;要自信&#xff0c;永远给自己传递正能量&#xff0c;这样自己周边的能量场…

Python datetime time计算时间差

一、计算时间差 """ python主文件 """ # -*- coding: utf-8 -*-import time"""主函数 """ if __name__ __main__:# 获取当前开始的日期和时间&#xff0c;例&#xff1a;2022-02-05 14:20:36strStartDateTime …

面试官 | AJAX请求为什么不安全?

作者 | 撒网要见鱼链接 | cnblogs.com/dailc/p/8191150.html# AJAX三问AJAX请求真的不安全么&#xff1f;AJAX请求哪里不安全&#xff1f;怎么样让AJAX请求更安全&#xff1f;# 前言本文包含的内容较多&#xff0c;包括AJAX&#xff0c;CORS&#xff0c;XSS&#xff0c;CSRF等内…

IE6,IE7 Firefox 兼容问题

2019独角兽企业重金招聘Python工程师标准>>> 关于ie6、ie7和ff浏览器兼容网友评论 0 条 转载到博客 2009-1-8 16:11:23 来源: 本站整理顶一下这些方法都是我平时用到时在网上找到收藏下来的呵呵&#xff0c;我提前声明一下免得误会!一、CSS HACK以下两种方法几乎能…

面试官 | 说一下什么是代理模式?

看了这篇文章&#xff0c;你会对静态代理模式&#xff0c;JDK 动态代理模式和 CGLIB 动态代理模式有个很清晰的认识。01、简介什么是代理模式代理模式也称为委托模式&#xff0c;属于结构型模式之一。在某些情况下&#xff0c;一个对象不适合或者不能直接引用另一个对象&#x…

面试官 | 说一下 JVM 常用参数有哪些?

作者 | SimpleSmile_5177来源 | i7q.cn/50SRVt前言说一下 JVM 常用的参数有哪些&#xff1f;是比较常用的面试问题&#xff0c;同时如果项目特别大了&#xff0c;需要增加一下堆内存的大小、或者是系统老是莫明的挂掉&#xff0c;想查看下gc日志来排查一下错误的原因&#xff…

CSS 实现按钮及线呼吸灯效果

1. [代码]style view sourceprint?01<style>02 body{03 font-family:Segoe UI Light,Segoe UI,Arial,微软雅黑,sans-serif;04 font-size: 20px;05 color:#333333;0607 }08 .breath {…

美团面试题 | JVM 堆内存溢出后,其他线程是否可继续工作?

作者&#xff1a;gosaintmrc来源&#xff1a;http://sina.lt/gqaM最近网上出现一个美团面试题&#xff1a;“一个线程OOM后&#xff0c;其他线程还能运行吗&#xff1f;”我看网上出现了很多不靠谱的答案。这道题其实很有难度&#xff0c;涉及的知识点有jvm内存分配、作用域、g…

Python格式化字符串f-string常用用法

简介&#xff1a; f-string&#xff0c;亦称为格式化字符串常量&#xff08;formatted string literals&#xff09;&#xff0c;是Python3.6新引入的一种字符串格式化方法&#xff0c;该方法源于PEP 498 – Literal String Interpolation&#xff0c;主要目的是使格式化字符串…

面试官 | Java 对象不使用时为什么要赋值为 null?

作者 | zhantong来源 | www.polarxiong.com前言许多Java开发者都曾听说过“不使用的对象应手动赋值为null“这句话&#xff0c;而且好多开发者一直信奉着这句话&#xff1b;问其原因&#xff0c;大都是回答“有利于GC更早回收内存&#xff0c;减少内存占用”&#xff0c;但再往…

CentOS 6.5下利用Rsyslog+LogAnalyzer+MySQL部署日志服务器

一、简介 LogAnalyzer 是一款syslog日志和其他网络事件数据的Web前端。它提供了对日志的简单浏览、搜索、基本分析和一些图表报告的功能。数据可以从数据库或一般的syslog文本文件中获取&#xff0c;所以LogAnalyzer不需要改变现有的记录架构。基于当前的日志数据&#xff0c;它…

国内各大厂 | 简历投递信息汇总和精美模板下载

作者 | 王磊来源 | Java中文社群1 前言为了让你的简历能被各大厂商的 HR 第一时间看到&#xff0c;我人工整理了以下投递渠道方便你能直接投递&#xff0c;下面一起来看&#xff08;排名不分先后&#xff09;。2 投递信息汇总阿里巴巴https://campus.alibaba.com/index.htm腾讯…