雪崩效应

标记-清除和节点复制这两种简单的追踪式垃圾收集,往往会导致程序在运行期卡顿,在一些特定场景下甚至很明显。谈起这个问题,很多时候会以客户端程序,即有人机交互的情况下,从软件体验角度来说,但广义上来说,只要是实时系统,都会受到这个影响,客户端的程序只要不是像大型游戏这种,一般来说内存占用也不多,因此卡顿并不是很明显,但如果是实时性要求比较高的服务器程序就不同了,比如我一个之前在网易的朋友就谈到,他们的一个游戏服务器的业务处理用的python,由于存在大量循环引用,因此启动了python的垃圾收集机制(对于循环引用,python用标记-清除来解决),结果影响了体验,最后的解决办法是,手工关掉自动垃圾收集,在每局游戏结束后空闲的时候再手工开启,这是用人工介入来解决卡顿问题,当然这种做法给开发带来了额外负担,就好像要求程序员的代码不能出线循环引用的漂浮垃圾一样,但很多时候也是迫不得已  

不过,这里我并不想谈卡顿对用户体验造成的直接影响。从接下来的分析可以看到,即便是一些对实时性要求很低的后台系统,在特定情况下,一个很小的问题就会产生和它本身不相称的灾难性后果,遗憾的是,这里所谓的“特定”情况并不罕见,且没有引起足够的重视(至少在我看过和接触过的很多系统中)  

看一个抽象版本的后台服务器:  
while (running) 
{ 
    req = get_req(); 
    process(req); 
} 

这个框架很简单,但大多服务器就是这么做的。这里的req是一种广义上的请求,泛指一切需要处理的事件。服务器做process(req)会消耗一定时间,如果在这段时间中,有req到来,一般是缓存在一个队列中,否则服务器就只能处理在get_req的时候“刚好”收到的请求了,显然不合理,当然了,这里的队列是一个广义上的,并不是说一定要有一个前置服务器管理所有队列,比方说,客户端发送一段数据上来,这段数据可能被前置服务器接收并缓存在自己维护的队列,或者,这段数据在tcp的recv缓冲中,由内核维护等待进程recv,或者,服务器tcp的recv缓冲满了,则数据可能一部分到了服务器,一部分在客户端的tcp发送缓冲,总之,req队列是一个抽象的概念,为讨论方便,不妨将req也看做是离散的请求序列  

如果req的生成速度小于服务器的处理速度,则整体来看每个req都可以被及时处理,从客户端来看,那些需要交互的请求都可以在较短时间内被应答,这种时候服务器显然是正常运行,接着考虑一种不正常的情况,假如req的速度大于服务器的处理速度,结果会如何,用一个理想化的例子来描述,假设每个客户端连接上来,发一个req,等待结果(比如http),每10ms来一个客户端连接和req,服务器做一次process需要20ms,即req的速度是服务器承载量的两倍,同时假设req队列长度无限,那结果会如何  

有些人可能不假思索就说,服务器会成功处理一半的请求。接下来具体分析下,看看这种观点是否正确:  
第一个请求在时刻0ms到达,时刻20ms返回,耗时20ms  
第二个请求在时刻10ms到达,时刻40ms返回,耗时30ms  
...  
第n个请求在时刻(n-1)*10ms到达,时刻n*20ms返回,耗时(n+1)*10ms  
可以看到,req的处理耗时是随着时间推移不断变长的,这个问题的关键在于,客户端一般来说不会等待这么久,可能过个10秒没等到结果,就因超时而主动断开,但断开消息是在它之前发送的req之后的,服务器不知道,还会处理它的req,做了无用功,实际上当n大于某个阈值时,服务器就会陷入瘫痪状态,满负荷运转,但是在做没用的事情,而且req队列会越来越长  

其实生活中也有类似的例子,比如我在公司吃中饭就有这样的经验,如果能早10分钟下去,就可以在20分钟后吃完回到办公室,如果晚10分钟下去,则需要40分钟,因为队伍已经比较长了  

这个例子显示,在某些设计下,若服务器处理能力小于外来请求量,实际的处理效果和请求量并不是简单的反比,而是会呈现非线性的关系,即便请求量只比最高容量高一点,也可能很快就使得服务器陷入瘫痪状态  

当然,req队列不可能是无限长的,假设req队列长度为m,如果队列满,则简单丢弃req,这样一来在上面这个例子下,如果队列满,则平均会丢弃一半的请求,然而,这并不能保证省下的一半都能处理成功,这是因为,放入队列的请求要等到队列前面的m-1个请求处理完毕,当m足够大的时候,服务器依然在不停地做无用功  

实际情况还不止于此,很多时候客户端有重试机制,比如说,客户端有最高3次的超时重试,则当请求量小于服务器容量时,系统正常运行,重试机制派不上用场,但如果请求量比服务器容量高,哪怕只是高10%,请求量很可能在短时间内暴涨到330%!而且都是废包,当服务器疲于奔命地处理无用作业的时候,又有更多的重试发起,不亚于DDOS攻击的流量会很快将系统打瘫  

对于可控系统,如果服务器能智能一点,这个问题或许能得到比较好的解决,具体做法也比较简单,每个req带上一个时间戳,服务器判断如果一个req没必要处理,则直接丢弃,因为客户端这时候很可能已经断线了,这样一来,上面这个例子中,就保证了即便请求量是容量的两倍,但依然有近一半客户端可以正常工作  

