Redis面试常见问题和性能监控


转载于:https://mp.weixin.qq.com/s/qvXm1pU8T_2mCZCjkTR7QA

1 Redis常见面试问题

1.1 Redis是单线程还是多线程

Redis不同版本之间采用的线程模型是不一样的,在Redis4.0版本之前使用的是单线程模型,在4.0版本之后增加了多线程的支持。

4.0之前虽然说Redis是单线程,也只是说它的网络I/O线程以及SetGet操作是由一个线程完成的。但是Redis持久化集群同步还是使用其他线程来完成。

4.0之后添加了多线程的支持,主要是体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async

1.2 使用单线程原因

那为什么Redis在4.0之前会选择使用单线程?而且使用单线程还那么快?

选择单线程主要是使用简单,不存在锁竞争,可以在无锁的情况下完成所有操作,不存在死锁和线程切换带来的性能和时间上的开销,但同时单线程也不能完全发挥出多核CPU性能

为什么单线程那么快主要有以下几个原因:

  • Redis的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。
  • 使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。
  • 采用 I/O 多路复用机制处理大量客户端的Socket请求,因为这是基于非阻塞的 I/O 模型,这就让Redis可以高效地进行网络通信,I/O的读写流程也不再阻塞。
  • 很多的客户端连接先到linux中的内核,内核和redis中间使用的是epoll(非阻塞的多路复用),那些进程一笔一笔的进行的。

在分布式情况下,这个数据一致性很重要。每个连接里边命令是顺序到达、顺序处理的,但是如果说里面有个key,这个key为a,那么两个客户端发了一个对a的操作,那么无论从网络当中跳跃谁先到达的,或者指定谁先轮到谁了。那么其实这两个人对一个的操作,很难判定是谁先谁后,但是如果是你一个人,它里边线性,而且没有使用多线程,线程还是安全的,虽然它可以有多线程,但是线程安全,对a的操作,这边能控制住,先创建a再删除a,只要这边能操作的话,那么这个数据是可以保证的。如果是单线程,这个客户端就是一个线程,就是一个socket里面也是一个线程,那么这个线程肯定是先发出一个创建a再发出一个删除a,但是如果客户端里边是多线程,那么这里一个创建命令和删除命令,指不定谁跑到前面了,如果线程不是安全的话,那么有可能先把删除的发出去,再把创建的发出去。
请添加图片描述
点击了解Linux中epoll原理机制

1.3 Redis高可用

Redis实现高可用主要有三种方式:主从复制哨兵模式,以及 Redis 集群

1.3.1 主从复制

将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟MySQL主从复制的原理一样。
在这里插入图片描述
点击了解redis 持久化中的主从复制同步

1.3.2 哨兵模式

使用 Redis 主从复制的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。
它专注于对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主主从切换,实现自动故障转移,确保整个 Redis 系统的可用性。

sentinel 主要做四件事情:

  • 监控 masterslave 状态,判断是否下线。
    每秒一次的频率向 masterslave 以及其他 sentinel 发送 PING 命令,如果该节点距离最后一次响应 PING 的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线,当 master 被标记主观下线。
    其他正在监视这个 master 的所有 sentinel 会按照每秒一次的频率确认 master 是否主观下线。
    当足够多的 sentinel 都认为 master 主观下线,则标记这个 master 客观下线。
  • 选举新 master,如果 master 出现故障,sentine 需要选举一个 slave 晋升为新 master。晋升为新 master 的 slave 是有条件的,先过滤不满足条件的,再打分排优先级。
    • slave 优先级,通过 replica-priority 100 配置,值越低,优先级越高。
    • 复制偏移量(processed replication offset),已复制的数据量越多越好,slave_repl_offsetmaster_repl_offset 差值越小。
    • slave runID,在优先级和复制进度都相同的情况下,runID 最小的 slave 得分最高,会被选为新主库。
    • 过滤掉下线、网络异常的 slave
    • 过滤掉经常与 master 断开的 slave
  • 选举领导者哨兵(Leader Sentinel),执行主从切换,从 sentinel 集群中选举一个 leader 执行故障自动切换。
    第一个判定 master 主观下线的 sentinel 收到其他 sentinel 节点的回复并确定 master 客观下线后,就会给其他 sentinel 节点发送命令申请成为 leader。
    选举领导者哨兵而不是直接从从节点中选举新的主节点,主要是为了以下原因:
    • 协调一致性:通过选举领导者哨兵,可以确保在集群中仅有一个哨兵负责主从切换,避免多个哨兵同时进行切换操作导致的不一致性。
    • 集中决策:领导者哨兵集中管理整个主从切换过程,使得切换过程更加有序和可控。
    • 分担职责:哨兵负责监控和管理 Redis 节点,而从节点主要用于数据复制和故障切换。在发生故障时,选举领导者哨兵来管理切换过程,可以让从节点专注于数据同步,分担系统负载
      成为 leader 的条件是收到的赞成票大于等于 quorum 的值且赞半数以上。
  • 通知,通知其他 slave 执行 replicaof 与新的 master 同步数据,并通知客户端与新 master 建立连接。

