上一篇介绍了 Redis 作为缓存服务器的问题。这节开始介绍 Redis 性能方面的问题。Redis 的性能主要会受以下几方面的影响。
由于使用了 IO 多路复用机制,避免了主线程一直处于等待网络连接或请求到来的状态,所以网络 IO 不是导致 Redis 阻塞的因素。但是如果网络 IO 过多,也会影响 Redis 的性能。
Redis 采用子进程的方式生成 RDB 快照文件和执行 AOF 日志重写操作,但是将 AOF 重写缓冲区中的数据追加到从服务器时,仍然会阻塞主进程。
主库在复制的过程中,创建和传输 RDB 文件都是由子进程来完成的,不会阻塞主线程。
但是,对于从库来说,它在接收了 RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,会阻塞从库(如果数据量大,需要较长的时间)。
此外,从库在清空当前数据库后,还需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢。
当我们部署 Redis 切片集群时,每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传递,同时,当需要进行负载均衡或者有实例增删时,数据会在不同的实例间进行迁移。
如果你使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移。
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。
主线程通过一个链表形式的任务队列和子线程进行交互。当收到操作命令时,主线程会把这个操作封装成一个任务,放入任务队列中,然后给客户端返回一个完成信息,表明任务已经完成。但实际上,这操作还没有执行,等到后台子线程从任务队列中读取任务后,才根据操作类型开始实际相关操作。
异步删除也称为惰性删除(lazy free)。删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。
lazy-free 是 Redis 4.0 新增的功能,默认是关闭的。
开启 lazy-free 后,Redis 在释放一个 key 的内存时,首先会评估代价,如果释放内存的代价很小,那么就直接在主线程中操作了,没必要放到异步线程中执行(不同线程传递数据也会有性能消耗)。
对于不同的数据类型,lazy-free 的释放策略也不同:String(不管内存占用多大)、List(节点数量小于 64)、Set(int 编码存储)、Hash/ZSet(ziplist 编码存储,或非 ziplist 编码存储且元素数量小于 64),这些情况下的 key 在释放内存时,依旧在主线程中操作。可见,即使开启了 lazy-free,String 类型的 bigkey,在删除时依旧有阻塞主线程的风险。所以还是尽量不要在 Redis 中存储 bigkey。
如果在 CPU 多核场景下,Redis 实例被频繁调度到不同 CPU 核上运行的话,每调度一次,一些请求就会受到运行时信息、指令和数据重新加载过程的影响,这就会导致某些请求的延迟明显高于其他请求。
当上下文切换发生后,Redis 主线程的运行时信息需要被重新加载到另一个 CPU 核上,而此时,另一个 CPU 核上的 L1、L2 缓存中,并没有 Redis 实例之前运行时频繁访问的指令和数据,所以,这些指令和数据都需要重新从 L3 缓存,甚至是内存中加载。这个重新加载的过程需要花费一定时间。而且,Redis 实例需要等待这个重新加载的过程完成后,才能开始处理请求,所以,这也会导致一些请求的处理时间增加。
可以通过绑定 Redis 实例和 CPU 核,可以有效降低 Redis 的处理时间。为了提升 Redis 的网络性能,有时还会把网络中断处理程序和 CPU 核绑定。
如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket(CPU 处理器)上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间。
所以,为了避免 Redis 跨 CPU Socket 访问网络数据,最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上,这样一来,Redis 实例就可以直接从本地内存读取网络数据了。
把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。
此时可以把一个 Redis 实例绑定到一个物理核上,这样,Redis 的主线程、子进程和后台线程可以共享使用一个物理核上的多个逻辑核。
也可以修改 Redis 的源码,把子进程和后台线程绑到不同的核上,从而避免对主线程的 CPU 资源竞争。
Redis 键值对的 key 可以设置过期时间。默认情况下,每 100 毫秒会删除一些过期 key,具体的算法如下:
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一个参数,默认是 20,那么,一秒内基本有 200 个过期 key 会被删除。
如果每秒钟删除 200 个过期key,并不会对 Redis 造成太大影响。但是如果触发了重复删除,Redis 就会一直删除以释放内存空间。删除操作是阻塞的(Redis 4.0 后可以用异步线程机制来减少阻塞影响)。所以,一旦该条件触发,Redis 的线程就会一直执行删除,这样一来,就没办法正常服务其他的键值操作了,就会进一步引起其他键值操作的延迟增加,Redis 就会变慢。
造成这种现象的原因大概率是大量数据同时过期。
Redis 的持久化会保存数据到磁盘,这个过程要依赖文件系统来完成,文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率,从而影响到 Redis 处理请求的性能。
改善办法:采用高速的固态硬盘(SSD)作为 Redis 日志的写入设备;采用合理的持久化策略。
Redis 是内存数据库,内存使用量大,如果没有控制好内存的使用量,或者和其他内存需求大的应用一起运行了,就可能受到 swap 的影响,而导致性能变慢。
内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写,所以,一旦触发 swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。
改善办法:增加机器的内存或者使用 Redis 集群。即纵向扩展和横向扩展。
Transparent Huge Page, THP,Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。
虽然内存大页可以给 Redis 带来内存分配方面的收益,但是在写时复制时(生成 RDB 时、重写 AOF 时),需要拷贝大页,会导致性能变慢。
使用 Redis 时不推荐使用内存大页。
碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。
为了尽可能减少碎片清理对 Redis 正常请求处理的影响,自动内存碎片清理功能在执行时,还会监控清理操作占用的 CPU 时间,而且还设置了两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,既保证清理工作能正常进行,又避免了降低 Redis 性能。这两个参数具体如下:
缓冲区主要就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。但因为缓冲区的内存空间有限,如果往里面写入数据的速度持续地大于从里面读取数据的速度,就会导致缓冲区需要越来越多的内存来暂存数据。当缓冲区占用的内存超出了设定的上限阈值时,就会出现缓冲区溢出。
为了避免客户端和服务器端的请求发送和处理速度不匹配,服务器端给每个连接的客户端都设置了一个输入缓冲区和输出缓冲区。
输入缓冲区会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理。
Redis 的客户端输入缓冲区大小的上限阈值,在代码中就设定为了 1GB,并没有提供参数让我们调节客户端输入缓冲区的大小。
可能导致溢出的情况主要是下面两种:
当 Redis 主线程处理完数据后,会把结果写入到输出缓冲区,再通过输出缓冲区返回给客户。
一般来说,主线程返回给客户端的数据主要分为两种:
因此,Redis 为每个客户端设置的输出缓冲区也分为两部分:
可能导致溢出的情况主要有:
缓冲区的另一个主要应用场景,是在主从节点间进行数据同步时,用来暂存主节点接收的写命令和数据。
在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
复制缓冲区一旦发生溢出,主节点也会直接关闭和从节点进行复制操作的连接,导致全量复制失败。
主节点上会为每个从节点都维护一个复制缓冲区,如果集群中的从节点数非常多的话,主节点的内存开销就会非常大。所以,我们还必须得控制和主节点连接的从节点个数,不要使用大规模的主从集群。
复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制。
本文介绍了影响 Redis 性能方面相关的几个问题。
至此我的 Redis 学习笔记就全部更新完毕了。因为只是为了学习 Redis 和应付面试,并没有涉及到很多应用方面的内容,也没有处处深入到源码分析,而且内容的覆盖面也不够广,希望以后有机会可以继续学习完善吧。
最后再扯点废话,我个人的话是在准备 24 秋招,方向是后端开发,语言可能会使用 Java 或者 Go。目前已经学完了计网、操作系统、MySQL、Redis,其中操作系统和 Redis 方面已经写了不少博客了,计网和 MySQL 打算在后续也会整理成博客。然后是算法方面,虽然在力扣上刷了很多题,但是真正遇到笔试还是感觉很吃力,后续打算再系统性地查漏补缺一下。之后就主要学习语言方面了。
这两个月一直在找实习,但是感觉真的挺难的,以往有面试机会的大厂,今年不是简历挂就是笔试挂,也有很多比我优秀得多的人也被挂了,除了感叹大环境不好外也没有别的办法了,说实话还是有点难受的。但是也没有那么多一帆风顺吧,还是得整理整理心情继续学习,争取能在秋招有个好成绩!