Redis-感觉-变慢了-检查这些潜在原因 (redis淘汰策略有哪些)
前言
本期继续分享 Redis 相关知识,帮助大家在 Redis 变慢时从容应对,冷静分析问题。为了方便阅读,文章分为上下两篇。
Redis 作为一款业内使用率最高的内存数据库,其性能非常高,单节点的 QPS 压测能达到 18 万以上。当应用访问 Redis 时,响应延迟变大可能会对业务造成巨大影响。
在日常使用 Redis 时,您可能遇到过以下问题:
- Redis 响应变慢,查询缓慢
- Redis 内存占用率过高,导致性能下降
- Redis 服务出现故障或异常
面对这些访问变慢问题,许多人往往不知所措,不知道从哪里下手。这是因为他们缺乏对 Redis 架构体系、核心功能实现原理以及命令使用限制的理解。
本文将一一分析可能导致 Redis 变慢的原因,帮助您在看完上篇后形成一个比较完整的排查思路方案。
Redis 真的变慢了吗?
当服务响应较慢时,首先需要排查内部原因,确定是否是由 Redis 服务导致的。因为系统可能涉及较长的链路和多个服务,例如同一个接口可能调用 MySQL、MQ、Redis 等其他组件和服务。
因此,需要确定访问 Redis 服务是否变慢,从而拖慢了整个服务的响应。首先需要自查:
- 检查系统负载是否过高,导致 Redis 服务响应变慢
- 查看 Redis 日志,查找是否存在异常或错误信息
- 使用 Redis 命令查看当前 Redis 服务器的状态,例如内存使用率、连接数等
通过自查,可以初步判断 Redis 服务是否变慢。如果是,则需要进一步分析具体原因。
后续部分将详细分析导致 Redis 变慢的常见原因,敬请关注下篇。
redis阻塞了怎么办
单线程你别阻塞,Redis时延问题分析及应对
Redis的事件循环在一个线程中处理,作为一个单线程程序,重要的是要保证事件处理的时延短,这样,事件循环中的后续任务才不会阻塞;当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的影响尤为严重;下面我们总结下在redis中有哪些耗时的场景及应对方法;
耗时长的命令造成阻塞
keys、sort等命令
keys命令用于查找所有符合给定模式 pattern 的 key,时间复杂度为O(N), N 为数据库中 key 的数量。当数据库中的个数达到千万时,这个命令会造成读写线程阻塞数秒;类似的命令有sunion sort等操作;如果业务需求中一定要使用keys、sort等操作怎么办?
解决方案:
在架构设计中,有“分流”一招,说的是将处理快的请求和处理慢的请求分离来开,否则,慢的影响到了快的,让快的也快不起来;这在redis的设计中体现的非常明显,redis的纯内存操作,epoll非阻塞IO事件处理,这些快的放在一个线程中搞定,而持久化,AOF重写、Master-slave同步数据这些耗时的操作就单开一个进程来处理,不要慢的影响到快的;同样,既然需要使用keys这些耗时的操作,那么我们就将它们剥离出去,比如单开一个redis slave结点,专门用于keys、sort等耗时的操作,这些查询一般不会是线上的实时业务,查询慢点就慢点,主要是能完成任务,而对于线上的耗时快的任务没有影响;
smembers命令
smembers命令用于获取集合全集,时间复杂度为O(N),N为集合中的数量;如果一个集合中保存了千万量级的数据,一次取回也会造成事件处理线程的长时间阻塞;
解决方案:和sort,keys等命令不一样,smembers可能是线上实时应用场景中使用频率非常高的一个命令,这里分流一招并不适合,我们更多的需要从设计层面来考虑;在设计时,我们可以控制集合的数量,将集合数一般保持在500个以内;比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围;
如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count];随机返回集合中的指定数量,当然,如果要遍历集合中的所有元素,这个命令就不适合了;
save命令
save命令使用事件处理线程进行数据的持久化;当数据量大的时候,会造成线程长时间阻塞(我们的生产上,reids内存中1个G保存需要12s左右),整个redis被block;save阻塞了事件处理的线程,我们甚至无法使用redis-cli查看当前的系统状态,造成“何时保存结束,目前保存了多少”这样的信息都无从得知;
解决方案:我没有想到需要用到save命令的场景,任何时候需要持久化的时候使用bgsave都是合理的选择(当然,这个命令也会带来问题,后面聊到);
fork产生的阻塞
在redis需要执行耗时的操作时,会新建一个进程来做,比如数据持久化bgsave:开启RDB持久化后,当达到持久化的阈值,redis会fork一个新的进程来做持久化,采用了操作系统的copy-on-wirte写时复制策略,子进程与父进程共享Page。如果父进程的Page(每页4K)有修改,父进程自己创建那个Page的副本,不会影响到子进程;fork新进程时,虽然可共享的数据内容不需要复制,但会复制之前进程空间的内存页表,如果内存空间有40G(考虑每个页表条目消耗 8 个字节),那么页表大小就有80M,这个复制是需要时间的,如果使用虚拟机,特别是Xen虚拟服务器,耗时会更长;在我们有的服务器结点上测试,35G的数据bgsave瞬间会阻塞200ms以上;
类似的,以下这些操作都有进程fork;
四个大点,搞懂 Redis 到底快在哪里?
现在我们都用高级语言来编程,比如Java、python等。也许你会觉得C语言很古老,但是它真的很有用,毕竟unix系统就是用C实现的,所以C语言是非常贴近操作系统的语言。Redis就是用C语言开发的,所以执行会比较快。
Redis将所有数据放在内存中,非数据同步正常工作中,是不需要从磁盘读取数据的,0次IO。内存响应时间大约为100纳秒,这是Redis速度快的重要基础。先看看CPU的速度:
拿我的电脑来说,主频是3.1G,也就是说每秒可以执行3.1*10^9个指令。所以说CPU看世界是非常非常慢的,内存比它慢百倍,磁盘比他慢百万倍,你说快不快?
借了一张《深入理解计算机系统》的图,展示了一个典型的存储器层次结构,在L0层,CPU可以在一个时钟周期访问到,基于SRAM的高速缓存春续期,可以在几个CPU时钟周期访问到,然后是基于DRAM的主存,可以在几十到几百个时钟周期访问到他们。
第一,单线程简化算法的实现,并发的数据结构实现不但困难且测试也麻烦。第二,单线程避免了线程切换以及加锁释放锁带来的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。当然了,单线程也会有它的缺点,也是Redis的噩梦: 阻塞。如果执行一个命令过长,那么会造成其他命令的阻塞,对于Redis是十分致命的 ,所以Redis是面向快速执行场景的数据库。
除了Redis之外,也是单线程,Nginx也是单线程,但他们都是服务器高性能的典范。
在这之前先要说一下传统的阻塞I/O是如何工作的:当使用read或者write对某一文件描述符(File Descriptor FD)进行读写的时候,如果数据没有收到,那么该线程会被挂起,直到收到数据。阻塞模型虽然易于理解,但是在需要处理多个客户端任务的时候,不会使用阻塞模型。
I/O多路复用实际上是指多个连接的**管理可以在同一进程。**多路是指网络连接,复用只是同一个线程。在网络服务中,I/O多路复用起的作用是一次性把多个连接的事件通知业务代码处理,处理的方式由业务代码来决定。在I/O多路复用模型中,最重要的函数调用就是I/O 多路复用函数,该方法能同时监控多个文件描述符(fd)的读写情况,当其中的某些fd可读/写时,该方法就会返回可读/写的fd个数。
Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll的read、write、close等都转换成事件,不在网络I/O上浪费过多的时间。实现对多个FD读写的监控,提高性能。
举个形象的例子吧。比如一个tcp服务器处理20个客户端socket。A方案:顺序处理,如果第一个socket因为网卡读数据处理慢了,一阻塞后面都玩蛋去。B方案:每个socket请求都创建一个分身子进程来处理,不说每个进程消耗大量系统资源,光是进程切换就够操作系统累的了。C方案**(I/O复用模型,epoll) :将用户socket对应的fd注册进epoll(实际上服务器和操作系统之间传递的不是socket的fd而是fd_set的数据结构),然后epoll只告诉哪些需要读/写的socket,只需要处理那些活跃的、有变化的socket fd的就好了。这样,整个过程只在调用epoll的时候才会阻塞,收发客户消息是不会阻塞的。
:-D 搜索微信号(ID: 芋道源码 ),可以获得各种 Java 源码解析、原理讲解、面试题、学习指南。
:-D 并且,回复【 书籍 】后,可以领取笔者推荐的各种 Java 从入门到架构的 100 本书籍。
:-D 并且,回复【 技术群 】后,可以加入专门讨论 Java、后端、架构的技术群。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。