图片

1.3.3 Redis Cluster(集群)

Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

图片

使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦master宕机会选举salve节点为master节点,那为什么还需要使用集群模式呢?
哨兵模式归根节点还是主从模式,在主从模式下我们可以通过增加salve节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是master节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。

集群中那么多Master节点,redis cluster在存储的时候如何确定选择哪个节点呢?
Redis Cluster采用的是数据分片实现节点选择的

Redis集群搭建

1.4 Redis内存(数据)淘汰策略

redis中,我们是可以去设置最大使用内存大小server.maxmemory的,当redis内存数据集大小上升到一定程度的时候,就会施行数据淘汰机制。
不同位数的操作系统,maxmemory 的默认值是不同的:

  • 在 64 位操作系统中,maxmemory 的默认值是 0,表示没有内存大小限制,那么不管用户存放多少数据到 Redis 中,Redis 也不会对可用内存进行检查,直到 Redis 实例因内存不足而崩溃也无作为。
  • 在 32 位操作系统中,maxmemory 的默认值是 3G,因为 32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位操作系统限制最大 3 GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃。

Redis提供了8种数据淘汰策略,分为不进行数据淘汰进行数据淘汰两类策略,
不进行数据淘汰的策略 noevictionRedis3.0之后,默认的内存淘汰策略),它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。

  • no-enviction:禁止淘汰数据,如果redis写满了将不提供写请求,直接返回错误

进行数据淘汰的策略:

  • volatile-lru:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰,即:最久未使用的键值
  • volatile-ttl:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。
  • volatile-random:从已经设置过期时间的数据集中,随机挑选数据淘汰。
  • volatile-lfu:从已经设置过期时间的数据集中,会使用LFU算法选择设置了过期时间的键值对,即:最不常用的键值
  • allkeys-lru:从所有的数据集中,挑选最近最少使用的数据淘汰。
  • allkeys-random:从所有的数据集中,随机挑选数据淘汰。
  • allkeys-lfu:淘汰整个键值中最不常用的键值

附录:LRULFU是不同的:

  • LRU是最近最少使用页面置换算法(Least Recently Used),也就是首先淘汰最长时间未被使用的页面
  • LFU是最近最不常用页面置换算法(Least Frequently Used),也就是淘汰一定时期内被访问次数最少的页

使用策略规则:

  • 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
  • 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random

1.5 Redis过期键删除策略

Redis过期键删除策略:

  • 定时删除:在设置键的过期时间的同时,创建一个timer,让定时器在键的过期时间到达时,立即执行对键的删除操作。(主动删除)
    对内存友好,但是对cpu时间不友好,有较多过期键的而情况下,删除过期键会占用相当一部分cpu时间。
  • 惰性删除:放任过期键不管,但是每次从键空间中获取键时,都检查取到的键是否过去,如果过期就删除,如果没过期就返回该键。(被动删除)
    cpu时间友好,程序只会在取出键的时候才会对键进行过期检查,这不会在删除其他无关过期键上花费任何cpu时间,但是如果一个键已经过期,而这个键又保留在数据库中,那么只要这个过期键不被删除,他所占用的内存就不会释放,对内存不友好。
  • 定期删除:每隔一段时间就对数据库进行一次检查,删除里面的过期键。(主动删除)采用对内存cpu时间折中的方法,每隔一段时间就对一些 key 进行采样检查,检查是否过期,如果过期就进行删除
    1、采样一定个数的key,采样的个数可以进行配置,并将其中过期的 key 全部删除;
    2、如果过期 key 的占比超过可接受的过期 key 的百分比,则重复删除的过程,直到过期key的比例降至可接受的过期 key 的百分比以下

1.6 Redis的key和value可以存储的最大值分别是多少

虽然Key的大小上限为512M,但是一般建议key的大小不要超过1KB,这样既可以节约存储空间,又有利于Redis进行检索。
value的最大值也是512M。对于String类型的value值上限为512M,而集合、链表、哈希等key类型,单个元素的value上限也为512M

