今天在网上看到一个问题,感觉其中有很多有意思的东西,回答了一下,分享到这里。
高并发是怎么导致系统崩溃的?
说说我的实际经历。
系统中有个数据处理程序,平常每秒需要处理600条数据,然后某一天有个组件出了问题,导致数据堆积,每秒处理的数据条数达到了3000,这导致数据处理程序的CPU和内存使用率暴增。
到目前为止,这还不是很严重。
但是因为这个程序处理数据时还要调用别的服务,此时被调用服务的CPU和内存使用率也开始暴增,受此影响,服务器上的其它程序因为内存和CPU资源紧张也开始变得运行缓慢,这导致很多程序的CPU和内存使用进一步恶化,然后某些程序因为无法分配内存而退出运行,又因为这些程序很关键,其它依赖它的程序也就跑不下去了,然后整个系统就崩溃了。
这里的关键是高并发导致了大量的CPU和内存资源使用,如果这些资源耗尽,系统就会崩溃。注意这里的并发高是相对资源而言的。
怎么解决这个问题?
核心自然是控制资源的使用,我有几个方法,分享给大家。
1、更高的容量设计。
在设计程序时,我们要根据业务预估它可能的TPS和QPS,并以此为基础制定程序的最大容量。比如预估平均每秒10次查询,高峰期也不会超过20,我们可以再加上一些故障时的富裕量,按照每秒60次查询进行设计。
然后我们设计程序时就要考虑一些问题,比如数据库能不能支持到每秒60次查询,依赖的其它服务能不能支持到每秒60次查询,服务器能不能分配到相应的资源,需要单个实例还是多个实例负载均衡,等等。然后程序写完之后,还要实际进行测试,看看每秒60次查询时程序和服务器实际的表现,比如内存和CPU使用情况,单次查询的响应时间。
当然容量并不是设计的越高越好,越高需要的资源越多,成本也越高。需要结合实际业务情况,在业务和成本都能承受得起的范围内选择。
2、使用协程或异步编程框架。
大家可能听说过Go的高并发能力特别强,原因就是因为Go搞出了协程。协程为什么这么优秀呢?
这是因为传统使用线程模型时,线程消耗的时间和空间成本比较高,时间成本就是CPU成本,空间成本就是内存成本。协程或者异步解决了这个问题,这又是咋回事呢?
在大部分业务程序中存在很多的IO操作,比如访问数据库、网络接口、读写文件等等,IO操作相比CPU操作慢了不知道多少个数量级。使用线程模型时,发起IO请求后,CPU要么等着要么切换给其它线程使用,这个操作浪费了CPU;另外等待IO返回时线程不会消失,而线程占用的内存比较大(Windows默认1M,Linux默认8M)。
在高并发的系统中,使用线程模型时,线程比较多,消耗的CPU和内存资源就会比较高。而使用异步编程时,IO操作提交后,线程就被释放了,等IO返回时再分配一个新线程进行处理,线程少了,CPU切换和内存占用也就少了,自然就可以支持更多的请求了。协程则是在异步的基础上更进一步,把程序执行的最小单位由线程变成了协程,线程的需求更少了。
使用消息队列可以看做是分布式的异步编程。
3、为核心服务分配独立资源。
这种方法就是保证核心服务的稳定,核心服务稳定,则大局不会乱。
当部分业务程序消耗大量的资源时,不会影响到核心服务,那么更多的业务程序就有更大的几率存活下来。这样就可以将影响范围控制到最小,而不会导致整个系统的崩溃。
4、限流、降级和熔断。
这是一些防御措施。
比如某个程序的QPS设计的最大容量是100,且这个100是经过实际验证的,那么我们就可以在qps达到100时拒绝更多的请求,这样可以保证部分业务请求,被拒绝的用户也可以进行重试,同时系统也不会崩溃。虽然限流会影响用户的使用体验,但总好过系统崩溃后谁都不能动。
关于限流,我开源过一个限流组件,支持固定窗口、滑动窗口、令牌桶和漏桶等多种算法,还通过使用Redis支持了分布式统一限流,有兴趣的可以看看这个专栏:FireflySoft.RateLimit - 萤火架构的专栏
降级说的是什么呢?如果某个服务持续过载,那么我们可以停止它。比如,服务原来的服务等级是99.999,停止后可能就变成了99.9,这就是降级,降低了服务等级。降级可以手动操作,也可以根据监控指标自动设置。
说到降级还有一个熔断,说的是如果我们访问某个服务持续报错,那么就可以减少对这个服务的调用,甚至完全停止调用。此时程序可以直接返回错误。熔断可以避免系统变得更加糟糕,因为一般程序出错时可能会重试,重试则会放大请求量,造成更大的并发。有了熔断可以让被调用的服务缓一会。
5、建设报警机制。
所有的程序基本上都无法完全消除BUG,也都存在一个可能的崩溃条件。为了及时发现并发突增,可以对请求数、网络流量、服务区的CPU和内存使用等情况进行监测,如果突破了设定的限制,就可以向系统维护人员进行报警,然后人工介入进行处理。
以上就是我的一些经验,希望对你有用。
欢迎关注我的公众号:萤火架构,技术提升不迷路!