分享一次机房出口带宽跑满的案例
背景
有客户反馈6月12号下午14:00左右xxx域名有大量数据传出,一度占满出口带宽,导致系统缓慢,希望我们尽快查一下,带宽监控如下图所示:
xxx正是我们部署在客户机房的招投标系统所使用的的域名。
概念
工作这么多年,带宽跑满这事也只是听说过,倒没亲身经历过,落到自己头上的时候多少有点不知所措,正式开始解决问题之前先把概念搞清楚,免得最终脱离目标。
网络带宽:网络带宽是指在单位时间(一般指的是1秒钟)内能传输的数据量。网络和高速公路类似,带宽越大,就类似高速公路的车道越多,其通行能力越强。数字信息流的基本单位是bit(比特),时间的基本单位是s(秒),因此bit/s(比特/秒)是描述带宽的单位,1bit/s是带宽的基本单位。
来源于百度百科
网速:网速一般是指电脑或手机上网时,上传和下载数据时,请求和返回数据所用的时间长短。电脑中存取数据的单位是“字节”,即byte(大写B),而数据通信是以“字位”作为单位,即bit(小写b),两者之间的关系是1byte=8bit。电信业务中提到的网速为1M、2M、3M、4M等是以数据通信的字位作为单位计算的。所以电脑软件显示的下载速度为200KB时,实际线路连接速率不小于1.6Mbit(1600Kbit)。
来源于百度百科
下载、上传:网络数据传输分为发送数据和接收数据两部分。上传就是向外部发送数据,下载为从外部接收数据。他们都受网络带宽和设备性能制约。在日常网络传输中大致1Mbps=1024/8KB/s=128KB/s(1/8)。例如上行的网络带宽为100Mbps,那么最大上传速度就是12800KB/s,也就是12.5MB/s。
用户申请的宽带业务速率指技术上所能达到的最大理论速率值,用户上网时还受到用户电脑软硬件的配置、所浏览网站的位置、对端网站带宽等情况的影响,故用户上网时的速率通常低于理论速率值。理论上:2M(即2Mb/s)宽带理论速率是:256KB/s(即2048Kb/s),实际速率大约为103--200KB/s;4M(即4Mb/s)的宽带理论速率是:512KB/s,实际速率大约为200---440KB/s。以此类推。
来源于测速网
大概总结一下,一般提到的1M带宽、10M带宽是指Mb/s,小b是代表比特,用户从浏览器下载时的速率显示是Byte(字节)/s(秒),1Byte/s=8bps,2M带宽的理论速率是256KB/s(2/8*1024),100M带宽的理论速率是12.5MB/s(100/8)。
初步思考
结合上一节的概念介绍,从服务器角度出发,对入网和出网带宽进行说明,以下内容来自阿里云官网介绍:
下图为数据流的方向说明
在我们案例中其实就是上图中的红框圈起来部分占满了,多余的比特流只能等着,和堵车一样,路就那么宽,超负荷了以后就只能堵车。
到这儿,基本问题才算是搞清楚了,“出口带宽被占满,导致用户请求长时间拿不到响应,其实系统一点也不忙,只是回来的路上堵车了。”
找出真凶
从访问量上来看,系统非常平稳,没有所谓的突发流量,再者说一个toB性质的网站,那点突发流量也是蚍蜉撼树。
从业务属性来看,就是普通的crud业务,对带宽的需求不大,不像抖音、快手这类带宽消耗大户,那究竟是什么占满了带宽呢,要是有日志就好了,还真有。这里顺便普及一个知识点,网站一般都会记录access log(访问日志),access log中会记录响应体的大小,所以我们只要抓取事发时间前后的access log分析是否有超大响应体的情况,果不其然在14:02分发现有两个附件下载的超大响应(响应体大小463兆),如下图所示(点击可放大):
可谓是防不胜防,最不起眼的附件功能,竟然成了“杀手锏”。
解决问题
正式解决之前先说一个生活中的案例,相信大家都不陌生。
笔者刚工作那阵和几个朋友一起合租,上网无疑成了大家下班之后消遣的主要方式,有听歌的、刷剧的、看八卦的、打网游的,无数次的听到打网游的小哥从房间冲出来大喊:“谁在干啥呢,我这卡的都不动了”,我心想“大家各玩各的,凭什么你打游戏就不让别人刷剧、听歌了”,想归想,架不住天天在屋里嚎啊,最后大家想出来一个办法,给每个人限速,比如上下行都不超过500KB/s,从那以后屋里确实安静了一些。
拍摄于西安老城根某一艺术品展览
回到我们的案例,可以借鉴限速\限流的思想来保护机房出口带宽不被下载请求给占满,大概有两种方式,下面分别介绍。
方案1:nginx层-limit_rate指令
http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate
Syntax: limit_rate rate; Default: limit_rate 0; Context: http, , , serverlocationif in location
Limits the rate of response transmission to a client. The is specified in bytes per second. The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit. rate
比如下面这个就是限制每个请求的下载速度不超过512KB/s
server { location /down { limit_rate 512k; } }
使用起来还是很方便的,考虑到并不是所有客户环境都有nginx,所以这个方案最终没有采用,而是选择在应用层自己处理,接下来介绍。
方案2:应用层通过Guava-RateLimiter
第一次接触RateLimiter是几年前,也是为了使用其限流功能,比如限制某接口的请求量不能超过100QPS,在处理当下这个出口带宽占满的案例时我着实没有想起它,究其原因我认为有两点:
-
没有理解透彻其背后原理;
-
看待问题没有遵循第一性原理,拿到问题时大多时候都是去百度找相同问题的解决方案,这样一来答案一定是局限的,无法创新。
虽然这一次借助RateLimiter来解决问题也是网络上的提示,但是带给我的收获不仅仅是解决当下问题这么简单,还有深层次的思考方式上的变革,我觉得这是非常重要的。
下面是一段再熟悉不过的文件下载的代码片段
String disposition = "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"); res.setHeader("Content-disposition", disposition); try(InputStream is = new FileInputStream("file")){ byte buf[] = new byte[4*1024]; int len = -1; while((len = is.read(buf)) != -1){ res.getOutputStream().write(buf,0,len); } res.getOutputStream().flush(); }
源源不断的从文件输入流中读取内容,然后写到网络输出流,不加任何限制的情况下导致机房出口带宽被占满,改进方式就是引入RateLimiter,往网络输出流中写之前调用RateLimiter获取令牌,如果超过了限定的速度就会阻塞。
//定义RateLimter,每秒发出1兆令牌 static RateLimiter rateLimiter = RateLimiter.create(1*1024*1024); String disposition = "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"); res.setHeader("Content-disposition", disposition); try(InputStream is = new FileInputStream("file")){ byte buf[] = new byte[4*1024]; int len = -1; while((len = is.read(buf)) != -1){ //输出之前前往RateLimiter获取令牌,保证不要超过1兆每秒 rateLimiter.acquire(len); res.getOutputStream().write(buf,0,len); } res.getOutputStream().flush(); }
限制效果如下,下载速度基本稳定在1兆左右:
最终采用方案2解决问题,在我们的场景中,其更通用、灵活。
总结
通过一个出口带宽被占满的案例介绍了两种解决方案,也啰嗦了一点自己思考问题的方式,希望能给大家带来些许收获,最后带来一个彩蛋,如果几年前我就认真看了RateLimiter的Api文档,处理当下问题时也能节约不少网络上找答案的时间,而且收获的是一手资料。
RateLimiter类注释里的一段,提到了网络限速的场景,一起来看下
As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000 permits per second: final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second void submitPacket(byte[] packet) { rateLimiter.acquire(packet.length); networkService.send(packet); }