1.7 Redis实现数据的去重

  • Redisset:它可以去除重复元素,也可以快速判断某一个元素是否存在于集合中,如果元素很多(比如上亿的计数),占用内存很大。
  • Redisbit:它可以用来实现比set内存高度压缩的计数,它通过一个bit设置为1或者0,表示存储某个元素是否存在信息。例如网站唯一访客计数,可以把user_id作为 bit 的偏移量 offset,如设置为1表示有访问,使用1 MB的空间就可以存放800多万用户的一天访问计数情况。
  • HyperLogLog:实现超大数据量精确的唯一计数都是比较困难的,HyperLogLog可以仅仅使用 12 k左右的内存,实现上亿的唯一计数,而且误差控制在百分之一左右。
  • bloomfilter布隆过滤器:布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中

1.8 Redis序列化

Redis什么时候需要序列化?

  • 序列化:将 Java 对象转换成字节流的过程。
  • 反序列化:将字节流转换成 Java 对象的过程。
    图片

为什么需要序列化呢?
打个比喻:作为大城市漂泊的码农,搬家是常态。当我们搬书桌时,桌子太大了就通不过比较小的门,因此我们需要把它拆开再搬过去,这个拆桌子的过程就是序列化。而我们把书桌复原回来(安装)的过程就是反序列化啦。

比如想把内存中的对象状态保存到一个文件中或者数据库中的时候(最常用,如保存到redis);再比喻想用套接字在网络上传送对象的时候,都需要序列化。
RedisSerializer接口 是 Redis 序列化接口,用于 Redis KEYVALUE 的序列化,有如下序列化方式:

  • JDK 序列化方式 (默认)
  • String 序列化方式
  • JSON 序列化方式
  • XML 序列化方式

1.9 大key

1.9.1 定义

Redis 中的 大key 是指存储在Redis中的占用内存较大的键值对。大key可能会导致Redis的性能下降,因为大key占用的内存较多,需要较长的时间来进行读写操作。而且,当大key被删除时,会阻塞Redis的其他操作。

常见的大key包括:

  • 存储大量数据的字符串类型键值对。
  • 存储大量元素的列表、集合或有序集合。
  • 包含大量字段的哈希表。

1.9.2 大Key解决方案

为了避免大keyRedis性能的影响,可以采取以下措施:

  • 大key拆分为多个较小的键值对,以减少每个键值对的内存占用。
  • 使用分布式缓存,将大key分散到多个Redis实例上。
  • 使用压缩算法对大key进行压缩,减少内存占用。
  • 使用Redis的分片功能,将大key分散到多个分片上,减少单个Redis实例的负载。
  • 大Key进行清理。将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。注意,要使用异步删除。
  • 监控Redis的内存水位。可以通过监控系统设置合理的Redis内存报警阈值进行提醒,例如Redis内存使用率超过70%、Redis的内存在1小时内增长率超过20%等。
  • 对过期数据进行定期清。堆积大量过期数据会造成大Key的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。

1.10 热Key

1.10.1 定义

Redis热key是指在Redis中被频繁访问的键。当某个键被频繁访问时,它就被认为是热key热key通常是由于某些热门操作、热门数据或者高并发访问所导致的

通常以其接收到的 Key 被请求频率来判定,例如:

  • QPS集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。
  • 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL操作请求。
  • CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的ZRANGE操作请求。

1.10.2 如何解决热key

热key可能对Redis的性能产生重大影响。当一个键被频繁访问时,Redis需要频繁地从内存中读取或写入该键的值,这可能导致内存带宽的瓶颈、CPU利用率的增加以及延迟的增加。此外,如果一个热key的值过大,可能会占用大量的内存,进一步影响Redis的性能
解决方案:

  • Redis集群架构中对热Key进行复制,数据分片
    Redis集群架构中,由于热Key的迁移粒度问题,无法将请求分散至其他数据分片,导致单个数据分片的压力无法下降。此时,可以将对应热Key进行复制并迁移至其他数据分片,例如将热Key foo复制出3个内容完全一样的Key并名为foo2、foo3、foo4,将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。
  • 使用读写分离架构。
    如果热Key的产生来自于读请求,您可以将实例改造成读写分离架构来降低每个数据分片的读请求压力,甚至可以不断地增加从节点。但是读写分离架构在增加业务代码复杂度的同时,也会增加Redis集群架构复杂度。不仅要为多个从节点提供转发层(如Proxy,LVS等)来实现负载均衡,还要考虑从节点数量显著增加后带来故障率增加的问题。Redis集群架构变更会为监控、运维、故障处理带来了更大的挑战。
  • 使用合适的数据结构
    根据实际情况选择合适的数据结构,如列表、集合、有序集合等,来存储热key的值,以提高读写性能。
  • 缓存策略
    使用合理的缓存策略,比如设置合适的过期时间、使用LRU算法等,以减少对热key的访问频率。
  • 持久化策略
    根据业务需求选择合适的持久化方式,如RDB快照、AOF日志等,以确保数据的安全性和可靠性。

