其他
探秘解析:服务器过载处理方式
本文将阐述过载相关的内容,这些内容是总结和分析了常用的一些过载处理的方式,并结合为我们系统开发过载保护中所遇到和用到的一些方法,期望能够给予大家在处理过载问题的一些参考。限于个人能力的问题,考虑不够全面,其中可能会出现错误,希望能够批评指正,不吝赐教,加以探讨。
2何为过载
“过载”一词,在海量服务的后台开发中,基本都会遇到。何为过载,即当前负载已经超过了系统的最大处理能力。例如,系统每秒能够处理的请求是100个,但实际每秒的请求量却是1000个,就可以判定系统出现了过载。过载的定义看似简单,但却是处理过载问题的关键。对于任何其他问题,同样得抓住问题的本质,方可不偏离问题核心,万变而不离其宗。
3过载后果
“过载”的出现,会导致部分服务不可用,如果处置不当,极有可能引起服务完全不可用,乃至雪崩。我们的系统中,由于是单线程状态机的处理模式,顺序处理所有链接的缓冲区消息,当出现处理能力的下降或者请求量大幅增加,导致处理能力小于请求量的情况下,消息就会在系统缓冲区中堆积,造成消息处理的延迟会持续增加,在正式环境中,链接数目较多,系统缓冲区较大,最终会导致消息处理延迟大到不可接受的程度,最终会导致处理的都是无效消息,造成服务不可用。当然具体的业务需要具体的分析,把握住问题的影响,才能够做到一切尽在掌握,根据“墨菲定律”,通常对后果的判断不应过于乐观,谨慎行事、考虑充分才能够做到胸有成竹。
4过载原因
“过载”的出现,不同系统模型的具体原因都会有所不同,例如CPU跑满,频繁读写导致IO瓶颈,内存耗尽,请求量突增等等。但究其根本原因,可以归结为两点:1、处理能力的下降;2、请求量的上升。只有对自身系统的有更深层和透彻的了解,才能更好地考虑如何处置问题。“头疼医头,脚疼医脚”的处理问题方式,只能解决一时之需,对症下药,才是解决问题的根本之道。
5过载保护
任何问题的保护行为可以依据事件发生的阶段分为:1、 发生前,预防;2、 发生时,处置;3、 发生后,恢复。但在保护的措施中,都和业务的模型有着相关性,没有完全统一的方案,适合自己的才是最好的。
5.1过载预防在过载发生前的预防,就需在系统设计之初,依据具体的业务模型可以考虑预防过载的措施:1、优化服务处理流程,降低处理资源消耗,提升自身处理能力;例如CPU消耗型服务,是否可以考虑优化算法,提升处理能力。2、分离处理模块;将负载分担到不同的模块或者服务器;例如IO是瓶颈的服务,考虑是否可以将IO模块进行分离。3、负载均衡;将请求量分流,降低单服请求量。4、轻重模块分离;重要模块单独部署和处理,防止模块之间的互相影响。5、前端防御;在前端控制请求频率,缓解后端压力;例如客户端可以做保护措施,控制聊天频率,点击操作失败,可以延时一段时间,才允许用户继续点击;前端服务发现后端出现过载问题,可选择性拒绝服务,降低后端压力。6、使用缓冲区;缓冲区的使用,可以帮我们抵挡请求量的抖动,但缓冲区的使用同样也有很多技巧,并非越大越好。首先需要考虑内存,cpu等资源的开销,业务的模型是否需要这么大的缓冲区。例如缓冲区过大,处理完整个缓冲区,都需要几十秒,而前端等待超时则为几秒,那么每次处理缓冲区的内容,都是旧的,前端认为都是超时,服务完全不可用。另外是后端却又处理成功,会导致系统信息不对称,从而导致更为严重的问题,例如,在游戏中购买道具的场景,前端扣用户的钱,认为超时失败而不给用户发对应的物品,后端却又执行成功了,严重运营问题就此产生。7、做好监控,及时告警;例如当CPU达到80%时,当处理请求超出一定阈值时,及时告警,做好扩容,优化等其他准备。当然依据业务模型的不同,还有很多预防的措施,依然是前述做到知底,才能够找出适合自身的方法。
5.2过载处置世界上不存在绝对完美的系统,我们不是上帝,出现问题是必然的。但出现问题并不可怕,关键是否能够处置好问题。过载的出现,理论上都有可能产生,向任何向外提供的服务,发起DDos攻击,都可以认为是过载的发生。在发生过载的情况下,处置不好的话,很可能出现下列情况:
处理过载的方法有许多,适用于不同的业务场景,并无绝对的最优方案,合适的才是最好的,但能匹配上“合适”一词,是对系统整体和经验的一个考验。下面介绍一些常用的处理方案以及我们是如何做的:
请求量阈值控制在系统部署上线之前,预估好系统的处理能力,限定最大同时能够处理的请求量、流量或者链接数。当请求量快接近于最大处理能力时,则告警,超过范围,则触发拒绝请求机制。由此可见对于阈值的设置是一个很关键的环节,阈值过高,依然可能导致过载,阈值过低,则又导致负载上不去。阈值的设置也会是一个不断调优的过程。该方法的优点和缺陷都很明显。优点:识别和处理简单;缺点:阈值的设定需要一定的经验,会有一定的难度,同时如果处理能力发生变化时,阈值就很难动态发生变化。
监控系统资源服务器监控CPU,内存等资源的使用情况,设定阈值,超出阈值,则可以认为过载,从而触发拒绝请求机制。优点:使用动态的资源数据,从相对根本的原因上识别过载,而无需过多关心具体的业务处理;缺点:一是处理相对复杂;二是在某些场景下,资源数据的耗尽并不意味着出现过载的情况。例如服务开了较大的内存池,看起来内存资源耗尽了,实际上负载是足够的,又如现在都是多核服务器跑着多进程或者多线程的服务,单一的CPU耗尽也不能够代表服务就出现过载,但又可能产生过载,这就和具体业务有关;三是在某些场景下,出现过载的情况,也不一定会耗尽资源,例如当前所有的服务都在等待之中(可能是后端的回复或者其他),同样也不会对CPU、内存、io、网络等资源造成影响,但依然进入了过载。总体来说该方式适合的场景相对会简单点。
检测请求到达时间依据请求处理的时延来判断是否过载。记录请求到达的时间戳,和处理请求结束的时间戳,得到请求到达自身服务器处理的时延,超出阈值,则可判定为超时失效,可以直接丢弃。使用独立模块读取系统缓冲区中数据,打上时间戳,存入消息缓冲区,在处理时,超过一定时延的请求,则拒绝处理,因为可以认为即使处理了也是无用的。从中可以看出时间戳很关键(为啥会单独提出这个问题,因为在后续的方案设计中,时间戳依然是解决过载问题的关键点,此处先卖个关子)。A、 时间戳如果使用本地读取时刻调用系统的时间函数获取,就没有考虑消息包到达系统缓冲区的时间,因此是万万不能这样做。B、 到可以通过ioctl调用SIOCGSTAMP的接口,获得时间戳,但这会加大系统开销,原因是每次recv完,都需要重新设置一下ioctl一次。并且不是线程安全的。C、 使用socket选项SO_TIMESTAMP,通过带外数据获取到数据到达系统缓冲区的时间。其处理方式如下图所示:
1、完全使用时间戳过期的方式来判断,并不一定适合所有场景,假设处理耗时过长,而在缓冲区中也呆了较长时间,但请求量并不大,服务器未过载,在处理一些需要强写入的情况下,单靠该机制也会稍许欠妥。但如果加入一些协议上层机制,告诉该消息务必执行,也是可避免的。
2、在出现过载的情况之下,很可能会导致整体的服务都会产生一个固定的延时,因为每次抛弃到可执行的范围内,至少会有一个超时时间范围内的延时,如果是较长的服务链的话,最前面的等待服务很可能会出现超时,因此其延时的设置相对也很困难,过小就太过灵敏,过大就会出现刚所述的问题。
4、剩下还有一些内容可以做更多优化:另外SO_TIMESTAMP使用的是系统时间,会受系统时间修改的影响,但这个问题也不大,因为即使修改了,影响的只是本次系统缓冲区的数据。其他可以考虑业务的轻重程度,做按服务来丢弃。
5.2.2 如何拒绝服务提供优雅的拒绝服务,同样也是十分重要的。依然还是那句话:合适的才是最好的。以下提供一些点给与做参考:
1、 业务链反馈控制;当业务链某个环节出现过载时,让业务链前部分请求都感知到业务链过载,采取控制请求的措施,降低问题环节负载,加快服务反馈处理。
2、 处理有效请求;正如检测请求到达时间的过载处理方案一样。我们丢弃掉过期的请求,保留有效期内的请求。但对于有效的定义也是因业务而异的,此处也不加以赘述。
3、 保证重要业务的处理,放弃非重要的业务处理;重要与非重要的权重,则与业务属性的定义就强相关了。
4、 丢弃耗时长的请求,处理耗时短的请求;在一个提供多样服务的系统,出现过载的情况之下,用一个耗时长的请求处理换取多个耗时短的请求处理,可能也是值得的。
5、 提供有损服务;例如在拉取个人信息的服务中出现了过载,可以有选择的拉取重要信息,不去执行其他非重要的逻辑,提高处理速率,提供有损的服务。
6、 丢弃外部请求,处理回包数据;系统在处理外部请求时,本身也会产生对外的请求,处理对外请求的回包,保证正在处理的请求能够正常处理完成。否则丢弃掉,反倒导致对已经处理的流程失败,得不偿失。
5.3 我们的过载处理方案采用前文所述的检测请求到达时间的方式处理过载问题,理论上也能够处理我们的过载问题。但考虑到系统的结构:首先并没有开辟专门的收取消息的模块和应用缓冲区;其次考虑到是否能够将该方案的问题1解决(非过载的情况之下,能够尽力处理的问题);最后需要不修改已有的业务层面内容。由此我们得出了一个新的处理思路。该思路主要包括三方面:过载识别,过载处理,过载恢复。看似和前述方案有相似之处,但细节上面还是有较大的不同,且看后续论述。
5.4 过载识别通过前文所述,我们可以用多种方案做,过载识别,如阈值控制,资源检测,时间等等。此处我们提个新方案:通过对比处理能力和外部请求量大小来识别过载,当请求量超过处理能力的80%,则判定为过载,触发过载处理。80%只是个经验值,触及到这个量,就应该告警,考虑优化扩容事宜。处理能力,难道不就是前面所述的配置的处理阈值么?但它不会动态变化,我们可以考虑对处理能力进行计算。而请求量,则是由前面一段时间所统计得到。
5.4.1 处理能力的计算处理能力可以定义为在单位时间内,系统能够处理的数据大小。我们系统框架的执行模型大致如下:
由前可知,统计时间C和单位时间,是需要设定的一个数值,目前该数值为30秒和5秒,经过测试可以满足要求。两个数值越大,过载识别的灵敏度就会越低,越小,则统计会过于频繁,耗费资源,且有抗抖动能力不够。
5.5 使用时间片处理过载
据前所述,我们可以采用到达本机系统缓冲区的时间来判断数据包是否过期,但由于相关的一些缺点,并且已有系统的问题,并不方便增加应用缓冲区等问题,而考虑其他的方案。相对较佳方案,兼顾服务质量,我们可每条协议中都带上请求的过期时间戳,过期了就直接将该包丢弃。但很可惜由于历史原因,协议中并没都带上时间戳,协议要全部做修改,几乎不可能,并且由于时间校准等问题,并不方便做修改,因而也放弃了。
最初还有一个方案,考虑到过载时刻,极可能对端的系统缓冲区也塞满了数据,则将链接断开再重新简历,缓冲区中的数据自然就会被清空,但该方法过于暴力,而且使用断开链接之后,还需要重新注册服务,其有效处理能力会下降许多。最后也会对此方案做测试数据对比。
目前所用到的方案,考虑到中转服务器(接入服务器和Proxy)会与服务器之间进行Hello包保活,而Hello包中有时间戳,依据该时间戳,连续两个Hello包间隔之间的数据,处于同一个时间片之中。另外很重要的一点是,我们内部链接都是TCP/IP长链接,这样数据包必然会保持一个有序的状态。因此变相将各个包的时间粒度放大,由此来达到过载的控制。该方案的好处,一是考虑到了对端的时间;二是将粒度放大,无需每个请求包都需要判断时间,只需要判断Hello包中的时间戳;三是真正过载的时刻,需要丢弃的包往往数量很大,通过每秒的Hello拒绝丢弃,也可提高丢弃的速率,相对较快的找到有效包。
5.5.1 算法
算法流程图如下所示:
1、 如果一个循环内执行时延超过一定阈值(可设置成较长时间),我们就有理由可以断定当前的状态是处于过载,立马触发过载保护。这样做的目的,主要是由于框架是单线程的处理模式,等到每次计算处理能力和请求量的时候,有可能就反应迟钝了。
2、 时间戳由于各种问题修改会导致各个服务器的unixtime不一致的问题,同时没有较好的时间同步机制,解决该问题的方法,在后续将详细阐述;
3、 只会丢弃请求包,对于通知和响应的消息包,不会丢弃,其原因前面也有所描述,此处不再赘述;
4、 如果最新Hello包中的时间戳小于本地记录的Hello时间标尺,会将该本地记录的Hello时间标尺替换;
5.5.2 Hello包中的时间戳之前我们使用gettimeofday或者time函数取得系统当前的时间,该函数返回的是unixtime,但都会收到本地时间设置的影响。主要会存在以下两个问题:1、不同服务器之间时间不同步;2、本地时间修改;解决这两个问题,分别采取了以下两个对应的措施:差值比对;确认当前收到的消息的是否过期。
1.服务器启动情况之下,在1s时刻B收到了A的Hello包,B记录其时间戳TB1(10s),同时记录接收到的本地时间戳TA1(1s),获得其中的差值∆T1(9s),将这些数据作为标尺。
2.当B接收到了第2个Hello包时,同样计算两端服务器时间戳差值∆T2 (9s),比对和,如果处于阈值范围之内,就表明数据没有过期。
3.当B接收到了第6个Hello包时,计算得到差值为∆T6 (7s),与标尺差值∆T1,发现超出了阈值,如果此时在已经识别出过载的情况之下,则会丢弃后续的来包,直至新的符合要求的Hello包到达。由此可以消除不同服务器之间时间不同步的问题,另外时间戳的粒度以秒为单位就会过粗,因此是以0.1秒为单位,同时参考上述算法,时间标尺是会根据情况进行重置的。
另外一个很重要的问题就是unix时间会受系统时间的改变而改变,那在过载的情况下,有人或者工具重新设置了一下时间戳,就乱了呢?
时间戳的选择;方法一:我们查找可以使用TSC的方式,来获取精确的时间,且不会因为系统时间的改动而改动,我们假设CPU主频是1MHZ,那么TSC就在1秒内增加1000000。那么获取当前时间伪代码就很简单了。当前时间=时间模块的启动时间+(TSC当前值-TSC初始值)/主频,但该时间由于计算的问题,可能会存在一定的偏差。
方法二:使用clock_gettime函数,使用CLOCK_MONOTONIC或者CLOCK_MONOTONIC_RAW参数。代表从过去某个固定的时间点开始的绝对的逝去时间,它不受任何系统time-of-day时钟修改的影响,如果你想计算出在一台计算机上不受重启的影响,两个事件发生的间隔时间的话,那么它将是最好的选择,但该时间自系统开机后就一直单调地增加(ntp adjtimex会影响其单调性,目前对于我们的需求是足够的),但它不像因用户的调整时间而产生跳变。而CLOCK_MONOTONIC_RAW是完全不受任何影响,是一个绝对的单调递增,是绝佳的选择,但其只能在linux较高版本中使用。
综合考虑,我们目前使用的方法二的CLOCK_MONOTONIC的方法。到此,我们上述的时间戳的问题,就得以解决了。
5.6 过载恢复
过载控制的恢复,需要同时满足以下两个条件才可以恢复:1、 请求量低于处理能力;2、 所有链接都不处于丢包状态;因为如果处于过载丢包状态,其处理数据量的速度是十分快的,如果单用条件1进行判断,一般都能够满足,但此时还是处于过载状态。
6测试
6.1测试方案
该消息就是命令测试服务器等待一定的时间,使用等待时间的变化来模拟处理能力的变化。
包中也有生成包的时间戳,处理时,会判断该时间戳是否过期,使用该方法来统计执行的是否有效包。
所有接入服务器的Hello包间隔为1秒,过高,则有效包执行较低,过低,则粒度过细,需要判断的次数较多。从后续效果上看,1秒的时间间隔是一个较好的选择,但依据业务的不同,可以设置不同的时间间隔。
由此来测试服务器的过载识别,处理能力,及恢复能力。在测试中,我们关注两个重要的数据:1、有效处理额定比率;即发生过载之后,能够处理的有效包,占理论处理能力的比率。比率越高,效果越好。2、拥塞恢复时间;即过载停止后,从过载状态恢复到正常的时间。时间越短,效果越好。
6.2 原始状态
以下是未做任何过载保护的处理,可以发现不做过载保护的服务,可用性是极差的。
6.3 时间片处理过载
由于本版先后采用了两种过载识别方案:一种是检测循环执行时间和直接使用时间戳是否过期的方式来判断过载;另外一种就是当前所使用的动态过载识别的方式。
6.3.1 非动态过载识别
6.3.2 动态过载识别
先给出一组数据,可以先看下我们计算出来的处理能力和请求量:
1、 当服务器没有请求时;此时每秒都会收到来至于接入服务器的Hello包,通过下图我们可以看出,每3秒的请求量是1056字节,我们计算出能够处理的请求量大概在2.8M左右,每次主循环大概有567us耗费在处理其他事物上。
收到大量处理耗时较高的数据;
7 后记
该算法在调整的过程途中,经过了N次修改,并做过多次实验对比,这是一个慢慢调优的过程,还有很多被推翻的实验没有在文中介绍。其中还有很多有关系到实现细节未详细讲述,还是需要详细了解代码才能够观察到,该算法也还需要在正式环境中运行才能逐步稳定,如有bug请联系我。限于系统实现的问题,许多在过载处理时候的内容,并没能够添加到算法之中,后续可以做更多的优化。