关于软件设计的两三事:为什么缓存可以撑住高并发请求,数据库却不能?
人生苦短,不如养狗
作者:闲宇
公众号:Brucebat的伪技术鱼塘
在之前一篇谈论高并发场景的文章中,在分析引入缓存可以缓解数据库压力时,闲宇曾提出过这样一个问题:为什么缓存可以撑住高并发请求,数据库却不能?当时由于篇幅限制,并没有做过多的分析,所以今天这篇文章我们就来具体分析一下这其中的原因。
一. 高并发场景下,数据库为什么会崩溃?
要想弄清楚“缓存可以,但是数据库不可以”的问题,我们需要先弄清楚在高并发场景下数据库撑不住的原因。
1. 大量请求导致资源耗尽
在高并发场景下,海量的请求意味着海量的网络IO以及读写操作。
每一个网络请求触达数据库后都需要在内存中创建一个文件描述符,海量的网络请求意味着需要创建海量的文件描述符,当然这是有上限的,不过依然需要占用较多的内存资源。而为了处理这些网络IO,CPU也需要不断地去响应对应的网络中断,这就意味着需要占用不少的CPU时间,除此以外,还需要加上上下文切换的开销。至此,对于一个单机的数据库来说,在还没有进行任何实质SQL处理的请求下,内存和CPU就已经消耗了不少。
而当SQL执行请求真正意义上进入到数据库中开始执行后,由于存在强一致性要求,数据库中的事务和锁机制会导致大量的事务堆积在锁等待队列当中无法执行,同时这也会导致数据库线程池耗尽。长时间的等待会触发数据库的锁等待超时机制,此时数据库就会尝试回滚那些超时等待的事务。需要注意,此时回滚操作本身就是一个非常消耗CPU和I/O资源的操作。在这种情况下,可用资源会被进一步压缩,此时回滚操作与正常事务之间会产生资源的竞争,接着就会形成“处理回滚 —> 资源紧张 —> 锁等待超时 —> 更多回滚”的死亡螺旋。
而当I/O资源消耗后,Redo Log就会无法及时落盘,所有的数据修改操作(包括正在执行的)都会被阻塞在log_wirte
阶段,也就是提交阶段。然后再一次出现锁等待超时,接着又一次进入到上面说的死亡螺旋中。
到这里,数据库服务内部其实已经完全卡主,处在一种活锁的状态无法自拔。此时,已经接收的网络请求会一直保持,由于内部资源紧张,对应请求一直无法处理,这些请求也不会被释放,对应的内存也会被一直占用。而在应用层则会出现大量的响应超时,从而触发重试机制,以至于更多的请求到达数据库,内存被进一步耗尽。
2. 资源耗尽触发崩溃
当所有的内存被消耗完毕后,操作系统的OOM Killer就会找上门来,强行将数据库进程终止掉,最终崩溃。除了OOM Killer,数据库还可能走上另一个结局,就是由于内部的死锁检测机制由于资源不足无法执行导致超时,InnoDB主动触发崩溃,导致MySQL服务停止。
二. 缓存为什么能避免崩溃
从上面对于数据库崩溃原因的分析可以发现,原因有二:
外部机制:由于资源耗尽导致出现OOM,最终触发操作系统的OOM Killer强制终止进程;
内部机制:在资源耗尽情况下,MySQL的死锁检测超时机制会导致InnoDB主动触发崩溃,最终MySQL服务停止;
但是对于Redis这类常用于缓存的内存数据库来说,却并不会受到这两个原因的困扰。
首先,Redis内部并没有像MySQL一样存在死锁检测超时机制。因为Redis只拥有简单的key-value
结构(当然,也没有那么简单)和无锁结构,所以并不需要数据库中锁相关的机制。同时,在Redis的事务本身就是串行执行的,且不会进行回滚,所以并不会出现资源竞争和回滚所需要的CPU以及IO开销。
第二,在Redis当中使用了非常多的措施来避免触发OOM Killer。具体措施如下:
内存淘汰策略:比如Redis提供了非常丰富的内存淘汰策略,当内存使用量达到阈值时就会触发对应的淘汰策略去释放内存,缓解当前的内存压力。
持久化机制:又比如Redis使用了提供了RDB和AOF两种持久化机制,用于将内存中的数据定期或者按需同步到磁盘当中。
内存碎片整理:Redis提供了诸如动态内存扩展和内存碎片整理命令等一些机制来减少内存碎片的影响。
分片:官方提供的Redis Cluster方案可以将数据分布到多个Redis实例中,有效分散单个实例的负载,避免单点内存瓶颈。
通过这些方式,Redis能够非常高效地使用内存,避免触发OOM Killer。当然,也会存在一些情况导致Redis崩溃,比如未设置maxmemory
或配置的值超过物理内存。需要注意,Redis这类内存数据库之所以能使用这些措施,最关键的一点就在于它们并不追求强一致性。
三、总结
通过上面的分析,我们可以了解到,数据库之所以会在高并发场景下因资源耗尽而崩溃,主要是在于操作系统本身存在一种名为OOM Killer的保护机制。这是一种用于在系统内存耗尽时防止系统崩溃的机制。而为了避免触发OOM Killer这一保护机制,Redis在内存管理上做了非常多的优化措施。这就是为什么前者抵御不住高并发场景下的洪峰,而后者可以。
当然,上面的分析更多是针对可用性的讨论,而没有对缓存的高性能做更多的讨论,有兴趣的朋友可以自行了解一下。
最后,新的一年,希望各位打工人、同学们都能身体健康,心想事成,早日暴富。新年快乐~~