简单的流量控制系统

流量控制概述

在一个后台系统中,流量控制属于基础组件的功能,其实,在好久以前的通信时代,流量控制就已经很是成熟了,在路由器交换机上面几乎都有全面的流量控制的解决方案,像QoS这类流量整形的方案,都已是在网络模型的各个层来进行流量的控制和分发了,能够按照通道,按照端口,IP,MAC,业务类型等各个维度对流量进行整形和控制,好比让语音类的这种高优先级的流量优先经过,而视频聊天这种丢了几帧数据其实没什么影响的低优先级流量慢点经过。前端

对于流量控制,在中后台系统中,通常分红两个类型吧,一种是对链接数进行控制,保证一个机器有可控的链接数,一种是对真实流量的控制,保证机器能经过的流量有多少。golang

流量控制和缓存同样,其实是对后端的服务起到一个保护的做用,不至于把后端的服务击穿,不一样的地方在于缓存保护的主要是读操做,若是用缓存来保护写操做的话,也是一个异步的过程,像下面这个图同样。算法

图片描述

而流量控制主要保护的就是写操做了,保证后端的服务别被写请求给击穿,目前咱们之因此不多见到流量控制的服务了,主要由于你们优化方向已经朝其余两个方向上来作了。后端

  • 一是优化了后端的服务,把后端服务变成了一个能够动态扩展的集群,如今都是说要弹性扩展嘛,因此把优化放到后端服务器上去了,让后端的服务器可以承载更多的写流量,流量控制的东西相比就少了,并且从用户体验上来讲,后端服务器可以承载更大的流量也能够保证数据的实时性更好,不会产生数据的延迟,因此扩展后端的集群规模成了一个优化方向。就像下图这样,后端的DB变成了一个集群来保证写规模的扩大。缓存

图片描述

  • 另一个优化方向就是引入了消息队列这个东西,实际上消息队列就是一个升级版本的流量控制系统,虽然它没有用到流量控制的这些个算法,可是它达到的目前和流控其实差不太多,效果还更好,因此如今一旦出现后端扛不住写的状况,都在中间加上一个消息队列来解决,一是解决了写入流量的可控,二是还把系统给解耦了,一箭双雕。服务器

图片描述

正由于上面的两个缘由,如今关注流量控制的人变少了。但有时候,若是后端的服务不能抗住写的压力,而且也没有足够的资源去部署一个消息队列的话(由于消息队列的部署是须要单独的服务器的,仍是有成本上的考虑),那么作一个简单的流控系统也基本能知足要求。微信

在本文中,基于链接的流量控制就不是咱们讨论的范围了,那个比较简单一点。网络

流控设计

咱们所说的流量控制,你们比较了解的通常分红两种算法,一种是漏桶算法,一种是令牌桶算法,咱们这里并不去深究这两种算法的区别,这个能够在网上很容易找到两种算法的定义和算法描述,这两种流控策略都是来源于路由器的IP层流量控制的算法,咱们从另一个角度来看看流控,咱们只借用这些算法的思想,从需求开始,本身一步一步设计一个流控系统。多线程

流控需求

首先,拿到一个流量控制的需求,需求是入口流量是5MB/s,可是峰值流量是100MB/s,出口的流量要控制在50MB/s之内,数据还不能丢弃,如何来实现这个系统。异步

初步设计

第一感受应该就是下图这个样子,中间有一个内存的FIFO队列,写入方不停的往这个队列里面写入数据,而另外一端不停的读取这个FIFO,而后把流量分发到后端上去,这样就完成了数据不能丢弃这个需求,很像前面的那个消息队列,可是慢着,要是前端的写入流量一直保持在峰值的话,那么这内存也爆了,因此除了内存的FIFO之外,还须要一个文件的FIFO来保证在一直是峰值的状况下保证数据的不丢失,你要是问要是硬盘满了怎么办,那我只能呵呵了,固然,也不是没有解决办法,把服务设计成多机模式嘛,这不在本文的讨论范围内。

图片描述

总之,按照上图的设计方法,基本能够知足数据不丢的状况了,对于FIFO的实现方式,能够有不少种,一种是本身开链表,两个指针一头一尾,一边写一边读,若是两边都是多线程的话,锁的设计须要特别注意,尽可能减小锁的消耗。还有若是是使用想golang这样的带channel的语言,那么直接丢到channel里面也行,不过这样就是内存不太可控,若是某一个时间段上的数据包都特别大的话,容易形成总体内存的飙升,看具体场景和硬件资源吧,这里就不在赘述了,那么,接下来就考虑流控了。

流控控制器

