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