关于c++:Redis为什么变慢了这些原因你都知道吗

1次阅读

共计 6968 个字符,预计需要花费 18 分钟才能阅读完成。

前言

Redis 作为内存数据库,领有十分高的性能,单个实例的 QPS 可能达到 10W 左右。但咱们在应用 Redis 时,常常时不时会呈现拜访提早很大的状况,如果你不晓得 Redis 的外部实现原理,在排查问题时就会一头雾水。

很多时候,Redis 呈现拜访提早变大,都与咱们的使用不当或运维不合理导致的。

这篇文章咱们就来剖析一下 Redis 在应用过程中,常常会遇到的提早问题以及如何定位和剖析。

欢送各位进群 973961276 一起聊聊技术吹吹牛,每周都会有几次抽奖送专业书籍的
流动,奖品虽不甚值钱,但也算个彩头不是

应用复杂度高的命令

如果在应用 Redis 时,发现拜访提早忽然增大,如何进行排查?

首先,第一步,倡议你去查看一下 Redis 的慢日志。Redis 提供了慢日志命令的统计性能,咱们通过以下设置,就能够查看有哪些命令在执行时提早比拟大。

首先设置 Redis 的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是奥妙,例如设置慢日志的阈值为 5 毫秒,同时设置只保留最近 1000 条慢日志记录:

# 命令执行超过 5 毫秒记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 1000 条慢日志
CONFIG SET slowlog-max-len 1000

设置实现之后,所有执行的命令如果提早大于 5 毫秒,都会被 Redis 记录下来,咱们执行 SLOWLOG get 5 查问最近 5 条慢日志:

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693       # 慢日志 ID
   2) (integer) 1593763337  # 执行工夫
   3) (integer) 5299        # 执行耗时(奥妙)
   4) 1) "LRANGE"           # 具体执行的命令和参数
      2) "user_list_2000"
      3) "0"
      4) "-1"
2) 1) (integer) 32692
   2) (integer) 1593763337
   3) (integer) 5044
   4) 1) "GET"
      2) "book_price_1000"
...

通过查看慢日志记录,咱们就能够晓得在什么工夫执行哪些命令比拟耗时,如果你的业务 常常应用 O(N) 以上复杂度的命令 ,例如sortsunionzunionstore,或者在执行O(N) 命令时操作的数据量比拟大,这些状况下 Redis 解决数据时就会很耗时。

如果你的服务申请量并不大,但 Redis 实例的 CPU 使用率很高,很有可能是应用了复杂度高的命令导致的。

解决方案就是,不应用这些复杂度较高的命令,并且一次不要获取太多的数据,每次尽量操作大量的数据,让 Redis 能够及时处理返回。

存储 bigkey

如果查问慢日志发现,并不是复杂度较高的命令导致的,例如都是 SETDELETE 操作呈现在慢日志记录中,那么你就要狐疑是否存在 Redis 写入了 bigkey 的状况。

Redis 在写入数据时,须要为新的数据分配内存,当从 Redis 中删除数据时,它会开释对应的内存空间。

如果一个 key 写入的数据十分大,Redis 在 分配内存时也会比拟耗时 。同样的,当删除这个 key 的数据时, 开释内存也会耗时比拟久

你须要查看你的业务代码,是否存在写入 bigkey 的状况,须要评估写入数据量的大小,业务层应该防止一个 key 存入过大的数据量。

那么有没有什么方法能够扫描当初 Redis 中是否存在 bigkey 的数据吗?

Redis 也提供了扫描 bigkey 的办法:

redis-cli -h $host -p $port --bigkeys -i 0.01

应用下面的命令就能够扫描出整个实例 key 大小的散布状况,它是以类型维度来展现的。

须要留神的是当咱们在线上实例进行 bigkey 扫描时,Redis 的 QPS 会突增,为了升高扫描过程中对 Redis 的影响,咱们须要管制扫描的频率,应用 -i 参数管制即可,它示意扫描过程中每次扫描的工夫距离,单位是秒。

应用这个命令的原理,其实就是 Redis 在外部执行 scan 命令,遍历所有 key,而后针对不同类型的 key 执行 strlenllenhlenscardzcard 来获取字符串的长度以及容器类型 (list/dict/set/zset) 的元素个数。

而对于容器类型的 key,只能扫描出元素最多的 key,但元素最多的 key 不肯定占用内存最多,这一点须要咱们留神下。不过应用这个命令个别咱们是能够对整个实例中 key 的散布状况有比拟清晰的理解。

