关于 redis 性能的排查 我首先能想到的方面是 以下几个 (可以类比数据库sql变慢的原因):
- redis 在大部分情况下都会使用单线程来完成指令操作,那么在一些命令中会导致变慢,他会阻塞其他指令 比如key * (改用 SCAN)
- redis 作为一个内存数据库,那么如果数据都在内存中可以获得,那么速度是很快的,但是如果涉及到要到磁盘去进行读取 速度就很很慢。
- 查看慢日志 查看慢的那些命令
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 是否变慢了
- 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
- 找到你认为可能变慢的 Redis 实例,测试这个实例的基准性能
- 如果你观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了
3. 是否存在大量复杂度高的命令
查看慢日志来了解是哪些命令耗时比较严重
首先采用以下命令来设置慢日志阈值
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
对于时间复杂度在O(N) 的命令 会造成运行比较慢
- 复杂度为O(N),比如SORT、SUNION、ZUNIONSTORE 聚合类命令
- 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 只有一个对象,但该对象会很多。
处理的方法有几个
- 避免big key写入
- 在删除的时候 使用 unlink 替换 del ,del 是阻塞的,但是 unlink该命令会执行命令之外的线程中执行实际的内存回收,因此它不是阻塞.主要是将 引用断开,后续在进行value 删除。
集中过期
redis 的删除策略有两种 :
- 被动删除: 只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
- 主动删除: 开一个线程 每隔若干时间,从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 处理的时候,阻塞的时间就会变得很长
方案:
- 控制实例大小为10G以下
- redis 最好不要部署在虚拟机上,
- 合理配置同步策略,以及减少全量同步的概率
内存大页
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 重启。