1.11 Redis中缓冲区

Redis中的缓冲区主要根据其功能和用途进行划分,可以归纳为以下几种:

  • 客户端缓冲区:
    • 输入缓冲区:缓存客户端发送过来的命令,Redis主线程从该缓冲区中读取命令进行处理。
    • 输出缓冲区:当 Redis主线程处理完数据后,将结果写入该缓冲区,再返回给客户端。
    • 缓存区溢出原因:
      • 写入了BigKey,即一次性写入了大量数据,超过了缓冲区的大小。
      • 服务端处理请求的速度过慢,导致无法及时处理请求,使得客户端发送的请求在缓冲区内越积越多。
  • 复制缓冲区:
    • 复制缓冲区:在全量复制过程中,主节点在向从节点传输RDB文件的同时,会继续接收客户端发送的写命令请求。这些写命令会先保存在复制缓冲区中,等RDB文件传输完成后,再发送给从节点去执行。
    • 复制积压缓冲区:在增量复制时,主节点和从节点进行常规同步时,会把写命令暂存在复制积压缓冲区中。
    • 溢出原因:
      • 主库传输RDB文件以及从库加载RDB文件耗时长,同时主库接收的写命令操作较多。
      • 缓冲区大小设置不合理。
  • AOF缓冲区:
    • AOF缓冲区:当Redis进行持久化时,会先将客户端传来的命令存放在AOF缓冲区,再根据具体的策略(always、everysec、no)去写入磁盘中的AOF文件中。
    • AOF重写缓冲区:在Redis进行AOF重写时,主进程会fork一个子进程进行AOF重写,此时主进程接收的指令会存放在AOF重写缓冲区中。当AOF重写完成后,这些指令会被追加到AOF文件中。
  • 内存级缓存:
    虽然不是严格意义上的缓冲区,但Redis的内存级缓存是其最常见的使用场景。通过将数据存储在内存中,减少读取数据库的频率,提高数据访问速度。
  • 其他内存使用:
    • Redis空进程自身内存消耗非常少,通常used_memory_rss在3MB左右,used_memory在800KB左右。
    • 对象内存:存储着用户所有的数据,消耗可以简单理解为sizeof(keys)+sizeof(values)。
    • 内存碎片:正常的碎片率在1.03左右

2 Redis监控

2.1 Redis监控指标

监控指标

  • 性能指标:Performance
  • 内存指标: Memory
  • 基本活动指标:Basic activity
  • 持久性指标: Persistence
  • 错误指标:Error

2.1.1 性能指标: Performance