针对 bigkey 的问题,Redis 官网在 4.0 版本推出了 lazy-free 的机制,用于异步开释 bigkey 的内存,升高对 Redis 性能的影响。即便这样,咱们也不倡议应用 bigkey,bigkey 在集群的迁徙过程中,也会影响到迁徙的性能,这个前面在介绍集群相干的文章时,会再具体介绍到。

集中过期

有时你会发现,平时在应用 Redis 时没有延时比拟大的状况,但在某个工夫点忽然呈现一波延时,而且 报慢的工夫点很有法则,例如某个整点,或者距离多久就会产生一次

如果呈现这种状况,就须要思考是否存在大量 key 集中过期的状况。

如果有大量的 key 在某个固定工夫点集中过期,在这个工夫点拜访 Redis 时,就有可能导致提早减少。

Redis 的过期策略采纳被动过期 + 懈怠过期两种策略:

  • 被动过期:Redis 外部保护一个定时工作,默认每隔 100 毫秒会从过期字典中随机取出 20 个 key,删除过期的 key,如果过期 key 的比例超过了 25%,则持续获取 20 个 key,删除过期的 key,周而复始,直到过期 key 的比例降落到 25% 或者这次工作的执行耗时超过了 25 毫秒,才会退出循环
  • 懈怠过期:只有当拜访某个 key 时,才判断这个 key 是否已过期,如果曾经过期,则从实例中删除

留神,Redis 的被动过期的定时工作,也是在 Redis 主线程中执行的,也就是说如果在执行被动过期的过程中,呈现了须要大量删除过期 key 的状况,那么在业务拜访时,必须等这个过期工作执行完结,才能够解决业务申请。此时就会呈现,业务拜访延时增大的问题,最大提早为 25 毫秒。

而且这个拜访提早的状况,不会记录在慢日志里 。慢日志中 只记录真正执行某个命令的耗时,Redis 被动过期策略执行在操作命令之前,如果操作命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但咱们的业务却感到了提早增大。

此时你须要查看你的业务,是否真的存在集中过期的代码,个别集中过期应用的命令是 expireatpexpireat命令,在代码中搜寻这个关键字就能够了。

如果你的业务的确须要集中过期掉某些 key,又不想导致 Redis 产生抖动,有什么优化计划?

解决方案是,在集中过期时减少一个随机工夫,把这些须要过期的 key 的工夫打散即可。

伪代码能够这么写:

# 在过期工夫点之后的 5 分钟内随机过期掉
redis.expireat(key, expire_time + random(300))

这样 Redis 在解决过期时,不会因为集中删除 key 导致压力过大,阻塞主线程。

另外,除了业务应用须要留神此问题之外,还能够通过运维伎俩来及时发现这种状况。

做法是咱们须要把 Redis 的各项运行数据监控起来,执行 info 能够拿到所有的运行数据,在这里咱们须要重点关注 expired_keys 这一项,它代表整个实例到目前为止,累计删除过期 key 的数量。

咱们须要对这个指标监控,当在 很短时间内这个指标呈现突增 时,须要及时报警进去,而后与业务报慢的工夫点比照剖析,确认工夫是否统一,如果统一,则能够认为的确是因为这个起因导致的提早增大。

实例内存达到下限

有时咱们把 Redis 当做纯缓存应用,就会给实例设置一个内存下限maxmemory,而后开启 LRU 淘汰策略。

当实例的内存达到了 maxmemory 后,你会发现之后的每次写入新的数据,有可能变慢了。

导致变慢的起因是,当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,必须先踢出一部分数据,让内存维持在 maxmemory 之下。

这个踢出旧数据的逻辑也是须要耗费工夫的,而具体耗时的长短,要取决于配置的淘汰策略:

  • allkeys-lru:不论 key 是否设置了过期,淘汰最近起码拜访的 key
  • volatile-lru:只淘汰最近起码拜访并设置过期的 key
  • allkeys-random:不论 key 是否设置了过期,随机淘汰
  • volatile-random:只随机淘汰有设置过期的 key
  • allkeys-ttl:不论 key 是否设置了过期,淘汰行将过期的 key
  • noeviction:不淘汰任何 key,满容后再写入间接报错
  • allkeys-lfu:不论 key 是否设置了过期,淘汰拜访频率最低的 key(4.0+ 反对)
  • volatile-lfu:只淘汰拜访频率最低的过期 key(4.0+ 反对)

具体应用哪种策略,须要依据业务场景来决定。

