关于 redis 性能的排查 我首先能想到的方面是 以下几个 (可以类比数据库sql变慢的原因):

  1. redis 在大部分情况下都会使用单线程来完成指令操作,那么在一些命令中会导致变慢,他会阻塞其他指令 比如key * (改用 SCAN)
  2. redis 作为一个内存数据库,那么如果数据都在内存中可以获得,那么速度是很快的,但是如果涉及到要到磁盘去进行读取 速度就很很慢。
  3. 查看慢日志 查看慢的那些命令

1. 进行排查,是否redis是否真的慢了

在服务内部集成链路追踪,查看对应方法在调用redis 服务时是否造成了延迟。**有可能是存在网络的原因导致在传输中出现了丢包等问题,**或者是redis确实出现了问题。

2. 进行基准测试,进行排查

在redis服务器中进行调用,测试对应实例的响应延迟情况

redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60

通过上面的命令可以知道这个实例在60秒内的最大响应延迟。

redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1 通过这个命令可以查看一段时间内redis的最大延迟,最小与平均延迟。

通过以下几步 可以判断redis 是否变慢了

  1. 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
  2. 找到你认为可能变慢的 Redis 实例,测试这个实例的基准性能
  3. 如果你观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了

3. 是否存在大量复杂度高的命令

查看慢日志来了解是哪些命令耗时比较严重

首先采用以下命令来设置慢日志阈值

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

对于时间复杂度在O(N) 的命令 会造成运行比较慢

  1. 复杂度为O(N),比如SORT、SUNION、ZUNIONSTORE 聚合类命令
  2. N 代表的数组特别大 (可以类别 在mysql 查询中 查出来数据比较大)

针对于 第一个问题 需要考虑的是 将聚合的操作在代码层面完成,由于复杂度的提高,对于CPU的耗费会很高,

第二个问题主要是由于 一次性返回了过多的数据,导致网络的传输消耗会很高。遇到这个问题,最好采用分页的方式对于数据进行分批的返回。

4. bigKey

redis 中bigKey会带来的问题,主要是由于在序列化的时候耗时比较长,会造成阻塞。

string长度大于10K,list长度大于10240认为是big bigkeys

同时在存储的时候耗时比较长,以及在删除的时候耗时也比较长。

redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

该名称内部使用了SCAN的原理来扫描每一个key

该命令也会有两个问题

  • 该命令会造成Redis 的 OPS 会突增,需要控制扫描的效率
  • 该命令在对于一些结构为 list set 会返回结构的数量多的那些ky, 但是不排除存在一个list 只有一个对象,但该对象会很多。

处理的方法有几个

  1. 避免big key写入
  2. 在删除的时候 使用 unlink 替换 del ,del 是阻塞的,但是 unlink该命令会执行命令之外的线程中执行实际的内存回收,因此它不是阻塞.主要是将 引用断开,后续在进行value 删除。

集中过期

redis 的删除策略有两种 :

  1. 被动删除: 只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
  2. 主动删除: 开一个线程 每隔若干时间,从redis中取出一部分数据,判断是否过期,如果过期数量超过了25%,那就再次进行,小于25%就不再进行。

在第二种策略情况下,在删除执行时,如果有新的命令过来,这只能等待删除数据结束后,才能执行后续的命令。后续的命令不会被记录到慢日志的,因为删除的任务是在执行任务之前。

当然在大量key 集中过期 更严重的问题是缓存雪崩,

处理方案就是在过期时间后 加一个随机时间,还有一种方法是在redis4.0之前 使用了一个lazy-free,在释放过期内存时,采用了后台线程的方式。lazyfree-lazy-eviction = yes

内存达到上限

当redis 能够使用的内存达到上限的时候,之后的命令如果没法命中在内存中,那么就只能删除一部分数据(使用内存淘汰策略),同时在磁盘中加载一部分数据,这样耗时就比较长

  • allkeys-lru:不管 key 是否设置了过期,淘汰最近最少访问的 key
  • volatile-lru:只淘汰最近最少访问、并设置了过期时间的 key
  • allkeys-random:不管 key 是否设置了过期,随机淘汰 key
  • volatile-random:只随机淘汰设置了过期时间的 key
  • allkeys-ttl:不管 key 是否设置了过期,淘汰即将过期的 key
  • noeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错误
  • allkeys-lfu:不管 key 是否设置了过期,淘汰访问频率最低的 key(4.0+版本支持)
  • volatile-lfu:只淘汰访问频率最低、并设置了过期时间 key(4.0+版本支持)

默认的淘汰策略 主要是 前两个,lru 最简单的就是构建一个list,将最近使用的就放到list的头位置,在redis 中是采用一个淘汰池来完成,(每次从实例中随机取出一批 key(这个数量可配置),然后淘汰一个最少访问的 key,之后把剩下的 key 暂存到一个池子中,继续随机取一批 key,并与之前池子中的 key 比较,再淘汰一个最少访问的 key。以此往复,直到实例内存降到 maxmemory 之下)。

如果在内存与磁盘交换数据的每次只交换很小的容量 影响不是很大,但是每次达到几百兆就会很大问题

在淘汰策略中 耗时最少的是 采用随机淘汰来完成,这要根据业务来进行。在4.0的内存淘汰可以使用

lazy-free。

fork耗时严重

在fork中 主要是通过创建子线程,二者共享同一个文件资源与文件描述符,采用这种方法,在很大程度上,避免文件的复制,同时,当父进程修改文件的话,子进程也会被感知到。在这时间 哈希表的 rehash 的负载因子会被提升到5。

fork 线程会消耗大量的cpu资源,如果实例的数据很大,在使用RDB 处理的时候,阻塞的时间就会变得很长

方案:

  1. 控制实例大小为10G以下
  2. redis 最好不要部署在虚拟机上,
  3. 合理配置同步策略,以及减少全量同步的概率

内存大页

Linux 系统进行虚拟内存管理。 顾名思义,除了标准的 4KB 大小的页面外,它们还能帮助管理内存中的巨大的页面。 使用“大内存页”,你最大可以定义 1GB 的页面大小。

在启动时,通过配置大页内存,linux 系统会预留一部分内存,即被“大内存页”占用的这些存储器永远不会被交换出内存。它会一直保留其中。该配置有利于如oracle 需要大量内存的应用

使用大页内存,所需要的页变少了。从而大大减少由内核加载的映射表的数量。这提高了内核级别的性能最终有利于应用程序的性能。

在配置了大页内存的话,在同步的时候,redis使用了一个写时复制,即父进程与子进程共用同一块内存,在同步过程时,如果数据发生了变化,那么主线程会复制内存,再进行拷贝,那么如果只需要修改10B的数据,大页内存就可能需要负责一块2M的内存区域。大内存在申请的时候耗时会比较长

是否开启了大页内存 : cat /sys/kernel/mm/transparent_hugepage/enabled

关闭大页内存 : echo never > /sys/kernel/mm/transparent_hugepage/enabled

AOF的配置

如果把AOF配置为always,那么在每次处理后就会进行同步,IO压力会比较大。

内存碎片

频繁修改 Redis 中的数据时,就有可能会导致 Redis 产生内存碎片。内存碎片会降低 Redis 的内存使用率,在redis 4,0之后 redis会自动进行内存碎片整理,在4.0之前,只能进行redis 重启。