Name Description
latency Redis响应一个请求的时间
instantaneous_ops_per_sec 平均每秒处理请求总数
hi rate(calculated)` 缓存命中率(计算出来的

2.1.2 内存指标: Memory

Name Description
used_memory 已使用内存
mem_fragmentation_ratio 内存碎片率
evicted_keys 由于最大内存限制被移除的key的数量
blocked_clients 由于BLPOP,BRPOP,or BRPOPLPUSH而备阻塞的客户端

2.1.3 基本活动指标:Basic activity

Name Description
connected_clients 客户端连接数
conected_laves slave数量
master_last_io_seconds_ago 最近一次主从交互之后的秒数
keyspace 数据库中的key值总数

2.1.4 持久性指标: Persistence

Name Description
rdb_last_save_time 最后一次持久化保存磁盘的时间戳
rdb_changes_sice_last_save 自最后一次持久化以来数据库的更改数

2.1.5 错误指标:Error

Name Description
rejected_connections 由于达到maxclient限制而被拒绝的连接数
keyspace_misses key 值查找失败(没有命中)次数
master_link_down_since_seconds 主从断开的持续时间(以秒为单位)

2.2 监控方式

Redis监控方式:

  • redis-benchmark
  • redis-stat
  • redis-faina
  • redislive
  • redis-cli
  • monitor
  • showlog
    get:获取慢查询日志
    len:获取慢查询日志条目数
    reset:重置慢查询日志

相关配置:

slowlog-log-slower-than 1000# 设置慢查询的时间下线,单位:微秒
slowlog-max-len 100 # 设置慢查询命令对应的日志显示长度,单位:命令数

2.2.1 info

info(可以一次性获取所有的信息,也可以按块获取信息)

  1. server:服务器运行的环境参数
  2. clients:客户端相关信息
  3. memory:服务器运行内存统计数据
  4. persistence:持久化信息
  5. stats:通用统计数据
  6. Replication:主从复制相关信息
  7. CPU:CPU使用情况
  8. cluster:集群信息
  9. Keypass:键值对统计数量信息

终端info命令使用

./redis-cli info 按块获取信息 | grep 需要过滤的参数
./redis-cli info stats | grep ops

交互式info命令使用

./redis-cli > info server

2.2.2 性能监控:

redis-cli info | grep ops # 每秒操作数

在这里插入图片描述

2.2.3 内存监控

内存监控

./redis-cli info | grep used | grep human
used_memory_human:2.99M #内存分配器从操作系统分配的内存总量
used_memory_rss_human:8.04M #操作系统看到的内存占用,top命令看到的内存
used_memory_peak_human:7.77M # redis内存消耗的峰值
used_memory_lua_human:37.00K # lua脚本引擎占用的内存大小

由于BLPOP,BRPOP,or BRPOPLPUSH而备阻塞的客户端,使用如下命令:

./redis-cli info | grep
blocked_clientsblocked_clients:0

由于最大内存限制被移除的key的数量

./redis-cli info | grep
evicted_keysevicted_keys:0 #

内存碎片率

./redis-cli info | grep
mem_fragmentation_ratiomem_fragmentation_ratio:2.74

已使用内存

./redis-cli info | grep
used_memory:used_memory:3133624

2.2.4 基本活动指标

redis连接了多少客户端 通过观察其数量可以确认是否存在意料之外的连接。如果发现数量不对劲,就可以使用lcient list指令列出所有的客户端链接地址来确定源头。

[root@CombCloud-2020110836 src]# ./redis-cli info | grep connected_clientsconnected_clients:1

[root@CombCloud-2020110836 src]# ./redis-cli info | grep connectedconnected_clients:1 # 客户端连接数量connected_slaves:1 # slave连接数量

2.2.5 持久性指标

持久性指标

[root@CombCloud-2020110836 src]# ./redis-cli info | grep rdb_last_save_timerdb_last_save_time:1591876204 # 最后一次持久化保存磁盘的时间戳
[root@CombCloud-2020110836 src]# ./redis-cli info | grep rdb_changes_since_last_saverdb_changes_since_last_save:0 # 自最后一次持久化以来数据库的更改数

2.2.6 错误指标

由于超出最大连接数限制而被拒绝的客户端连接次数,如果这个数字很大,则意味着服务器的最大连接数设置得过低,需要调整maxclients

[root@CombCloud-2020110836 src]# ./redis-cli info | grep
connected_clientsconnected_clients:1

key值查找失败(没有命中)次数,出现多次可能是被hei ke gongjji

[root@CombCloud-2020110836 src]# ./redis-cli info | grep
keyspacekeyspace_misses:0

主从断开的持续时间(以秒为单位)

[root@CombCloud-2020110836 src]# ./redis-cli info | grep
rdb_changes_since_last_saverdb_changes_since_last_save:0

复制积压缓冲区如果设置得太小,会导致里面的指令被覆盖掉找不到偏移量,从而触发全量同步

[root@CombCloud-2020110836 src]# ./redis-cli info | grep
backlog_sizerepl_backlog_size:1048576

通过查看sync_partial_err变量的次数来决定是否需要扩大积压缓冲区,它表示主从半同步复制失败的次数

[root@CombCloud-2020110836 src]# ./redis-cli info | grep sync_partial_errsync_partial_err:1

2.3 redis性能测试命令

./redis-benchmark -c 100 -n 5000
说明:100个连接,5000次请求对应的性能

在这里插入图片描述

2.4 Redis响应慢

一旦 Redis 请求延迟增加,可能就会导致业务系统雪崩

2.4.1 延迟基线测量

redis-cli 命令提供了--intrinsic-latency 选项,用来监测和统计测试期间内的最大延迟(以毫秒为单位),这个延迟可以作为 Redis 的基线性能。

redis-cli --latency -h host -p port

比如执行如下指令:

redis-cli --intrinsic-latency 100
Max latency so far: 4 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 41 microseconds.
Max latency so far: 57 microseconds.
Max latency so far: 78 microseconds.
Max latency so far: 170 microseconds.
Max latency so far: 342 microseconds.
Max latency so far: 3079 microseconds.

45026981 total runs (avg latency: 2.2209 microseconds / 2220.89 nanoseconds per run).
Worst run took 1386x longer than the average latency.

注意:参数100是测试将执行的秒数。我们运行测试的时间越长,我们就越有可能发现延迟峰值。
通常运行 100 秒通常是合适的,足以发现延迟问题了,当然我们可以选择不同时间运行几次,避免误差。

此次运行的最大延迟是 3079 微秒,所以基线性能是 3079 (3 毫秒)微秒。

需要注意的是,我们要在 Redis 的服务端运行,而不是客户端。这样,可以避免网络对基线性能的影响。

可以通过 -h host -p port 来连接服务端,如果想监测网络对 Redis 的性能影响,可以使用 Iperf 测量客户端到服务端的网络延迟。

如果网络延迟几百毫秒,说明网络可能有其他大流量的程序在运行导致网络拥塞,需要找运维协调网络的流量分配。

2.4.2 慢指令监控

如何判断是否是慢指令呢?
看操作复杂度是否是O(N)。官方文档对每个命令的复杂度都有介绍,尽可能使用O(1)O(log N)命令。

涉及到集合操作的复杂度一般为O(N),比如集合全量查询HGETALL、SMEMBERS,以及集合的聚合操作:SORT、LREM、 SUNION等。

那么有监控数据可以观测呢?代码不是我写的,不知道有没有人用了慢指令。
有两种方式可以排查到:

  • 使用 Redis 慢日志功能查出慢命令;
  • latency-monitor(延迟监控)工具。

此外,可以使用自己(top、htop、prstat 等)快速检查 Redis 主进程的 CPU 消耗。如果 CPU 使用率很高而流量不高,通常表明使用了慢速命令。

2.4.2.1 慢日志功能

Redis 中的 slowlog 命令可以让我们快速定位到那些超出指定执行时间的慢命令,默认情况下命令若是执行时间超过 10ms 就会被记录到日志。

slowlog 只会记录其命令执行的时间,不包含 io 往返操作,也不记录单由网络延迟引起的响应慢。

我们可以根据基线性能来自定义慢命令的标准(配置成基线性能最大延迟的 2 倍),调整触发记录慢命令的阈值。

可以在 redis-cli 中输入以下命令配置记录 6 毫秒以上的指令:

redis-cli CONFIG SET slowlog-log-slower-than 6000

也可以在 Redis.config 配置文件中设置,以微秒为单位。

想要查看所有执行时间比较慢的命令,可以通过使用 Redis-cli 工具,输入 slowlog get 命令查看,返回结果的第三个字段以微秒位单位显示命令的执行时间。

假如只需要查看最后 2 个慢命令,输入 slowlog get 2 即可。
示例:获取最近2个慢查询命令

127.0.0.1:6381> SLOWLOG get 2
1) 1) (integer) 6
   2) (integer) 1458734263
   3) (integer) 74372
   4) 1) "hgetall"
      2) "max.dsp.blacklist"
2) 1) (integer) 5
   2) (integer) 1458734258
   3) (integer) 5411075
   4) 1) "keys"
      2) "max.dsp.blacklist"

以第一个 HGET 命令为例分析,每个 slowlog 实体共 4 个字段:

  • 字段 1:1 个整数,表示这个 slowlog 出现的序号,server 启动后递增,当前为 6。
  • 字段 2:表示查询执行时的Unix时间戳。
  • 字段 3:表示查询执行微秒数,当前是 74372 微秒,约 74ms。
  • 字段 4: 表示查询的命令参数,如果参数很多或很大,只会显示部分参数个数。当前命令是hgetall max.dsp.blacklist。

2.4.2.2 Latency Monitoring

Redis2.8.13 版本引入了 Latency Monitoring 功能,用于以秒为粒度监控各种事件的发生频率。

启用延迟监视器的第一步是设置延迟阈值(单位毫秒)。只有超过该阈值的时间才会被记录,比如我们根据基线性能(3ms)的 3 倍设置阈值为 9 ms。

可以用 redis-cli 设置也可以在 Redis.config 中设置;

CONFIG SET latency-monitor-threshold 9

工具记录的相关事件的详情可查看官方文档:https://redis.io/topics/latency-monitor

如获取最近的 latency

127.0.0.1:6379> debug sleep 2
OK
(2.00s)
127.0.0.1:6379> latency latest
1) 1) "command"
   2) (integer) 1645330616
   3) (integer) 2003
   4) (integer) 2003

以上字段说明:

  1. 事件的名称;
  2. 事件发生的最新延迟的 Unix 时间戳;
  3. 毫秒为单位的时间延迟;
  4. 该事件的最大延迟。

2.4.3 如何解决 Redis 变慢

Redis 的数据读写由单线程执行,如果主线程执行的操作时间太长,就会导致主线程阻塞。
那么分析下都有哪些操作会阻塞主线程,我们又该如何解决?

2.4.3.1 网络通信导致的延迟

客户端使用 TCP/IP 连接或 Unix 域连接连接到 Redis1 Gbit/s 网络的典型延迟约为 200 us

redis 客户端执行一条命令分 4 个过程:

发送命令-〉 命令排队 -〉 命令执行-〉 返回结果

这个过程称为 Round trip time(简称 RTT, 往返时间),mget mset 有效节约了 RTT,但大部分命令(如 hgetall,并没有 mhgetall)不支持批量操作,需要消耗 N RTT ,这个时候需要 pipeline来解决这个问题。

Redis pipeline 将多个命令连接在一起来减少网络响应往返次数。

在这里插入图片描述

2.4.3.2 慢指令导致的延迟

根据上文的慢指令监控查询文档,查询到慢查询指令。可以通过以下两种方式解决:

  • Cluster 集群中,将聚合运算等 O(N) 操作运行在 slave 上,或者在客户端完成。
  • 使用高效的命令代替。使用增量迭代的方式,避免一次查询大量数据,具体请查看SCAN、SSCAN、HSCAN和ZSCAN命令。

除此之外,生产中禁用KEYS 命令,它只适用于调试。因为它会遍历所有的键值对,所以操作延时高。

2.4.3.3 Fork生成 RDB导致的延迟

生成 RDB 快照,Redis 必须 fork 后台进程。fork 操作(在主线程中运行)本身会导致延迟
Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化,减少内存占用。

在这里插入图片描述

fork 会涉及到复制大量链接对象,一个 24 GB 的大型 Redis 实例需要 24 GB / 4 kB * 8 = 48 MB 的页表。
执行 bgsave 时,这将涉及分配和复制 48 MB 内存。

此外,从库加载 RDB 期间无法提供读写服务,所以主库的数据量大小控制在 2~4G 左右,让从库快速的加载完成。

2.4.3.4 内存大页(transparent huge pages)

常规的内存页是按照 4 KB 来分配,Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配。

Redis 使用了 fork 生成 RDB 做持久化提供了数据可靠性保证。

当生成 RDB 快照的过程中,Redis 采用 写时复制 技术使得主线程依然可以接收客户端的写请求。

也就是当数据被修改的时候,Redis 会复制一份这个数据,再进行修改。

采用了内存大页,生成 RDB 期间,即使客户端修改的数据只有 50B 的数据,Redis 需要复制 2MB 的大页。当写的指令比较多的时候就会导致大量的拷贝,导致性能变慢。

使用以下指令禁用 Linux 内存大页即可:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

2.4.3.5 swap:操作系统分页

当物理内存(内存条)不够用的时候,将部分内存上的数据交换到 swap 空间上,以便让系统不会因内存不够用而导致 oom 或者更致命的情况出现。

当某进程向 OS 请求内存发现不足时,OS 会把内存中暂时不用的数据交换出去,放在 SWAP 分区中,这个过程称为 SWAP OUT

当某进程又需要这些数据且 OS 发现还有空闲物理内存时,又会把 SWAP 分区中的数据交换回物理内存中,这个过程称为 SWAP IN
内存 swap 是操作系统里将内存数据在内存磁盘间来回换入和换出的机制,涉及到磁盘的读写。

触发 swap 的情况有哪些呢?
对于 Redis 而言,有两种常见的情况:

  • Redis 使用了比可用内存更多的内存;
  • Redis 在同一机器运行的其他进程在执行大量的文件读写 I/O 操作(包括生成大文件的 RDB 文件和 AOF 后台线程),文件读写占用内存,导致 Redis 获得的内存减少,触发了 swap

那么如何排查是否因为 swap 导致的性能变慢呢?

Linux 提供了很好的工具来排查这个问题,所以当怀疑由于交换导致的延迟时,只需按照以下步骤排查。

2.4.3.5.1 获取 Redis 实例 pid
$ redis-cli info | grep process_id
process_id:13160

进入此进程的 /proc 文件系统目录:

cd /proc/13160

在这里有一个 smaps 的文件,该文件描述了 Redis 进程的内存布局,运行以下指令,用 grep 查找所有文件中的 Swap 字段。

$ cat smaps | egrep '^(Swap|Size)'
Size:                316 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  8 kB
Swap:                  0 kB
Size:                 40 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:             720896 kB
Swap:                 12 kB

每行 Size 表示 Redis 实例所用的一块内存大小,和 Size 下方的 Swap 对应这块 Size 大小的内存区域有多少数据已经被换出到磁盘上了。
如果 Size == Swap 则说明数据被完全换出了。

可以看到有一个 720896 kB 的内存大小有 12 kb 被换出到了磁盘上(仅交换了 12 kB),这就没什么问题。

Redis 本身会使用很多大小不一的内存块,所以,你可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 720896KB。不同内存块被换出到磁盘上的大小也不一样。

注意 : 如果 Swap 一切都是 0 kb,或者零星的 4k ,那么一切正常。
当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时,Redis 实例的内存压力很大,很有可能会变慢。

2.4.3.5.2 解决方案
  1. 增加机器内存;
  2. Redis 放在单独的机器上运行,避免在同一机器上运行需要大量内存的进程,从而满足 Redis 的内存需求;
  3. 增加Cluster集群的数量分担数据量,减少每个实例所需的内存。

2.4.3.6 AOF 和磁盘 I/O 导致的延迟

为了保证数据可靠性,Redis 使用 AOFRDB 快照实现快速恢复和持久化。
可以使用 appendfsync 配置将 AOF 配置为以三种不同的方式在磁盘上执行 write 或者 fsync (可以在运行时使用 CONFIG SET命令修改此设置,比如:redis-cli CONFIG SET appendfsync no)。

  • noRedis 不执行 fsync,唯一的延迟来自于 write 调用,write 只需要把日志记录写到内核缓冲区就可以返回。
  • everysecRedis 每秒执行一次 fsync。使用后台子线程异步完成 fsync 操作。最多丢失 1s 的数据。
  • always:每次写入操作都会执行 fsync,然后用 OK 代码回复客户端(实际上 Redis 会尝试将同时执行的许多命令聚集到单个 fsync 中),没有数据丢失。在这种模式下,性能通常非常低,强烈建议使用快速磁盘和可以在短时间内执行 fsync 的文件系统实现。

我们通常将 Redis 用于缓存,数据丢失完全可以从数据获取,并不需要很高的数据可靠性,建议设置成 no 或者 everysec

除此之外,避免 AOF 文件过大, Redis 会进行 AOF 重写,生成缩小的 AOF 文件。

可以把配置项 no-appendfsync-on-rewrite设置为 yes,表示在 AOF 重写时,不进行 fsync 操作。也就是说,Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就直接返回了。

2.4.3.7 expires淘汰过期数据

Redis 有两种方式淘汰过期数据:

  • 惰性删除:当接收请求的时候发现 key 已经过期,才执行删除;
  • 定时删除:每 100 毫秒删除一些过期的 key

定时删除的算法如下:

  1. 随机采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP个数的 key,删除所有过期的 key
  2. 如果发现还有超过 25%key 已过期,则执行步骤一。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP默认设置为 20,每秒执行 10 次,删除 200key 问题不大。

如果触发了第二条,就会导致 Redis 一致在删除过期数据去释放内存。而删除是阻塞的

那么触发条件是什么

就是大量的 key 设置了相同的时间参数。同一秒内,大量 key 过期,需要重复删除多次才能降低到 25% 以下。

简而言之:大量同时到期的 key 可能会导致性能波动。

2.4.3.7.1 解决方案

如果一批 key 的确是同时过期,可以在 EXPIREATEXPIRE 的过期时间参数上,加上一个一定大小范围内的随机数,这样,既保证了 key 在一个邻近时间范围内被删除,又避免了同时过期造成的压力。

2.4.3.8 bigkey

通常我们会将含有较大数据或含有大量成员、列表数的 Key 称之为大 Key,下面我们将用几个实际的例子对大 Key 的特征进行描述:

  • 一个 STRING 类型的 Key,它的值为 5MB(数据过大)
  • 一个 LIST 类型的 Key,它的列表数量为 10000 个(列表数量过多)
  • 一个 ZSET 类型的 Key,它的成员数量为 10000 个(成员数量过多)
  • 一个 HASH 格式的 Key,它的成员数量虽然只有 1000 个但这些成员的 value 总大小为 10MB(成员体积过大)

bigkey 带来问题如下:

  1. Redis 内存不断变大引发 OOM,或者达到 maxmemory 设置值引发写阻塞或重要 Key 被逐出;
  2. Redis Cluster 中的某个 node 内存远超其余 node,但因 Redis Cluster 的数据迁移最小粒度为 Key 而无法将 node 上的内存均衡化;
  3. bigkey 的读请求占用过大带宽,自身变慢的同时影响到该服务器上的其它服务;
  4. 删除一个 bigkey 造成主库较长时间的阻塞并引发同步中断或主从切换;
2.4.3.8.1 查找bigkey

使用 redis-rdb-tools 工具以定制化方式找出大 Key

2.4.3.8.2 解决方案
  1. 对大 key 拆分
    如将一个含有数万成员的 HASH Key 拆分为多个 HASH Key,并确保每个 Key 的成员数量在合理范围,在 Redis Cluster 结构中,大 Key 的拆分对 node 间的内存平衡能够起到显著作用。
  2. 异步清理大key
    Redis 自 4.0 起提供了 UNLINK 命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的 Key,通过 UNLINK,可以安全的删除大Key甚至特大 Key
posted @ 2021-08-30 19:10  上善若泪  阅读(728)  评论(0编辑  收藏  举报