咱们最常应用的个别是 allkeys-lruvolatile-lru策略,它们的解决逻辑是,每次从实例中随机取出一批 key(可配置),而后淘汰一个起码拜访的 key,之后把剩下的 key 暂存到一个池子中,持续随机取出一批 key,并与之前池子中的 key 比拟,再淘汰一个起码拜访的 key。以此循环,直到内存降到 maxmemory 之下。

如果应用的是 allkeys-randomvolatile-random策略,那么就会快很多,因为是随机淘汰,那么就少了比拟 key 拜访频率工夫的耗费了,随机拿出一批 key 后间接淘汰即可,因而这个策略要比下面的 LRU 策略执行快一些。

但以上这些逻辑都是在拜访 Redis 时,真正命令执行之前执行的,也就是它会影响咱们拜访 Redis 时执行的命令。

另外,如果此时 Redis 实例中有存储 bigkey,那么 在淘汰 bigkey 开释内存时,这个耗时会更加久,提早更大,这须要咱们分外留神。

如果你的业务访问量十分大,并且必须设置 maxmemory 限度实例的内存下限,同时面临淘汰 key 导致提早增大的的状况,要想缓解这种状况,除了下面说的防止存储 bigkey、应用随机淘汰策略之外,也能够思考 拆分实例 的办法来缓解,拆分实例能够把一个实例淘汰 key 的压力 摊派到多个实例 上,能够在肯定水平升高提早。

fork 耗时重大

如果你的 Redis 开启了主动生成 RDB 和 AOF 重写性能,那么有可能在后盾生成 RDB 和 AOF 重写时导致 Redis 的拜访提早增大,而等这些工作执行结束后,提早状况隐没。

遇到这种状况,个别就是执行生成 RDB 和 AOF 重写工作导致的。

生成 RDB 和 AOF 都须要父过程 fork 出一个子过程进行数据的长久化,fork 执行过程中,父过程须要拷贝内存页表给子过程,如果整个实例内存占用很大,那么须要拷贝的内存页表会比拟耗时,此过程会耗费大量的 CPU 资源,在实现 fork 之前,整个实例会被阻塞住,无奈解决任何申请,如果此时 CPU 资源缓和,那么 fork 的工夫会更长,甚至达到秒级。这会重大影响 Redis 的性能

咱们能够执行 info 命令,查看最初一次 fork 执行的耗时latest_fork_usec,单位奥妙。这个工夫就是整个实例阻塞无奈解决申请的工夫。

除了因为备份的起因生成 RDB 之外,在主从节点第一次建设数据同步时,主节点也会生成 RDB 文件给从节点进行一次全量同步,这时也会对 Redis 产生性能影响。

要想防止这种状况,咱们须要布局好数据备份的周期,倡议在 从节点上执行备份,而且最好放在低峰期执行。如果对于失落数据不敏感的业务,那么不倡议开启 AOF 和 AOF 重写性能。

另外,fork的耗时也与零碎无关,如果把 Redis 部署在虚拟机上,那么这个工夫也会增大。所以应用 Redis 时倡议部署在物理机上,升高 fork 的影响。

绑定 CPU

很多时候,咱们在部署服务时,为了进步性能,升高程序在应用多个 CPU 时上下文切换的性能损耗,个别会采纳过程绑定 CPU 的操作。

但在应用 Redis 时,咱们不倡议这么干,起因如下。

绑定 CPU 的 Redis,在进行数据长久化时,fork出的子过程,子过程会继承父过程的 CPU 应用偏好,而此时子过程会耗费大量的 CPU 资源进行数据长久化,子过程会与主过程产生 CPU 争抢,这也会导致主过程的 CPU 资源有余拜访提早增大。

所以在部署 Redis 过程时,如果须要开启 RDB 和 AOF 重写机制,肯定不能进行 CPU 绑定操作!

AOF 配合不合理

下面提到了,当执行 AOF 文件重写时会因为 fork 执行耗时导致 Redis 提早增大,除了这个之外,如果开启 AOF 机制,设置的策略不合理,也会导致性能问题。

开启 AOF 后,Redis 会把写入的命令实时写入到文件中,但写入文件的过程是先写入内存,等内存中的数据超过肯定阈值或达到肯定工夫后,内存中的内容才会被真正写入到磁盘中。

AOF 为了保障文件写入磁盘的安全性,提供了 3 种刷盘机制:

  • appendfsync always:每次写入都刷盘,对性能影响最大,占用磁盘 IO 比拟高,数据安全性最高
  • appendfsync everysec:1 秒刷一次盘,对性能影响绝对较小,节点宕机时最多失落 1 秒的数据
  • appendfsync no:依照操作系统的机制刷盘,对性能影响最小,数据安全性低,节点宕机失落数据取决于操作系统刷盘机制

