救火队员的那些事(4)-关于流控
这次救火讨论的是流控,流控可以很简单,也可以非常复杂,特别是动态流控。我们有一个产品在T国某个运营商遇到了麻烦,这个运营商的母公司是欧洲的运营商,而欧洲的运营商对于产品验收的苛刻是出了名的,而这次给我们带来麻烦的就是流控。
这个产品的大致功能就是把订阅的短消息或者彩信内容发送到户手机上,就类似于在几年前火了一段时间的手机报。这个产品的流控有些复杂:
1、首先这套系统部署是集群,假设集群有6台主机,这6台主机的每秒下发的消息量不能超过一个值,假设为10000,为什么有这个要求,是因为下游执行发送消息的短消息网关或彩信网关有流控,你发的快了,下游系统会拒绝你,所以这个总的TPS不能超。
2、这些消息的来源都是来自于某个SP或者CP,每个CP或者SP在签约平台的时候,它有一个最大每秒发送量,发送量分为彩信、短信以及WAP Push渠道。
3、除了上面的限制以外,CP和SP又分了优先级,如果低先级和高优先级的时候在一起发送的,一定要先等到高优先级的先发送完成。
还有一些规则要求,时间比较久了,我记不是特别清楚了。
这套系统在国内和国外不少地方都上线了,不能说质量多好,也没有太大的问题,我接手这个产品大概几个月的时候在这个局点遇到了问题,客户投诉他们通过线上实际的监控发现2个严重的问题,如果不解决,他们会******:
1,这套系统在高峰期间实际下发的每秒流量没有达到当时他们购买的流量,比如他们购买为10000条/秒,但是他们实际监测发现高峰的时候,可能只有9000条/秒.
2,系统的流控不稳定,当时合同签署的流控上下波动不能大于正负10%。举个例子,假设他们有一个最大的SP,在上午10点到11点独占发送消息,他们设定按200条/秒发送,这时没有任何其它的SP抢占通道,那么消息发送速率应该是在180~220条/秒,但是实际发送的速率会在这个区间之外。
当时我接手这个产品几个月时间,对系统的大致实现有了解,但是细节并不是清楚,负责这个产品的小组长找到我求助,不懂也要硬着头皮上。一开始以为是个小问题,这个产品的开发骨干试了几种方案过了一周,发现都很难达到客户的要求,这时客户和项目经理失去了耐心,开始把问题往高层领导汇报,一旦高层领导知道,后果可想而知。我当时带着这个产品的小组长以及开发骨干开始做这个问题的攻关,前后总共投入了3周左右的时间(有一半的时间基本上是都是在通宵)。
经过几天走读代码以及实际测试验证,我发现原来的流控实现方案存在严重的缺陷,原来的方案是存储过程实现上面的流控的流量的分配动作,这个存储过程每1秒运行一次,每次运行的时候它会把当前需要下发的任务根据总流控、CP以及SP等计算一遍,然后把需要下发的数据加载到Oracle的分区表中,把要发送的速率插入到任务表中,然后集群是每个任务下发的线程从任务表中读到任务,然后再从分区表中加载上任务要发送的数据,再周而复始的存储过程不断计算分配任务、集群每个节点加载任务数据发送数据。
这个产品的最初的开发在2004~2005年左右,最初的设计人员找不到了,但是我猜想为什么用数据库来解决,是因为如何控制每秒发送的消息在集群下并不好实现,而数据库存放集中式数据是最简单的实现方式。这种方案对于大部分运营商对流控的准确性要求没有那么高时,其实并不是太大的问题,只要系统的负载不是过重,消息能基本准确的发送就可以了。
回到客户发现的2个问题,可以大致感性的分析原因:
1、因为任务的分配是每秒重新计算一次,计算完成以后,下发线程要再从分区表中加载数据,这都需要时间,即数据不是立即下发,会导致之前分配的速率执行时间被拉长了,所以很难达到总容量
2、流控的不准确性:单个线程发送的流控算法优化问题,我会在后面再讨论这个问题
针对第一个问题,我们最后设计了一个流控中心的应用,抛弃了原来通过数据库进行任务的分配的逻辑,其它所有发送消息的应用通过Netty连接到流控中心,当前任务以什么速率发送完全以流控中心的指标为准,而流控中心它的计算逻辑全在内存中实现。流控中心和消息发送应用之间双向通信,当下发的速率要调整的时候,流控中心可以把速率主动的下发给消息中心。当时因为时间紧,我们并没有用专门的机器来部署流控中心,而是把所有的发送消息的主机在启动的时候把自己的IP地址写到数据库的一张表中,然后在这张表中最先插入数据的那台机器就兼容来流控,顺便发送消息。因为流控并不是很耗性能,所以即使发送消息对性能影响不大。如果负责流控的机器挂了的话,再由心跳机制把那台挂了的机器删除掉,这时其它发送消息的节点再重新连接到新的负责流控的主机上。
看到这,大家可能觉得这招很土,其实当时我想过用JGroup来做集群的管理,但是如果你在生产环境用过JGroup的话,就会发现这玩意太复杂了,我当时在很多项目都被坑过。那时也没有见过其它什么更好的集群通信的开源组件了,加上项目时间紧,我们按更加稳妥的方案实现。
而针对第二个问题,难度相对要小一些。原来的方案采用了一个定时器,这个定时器每秒运行一次,每次运行的时候把一个Semaphore置成需要发送的TPS,每个发送线程在发送之前accquire,如果能取到就发送,取不到就阻塞,然后再下一个1秒的时候再把这个信号量Release到发送的TPS。这么实现会导致发送的在1秒内不平均,比如说:我要一秒发送30条消息,有可能在1/3秒的时候就把消息都发完了,然后在剩下的2/3秒什么也没发,这样如果在下游采样统计TPS的周期不是按1秒来计算,而是按1/6计算的时候,明显发送速率就不稳定。
改进以后的流控算法是参考了一个兄弟产品的方案,称之为滑动窗口流控算法,因为画图比较花时间,我简单描述一下。这个算法将1秒分成10个小窗口,还是以上面每秒发送30条为例,在第一个小窗口时,我需要发送3条消息,到第2个小窗口时我只需要累计发送2*3=6条消息,到第3个小窗口时,我只需要累计发送3*3=9条消息,这个算法的好处是发送的消息速率更加平滑,将下游发送的速率的波动给抹平掉。这个算法还有一个好处,不像上面一个算法要有定时器不断的清零,而只需要很简单的获取当前系统时间-系统启动时间,就可以算出来当前处在哪个发送窗口,系统的开销不仅小而且更准确。
经过上面的2个优化以后,达到了预期的目标,后面这个产品的基线版本的流控算法又重新进行了设计,后面的随笔再说后来怎么优化的。1年以后的基线版本优化以后,这个产品的小组长离开了公司,这个产品的开发骨干去了海外常驻,也没有了联系。但是写到这,就想到3个人像个落魄鬼一样在公司不分昼夜的讨论方案、改代码、测试…