既然须要流量控制,那么就是发送端在发送数据的时候得知道我如今这个数据能不能发,能发的话能够全发出去仍是只能发一部分,最简单的办法就是有个总体的流量控制器,每次发送端发送数据的时候都去询问一下这里流量控制器,如今有多少配额,我能用多少,结构图以下图所示,发送端去询问流量控制器,而后拿到一个发送的配额,按照这个配额进行发送。

图片描述

如今FIFO队列也有了,流量控制器也有了,可是最关键的就是流量控制器如何工做的呢?接下来就要设计这个流量控制器了。

流控算法

对于流控算法的设计,由于是配额制的,因此咱们首先得有一个配额的产生机制,好比需求里面说的50MB/s,那就是每秒能够产生50MB的配额,这个简单,你把配额当作一个池子,每秒往池子里加50MB就好了,一旦池子满了,就不加了嘛,这里说的是每秒50MB,实际上加的时候能够按照毫秒来,好比每10毫秒往池子里面加0.5MB,这个用一个线程循环的线程就能够完成。

Quota=0
while(1){ sleep(10MS) ; Quota+=0.5;if(Quota>=50){continue;}  }

若是是golang的话,也能够把这一部分交给channel来作,写满了就阻塞在channel上了,就像下面这样:

QuotaChannel:=make([]int,100)
for{
    QuotaChannel<-5
    time.Sleep(time.Millisecond*10)
}

配额产生搞定了,那么配额的消耗就是这个的反操做嘛,代码就不写了,可是若是是像前面那样使用一个变量的话,那么读写都要加锁,而用golang的channel的话,就不用加锁了,看上去后面的效率更高,但实际上差很少,由于golang的channel在实现上也是加了锁的,并且锁的粒度还比较大,因此用channel并无什么效率上的提高。

整个流控算法实现完之后,就是下图这样样子了。

图片描述

上面实现了一个简单的流控算法,咱们没有去深究令牌桶和漏桶算法的具体实现方式,只是按照咱们本身的思想去设计了一个流控算法,实际上和令牌桶的思想基本是一致的,你们能够去具体的看看令牌桶,再在算法上作一些优化。

流控模型的层级

通常的流控设计是跑在TCP层上的,就是能限制TCP层的流量,这样有一个好处就是当你要发送的一个数据包大于50MB的时候,也可使用这个流控模型,由于在TCP的链接上是能够分包进行发送的,能够拆成屡次进行发送,这没有什么问题。

若是你的应用是跑在HTTP协议上的,如今不少语言都集成了HTTP的包,直接调用POST请求的API就能够发送数据了,这时候若是出现大于50MB的数据包,就没法进行拆包发送了,出现这种状况,就须要根据实际的业务要求来进行修改和优化了。

若是只是偶发状况而且后端服务也能够忍受的话,那就忍了吧,若是不是偶发状况或者后端服务彻底不能忍,那你就别用语言自带的HTTP包了,发送的时候本身创建TCP链接本身发送吧,这种改造也不是很复杂。

关于流控的其余

这篇文章说到的是一个流控的模型,借鉴了令牌桶和漏桶的一些算法,但不要觉得流控就是这样的,令牌桶的模型,只是路由器中对IP报文进行流控的一种方式,在路由器上,还有TCP滑动窗口的流控方式,底层还有MAC层的流控方式等等。

实际上在TCP层以上是无法作到比较准确的流控的,由于一些协议的开销,TCP自动重传的开销,这个流控器都监测不到,根本就无法准确的流控,只能说是作作简单的流量限制,就拿本文说的例子来讲,需求说是须要50MB/s的速度,这是一秒的速度,准确来说需求方是但愿50MB的数据在一秒钟之内均匀的发送出去,而咱们的这个流控模型根本没法作到这一点,咱们来了一个数据,若是发现池子够大,那么50MB的数据直接就发走了,只是下一次发送的时候咱们还须要等0.5秒而已,而均匀的端到端的流控,通常采用的都是TCP的滑动窗口调整来实现,这已经超过本文要描述的了,若是你们感兴趣能够深刻去研究一下路由器的流控方式,包括滑动窗口流控啊,MAC层流控【802.1Qbb】啊。

通信领域仍是有不少很牛逼的算法的,只是如今通信领域有些没落了,互联网起来了,作个高可用,分布式就很牛叉同样,实际上和通信领域的许多东西比起来就是渣渣啊。就如同本文,写了这么多,可能在路由器芯片的功能介绍文档上,就是一行,呵呵。


若是你以为不错,欢迎转发给更多人看到,也欢迎关注个人公众号,主要聊聊搜索,推荐,广告技术,还有瞎扯。。文章会在这里首先发出来:)扫描或者搜索微信号XJJ267或者搜索西加加语言就行

图片描述

相关文章
相关标签/搜索