当应用第一种机制 appendfsync always 时,Redis 每解决一次写命令,都会把这个命令写入磁盘,而且 这个操作是在主线程中执行的

内存中的的数据写入磁盘,这个会减轻磁盘的 IO 累赘,操作磁盘老本要比操作内存的代价大得多。如果写入量很大,那么每次更新都会写入磁盘,此时机器的磁盘 IO 就会十分高,拖慢 Redis 的性能,因而咱们不倡议应用这种机制。

与第一种机制比照,appendfsync everysec会每隔 1 秒刷盘,而 appendfsync no 取决于操作系统的刷盘工夫,安全性不高。因而咱们举荐应用 appendfsync everysec 这种形式,在最坏的状况下,只会失落 1 秒的数据,但它能放弃较好的拜访性能。

当然,对于有些业务场景,对失落数据并不敏感,也能够不开启 AOF。

应用 Swap

如果你发现 Redis 忽然变得十分慢,每次拜访的耗时都达到了几百毫秒甚至秒级,那此时就查看 Redis 是否应用到了 Swap,这种状况下 Redis 基本上曾经无奈提供高性能的服务。

咱们晓得,操作系统提供了 Swap 机制,目标是为了当内存不足时,能够把一部分内存中的数据换到磁盘上,以达到对内存应用的缓冲。

但当内存中的数据被换到磁盘上后,拜访这些数据就须要从磁盘中读取,这个速度要比内存慢太多!

尤其是针对 Redis 这种高性能的内存数据库来说,如果 Redis 中的内存被换到磁盘上,对于 Redis 这种性能极其敏感的数据库,这个操作工夫是无奈承受的。

咱们须要查看机器的内存应用状况,确认是否的确是因为内存不足导致应用到了 Swap。

如果的确应用到了 Swap,要及时整顿内存空间,开释出足够的内存供 Redis 应用,而后开释 Redis 的 Swap,让 Redis 从新应用内存。

开释 Redis 的 Swap 过程通常要重启实例,为了防止重启实例对业务的影响,个别先进行主从切换,而后开释旧主节点的 Swap,重新启动服务,待数据同步实现后,再切换回主节点即可。

可见,当 Redis 应用到 Swap 后,此时的 Redis 的高性能根本被废掉,所以咱们须要提前预防这种状况。

咱们须要对 Redis 机器的内存和 Swap 应用状况进行监控,在内存不足和应用到 Swap 时及时报警进去,及时进行相应的解决。

网卡负载过高

如果以上产生性能问题的场景,你都躲避掉了,而且 Redis 也稳固运行了很长时间,但在某个工夫点之后开始,拜访 Redis 开始变慢了,而且始终继续到当初,这种状况是什么起因导致的?

之前咱们就遇到这种问题,特点就是从某个工夫点之后就开始变慢,并且始终继续。这时你须要检查一下机器的网卡流量,是否存在网卡流量被跑满的状况。

网卡负载过高,在网络层和 TCP 层就会呈现数据发送提早、数据丢包等状况。Redis 的高性能除了内存之外,就在于网络 IO,申请量突增会导致网卡负载变高。

如果呈现这种状况,你须要排查这个机器上的哪个 Redis 实例的流量过大占满了网络带宽,而后确认流量突增是否属于业务失常状况,如果属于那就须要及时扩容或迁徙实例,防止这个机器的其余实例受到影响。

运维层面,咱们须要对机器的各项指标减少监控,包含网络流量,在达到阈值时提前报警,及时与业务确认并扩容。

总结

以上咱们总结了 Redis 中常见的可能导致提早增大甚至阻塞的场景,这其中既波及到了业务的应用问题,也波及到 Redis 的运维问题。

可见,要想保障 Redis 高性能的运行,其中波及到 CPU、内存、网络,甚至磁盘的方方面面,其中还包含操作系统的相干个性的应用。

作为开发人员,咱们须要理解 Redis 的运行机制,例如各个命令的执行工夫复杂度、数据过期策略、数据淘汰策略等,应用正当的命令,并联合业务场景进行优化。

作为 DBA 运维人员,须要理解数据长久化、操作系统 fork 原理、Swap 机制等,并对 Redis 的容量进行正当布局,预留足够的机器资源,对机器做好欠缺的监控,能力保障 Redis 的稳固运行。

零根底和大三大四的敌人看这里 >>c/c++ 企业级我的项目实战
曾经工作了想持续自我晋升跳槽涨薪的工程师看这里 >>c/c++ linux 服务器高级架构师学习

正文完
 0