一个系统过载的案例及其解决办法
系统出现过载现象 (或问题) 的原因和场景有很多,这里并不试图归纳总结;而是如题,就一个特定的案例,分享一些过载保护的实践办法。
案例
系统 R 需要通过轮询 (读取) 数据库中存储的记录状态,进行一些业务补偿的操作,而后再对数据库的状态进行更新。
轮询机制实现并未采用定时周期的方式,而是采用 请求 - 响应 - 再请求 的事件驱动方式。
在压测的时候发现,数据库访问 (无论读写) 大量失败,错误日志狂刷。
分析
经排查,由于数据库被多个系统公用,其它系统的一些 SQL 执行耗时超长,资源占用过多,导致系统 R 的数据库访问请求超时失败,而失败后的不断重试,变本加厉,最终数据库撑不住挂了。
类似的典型场景,如 秒杀 :
海量并发请求下,系统已经出现过载,请求响应过缓,而用户耐不住,则不断尝试刷新页面,寄期望于请求被响应。可事与愿违,系统在没有保护的情况下,将持续超过载的状态,直到 宕机.
解决
面对过载,可能最容易被想到的是 扩容. 是的,它是一种不降低服务质量的必不可少的预防方案,但我们不得不接受一个事实:
系统的容量 (或处理能力) 始终是有限的,即使不考虑成本的扩容,也只能应付你可以预见程度,一旦有个”万一”, 系统仍将崩溃。
若服务可以降级,则 限流 是能够应对”万一”的良方,下面就 限流 这个思路,分享几个办法。
定时
将轮询改用定时来实现,就可以保证稳定的访问节奏,加之一旦支持动态修改定时周期时间,便可以根据实际情况灵活调整节奏了。
不仅做到了 限流 , 更是做到了 流控, 不错。
可以实际应用中会发现,初始的定时周期很难找到合适的值,需要在处理实时性和请求量之间平衡。
个人不太喜欢用定时方案,它不仅让单元测试结果不稳定,还浪费线程。
休息
请求 - 响应 - 再请求 的事件驱动的轮询方式,有个好处:就是在正常情况下,让访问节奏由数据库说了算,即数据库响应快,轮询则快,反之则放缓。只可惜异常的情况下,就如案例中那样。
休息很好理解:
若人累了,就不要继续强撑,让身体休息一会 (踹口气), 再恢复工作。
同理,当数据库访问失败后,优雅地拒绝掉随后若干次请求,让数据库休息一会。
如何做到优雅?就本案例而言则是,读取失败后的若干次请求迅速返回空集合。总之,拒绝可以是除抛异常外,任何对处理逻辑有意义的默认NULL结果。
若干次到底是多少次?这与选择定时时间一样是需要平衡的。
补偿
本案例中轮询的读取失败返回空集合是个好的休息办法,可要是状态更新失败呢?没法返回结果,只能抛异常,但异常又会导致重试,这不仅没让数据库休息,且可能导致大量ERROR Stacktrace日志输出。
不抛异常,而将异常转换为一次失败记录 (日志), 这些记录可以用来对数据库状态的不一致进行补偿操作,具体如何补偿,什么时机开始补偿,这里就不展开细说了,它们都需要由业务场景来决定。
写在最后,上述三种办法都不是银弹,也并非互斥,完全可以根据情况结合使用的。
The end.