但是,这样做的一个前提是,客户端的超时时间相对可控,而有的时候,超时时间和重试次数是无法准确预估的,最最典型的例子,比如铁道部的售票网站,平心而论,我不认为这个系统烂到连非节假日低峰时期的交易量都撑不住,问题在于,当请求量超过容量的时候,客户端的超时时间和次数都是不可控的,心急的用户可能两三秒就刷一次页面,或者开多个页面,用抢票软件等,使得请求量呈爆炸性增长,形成恶性循环,这个时候就算临时扩容,也为时已晚了。假设这个系统可以支撑的交易量是1亿,当请求量是1.5亿的时候,就算只能保证9千万用户能正常,口碑大概也不会这么差  

如果说,所有用户能保持一个相对缓和的方式,比如每次只开一个页面,等待时间长一些再去重试,不用自动抢票,那么从整体上看,能买到票的人或许还多点,但是,在其他人都抢票的情况下,你不抢反而更吃亏,这是一个囚徒困境。当然,系统可以采用一些强制手段,比如限制单个用户的请求频率,但总得来说,不好的后果不能让用户来担责任,根本解决办法还是从系统本身着手,事先就应该增加容量,或在业务允许范围内做一些缓和用户行为的方案,例如,对一时处理不了的请求进行排队,通知客户在一定时间以后查询结果等,不过话说回来,如果是抢小米的产品,这样做或许可以,因为就算抢不到也没什么,但买票这种事情,拖个半天才知道买不到,再去换行程可能已经晚了,因此,保证一定容量,或者说在高峰期保证容量还是必须的,至于说投入的成本在平时可能浪费,可安排给其他方面使用,或采用在高峰期临时从非要害系统调配资源的手段,很多也都不限于技术层面了  

如果一个系统的平均请求量超过容量,从根本上说是规划出了问题,如果能合理控制流量(比如接入层控制一下用户连接数),似乎可以做到事先预防。但是,当一个系统的平均请求量在合理范围的时候,短期内的突增有时候难以避免,可能导致系统进入不可恢复的瘫痪状态,就像雪崩一样,一个短期的突发事件会导致不可控的后果,典型情况是请求队列突然堆积,导致部分客户端进入反复重试状态,即便这种堆积是暂时性的,请求量也会像雪球一样越滚越大  

2009年,我参与了一个IM系统,核心使用java的一个开源系统,一开始没有任何问题,后面随着业务量上涨,数据库压力增大,参考一个推荐的做法,增大了系统的cache,结果当在线人数达到十万的时候(据一些数据,当时这个系统在整个欧洲在线只有八千多人),jvm内存使用达到了20G,问题就来了,开服5分钟后,垃圾收集启动,耗时20余秒,然后整个系统就进入瘫痪状态,占了几个cpu在疯跑,还啥事都不干,只能停了重启,结果5分钟后又不行了,其实并不是系统本身撑不住,在开始运转的这段时间,还是相当闲的,就垃圾收集机制而言,这个系统用java还不如python的引用计数,虽然后者可能cpu资源占用要多些  

具体解决办法,由于客户端的重试是可控的,上面已经讨论过了,不过由于改开源代码风险比较大,最后通过升级jvm并采用其他一些垃圾收集相关的配置,暂时缓解了这个问题。但可以预见到,随着业务发展,人数再多的时候还是可能出现问题,所以最后这套系统还是自研了,解决方法也很简单:  
1 cache分离,比如用memcached,或jni(这个开发成本高些),减少jvm本身内存  
2 系统分布式,避免所有业务都集中在一个进程上的单点风险  

这个例子中,请求量本身很小,而是因为jvm暂停,相对来说这段时间请求量就大了。反过来也是一个道理,比如一个服务器平时资源占用较小,高峰期CPU满一段时间,也可能导致堆积,因此,服务器进程的资源占用最好控制在相对比较低的范围,尽管表面上会造成资源浪费,但这是为了更长期的稳定  

有的时候,即便各方面都保证了一个服务器的持续运转过程的正常,对其的一些运维操作也可能导致类似情况,例如,一个负责网络传输的服务器进程,对外接入50万长连接,由于系统保证了应用层的数据确认机制,上层数据保证不会丢失,因此运维人员在做维护时候可能直接就停掉这个进程,然后再启动,这方便了系统的维护操作。然而,关闭这个进程会导致所有客户端的连接在很短的时间被关闭,假设客户端的连接重试间隔时间相同(很多系统都随手这么设计),那么当服务器重启后,会在一个很短的时间内面对外面几十万的连接请求,对其他业务请求来说,服务器会在一段时间忙于accept,从而造成堆积,再者,因为listen的队列限制,很多请求不会建立成功,这些客户端的行为又都是一致的,于是在下一个周期中,又有很多连接请求发送到服务器,周而复始,就算服务器不瘫痪也会造成运行不平稳的问题  

解决这个问题的方案也简单,只要将客户端的行为随机化即可,比如说,每次重试间隔时间在10到60秒中随机选一个数字,这样几十万的连接请求就平摊到50秒内,服务器可以很轻易的处理,而且不影响其他业务请求。对一个服务器来说,不但要考虑业务、架构、性能,压力均摊也是很重要的,稳定压倒一切
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值