redis 性能变差的原因
redis 性能变差的原因?
1.使用复杂度过高的命令
如果你的应用程序执行的redis命令有以下特点,那么有可能会导致操作延迟变大:
- 经常使用O(N)以上复杂度的命令,例如SORT,SUNION,ZUNIONSTORE聚合类命令
- 使用O(N)复杂度的命令,但N的值非常大
第一种情况导致变慢的原因在于,redis在操作内存数据时,时间复杂度过高,要花费更多的CPU资源。
第二种情况导致变慢的原因在于,redis一次需要返回客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。
解决方案
- 尽量不适用O(N)以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
- 执行O(N)命令,保证N尽量小,每次获取尽量少的数据,让redis可以及时处理返回
2. 操作Bigkey
Redis在写入数据时,需要为新的数据分配内存,相对应的,当从redis中删除数据时,它会释放对应的内存空间。
如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey。
解决方案
- 业务应用尽量避免写入bigkey
- 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
- 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行
3.集中过期
如果有大量的 key 在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延时变大。
为什么集中过期会导致redis延迟变大?
这就需要我们了解redis的过期策略是怎样的。
redis的过期数据采用被动过期+主动过期两种策略:
- 被动过期: 只有当访问某个key时,才判断这个key是否已过期,如果已过期,则从实例中删除
- 主动过期: redis内部维护了一个定时任务,默认每隔100毫秒(1秒10次)就会从全局的过期哈希表随机取出20个key,然后删除其中过期的key,如果过期key的比例超过了25%,则继续重复此过程,直到过期key的比例的下降到25%以下,或者这次任务的执行耗时超过了25毫秒,才会退出循环。
注意,这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。
也就是说如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。此时就会出现,应用访问 Redis 延时变大。
解决方案
- 集中过期key增加一个随机过期时间,把集中过期的时间打散,降低redis清理过期key的压力。
- 如果使用的是redis是4.0以上的版本,可以开启lazy-free机制,当删除过期key时,把释放内存的操作放到后台线程中进行,避免阻塞主线程
4.实例内存达到上限
当我们把 Redis 当做纯缓存使用时,通常会给这个实例设置一个内存上限 maxmemory,然后设置一个数据淘汰策略。
而当实例的内存达到了 maxmemory 后,你可能会发现,在此之后每次写入新数据,操作延迟变大了。这是为什么?
原因在于,当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。
这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置的淘汰策略:
allkeys-lru
:不管 key 是否设置了过期,淘汰最近最少访问的 keyvolatile-lru
:只淘汰最近最少访问、并设置了过期时间的 keyallkeys-random
:不管 key 是否设置了过期,随机淘汰 keyvolatile-random
:只随机淘汰设置了过期时间的 keyallkeys-ttl
:不管 key 是否设置了过期,淘汰即将过期的 keynoeviction
:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误allkeys-lfu
:不管 key 是否设置了过期,淘汰访问频率最低的 key(4.0+版本支持)volatile-lfu
:只淘汰访问频率最低、并设置了过期时间 key(4.0+版本支持)
解决方案
- 避免存储bigkey,降低释放内存的耗时
- 淘汰策略改为随机淘汰,随机淘汰比LRU要快很多
- 拆分实例,把淘汰key的压力分摊到多个实例上
- 如果使用的是redis 4.0以上的版本,开启layz-free机制,把淘汰key释放内存的操作放到后台线程执行
5. fork耗时严重
当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。
主进程创建子进程,会调用操作系统提供的 fork 函数。
而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。
而且这个 fork 过程会消耗大量的 CPU 资源,在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。
如果此时你的 CPU 资源本来就很紧张,那么 fork 的耗时会更长,甚至达到秒级,这会严重影响 Redis 的性能。
解决方案
- 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
- 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
- Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
- 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步
6. 开启内存大页
什么是内存大页?
我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB。Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。
这对redis会有什么影响?
当 Redis 在执行后台 RDB 和 AOF rewrite 时,采用 fork 子进程的方式来处理。但主进程 fork 子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。
也就是说,主进程一旦有数据需要修改,Redis 并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据,这就是所谓的「写时复制」。
写时复制你也可以理解成,谁需要发生写操作,谁就需要先拷贝,再修改。
这样做的好处是,父进程有任何写操作,并不会影响子进程的数据持久化(子进程只持久化 fork 这一瞬间整个实例中的所有数据即可,不关心新的数据变更,因为子进程只需要一份内存快照,然后持久化到磁盘上)。
但是请注意,主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。
解决方案
- 关闭内存大页
7.开启AOF
如果你的 AOF 配置不合理,还是有可能会导致性能问题。
当 Redis 开启 AOF 后,其工作原理如下:
- Redis 执行写命令后,把这个命令写入到 AOF 文件内存中(write 系统调用)
- Redis 根据配置的 AOF 刷盘策略,把 AOF 内存数据刷到磁盘上(fsync 系统调用)
为了保证 AOF 文件数据的安全性,Redis 提供了 3 种刷盘机制:
- appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高
- appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机
- appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据
下面我们依次来分析,这几个机制对性能的影响。
如果你的 AOF 配置为 appendfsync always,那么 Redis 每处理一次写操作,都会把这个命令写入到磁盘中才返回,整个过程都是在主线程执行的,这个过程必然会加重 Redis 写负担。
原因也很简单,操作磁盘要比操作内存慢几百倍,采用这个配置会严重拖慢 Redis 的性能,因此我不建议你把 AOF 刷盘方式配置为 always。
我们接着来看 appendfsync no 配置项。
在这种配置下,Redis 每次写操作只写内存,什么时候把内存中的数据刷到磁盘,交给操作系统决定,此方案对 Redis 的性能影响最小,但当 Redis 宕机时,会丢失一部分数据,为了数据的安全性,一般我们也不采取这种配置。
8. 绑定CPU
有时候在部署redis时,需要绑定CPU来提高其性能,建议仔细斟酌再操作。为什么?
因为 Redis 在绑定 CPU 时,是有很多考究的,如果你不了解 Redis 的运行原理,随意绑定 CPU 不仅不会提高性能,甚至有可能会带来相反的效果。
我们都知道,一般现代的服务器会有多个 CPU,而每个 CPU 又包含多个物理核心,每个物理核心又分为多个逻辑核心,每个物理核下的逻辑核共用 L1/L2 Cache。
而 Redis Server 除了主线程服务客户端请求之外,还会创建子进程、子线程。
其中子进程用于数据持久化,而子线程用于执行一些比较耗时操作,例如异步释放 fd、异步 AOF 刷盘、异步 lazy-free 等等。
如果你把 Redis 进程只绑定了一个 CPU 逻辑核心上,那么当 Redis 在进行数据持久化时,fork 出的子进程会继承父进程的 CPU 使用偏好。
而此时的子进程会消耗大量的 CPU 资源进行数据持久化(把实例数据全部扫描出来需要耗费CPU),这就会导致子进程会与主进程发生 CPU 争抢,进而影响到主进程服务客户端请求,访问延迟变大。
这就是 Redis 绑定 CPU 带来的性能问题。
解决方案
如果你确实想要绑定 CPU,可以优化的方案是,不要让 Redis 进程只绑定在一个 CPU 逻辑核上,而是绑定在多个逻辑核心上,而且,绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用 L1/L2 Cache。
当然,即便我们把 Redis 绑定在多个逻辑核心上,也只能在一定程度上缓解主线程、子进程、后台线程在 CPU 资源上的竞争。
因为这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。
进一步优化
可能你已经想到了,我们是否可以让主线程、子进程、后台线程,分别绑定在固定的 CPU 核心上,不让它们来回切换,这样一来,他们各自使用的 CPU 资源互不影响。
其实,这个方案 Redis 官方已经想到了。
Redis 在 6.0 版本已经推出了这个功能,我们可以通过以下配置,对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心。
9. 使用Swap
如果你发现 Redis 突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,那此时你就需要检查 Redis 是否使用到了 Swap,在这种情况下 Redis 基本上已经无法提供高性能的服务了。
什么是 Swap?为什么使用 Swap 会导致 Redis 的性能下降?
如果你对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。
问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!
尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。
此时,你需要检查 Redis 机器的内存使用情况,确认是否存在使用了 Swap。
解决方案
- 增加机器的内存,让redis有足够的内存可以使用
- 整理内存空间,释放出足够的内存供redis使用,然后释放redis的Swap,让redis重新使用内存
10.碎片整理
Redis 的数据都存储在内存中,当我们的应用程序频繁修改 Redis 中的数据时,就有可能会导致 Redis 产生内存碎片。
解决方案
- 如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决
- 如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理