阅读《Redis深度历险:核心原理和应用实践》的总结-原理-04

一、info命令

在使用 Redis 时,时常会遇到很多问题需要诊断,在诊断之前需要了解 Redis 的运行状 态,通过强大的 Info 指令,你可以清晰地知道 Redis 内部一系列运行参数。

Info 指令显示的信息非常繁多,分为 9 大块,每个块都有非常多的参数,这 9 个块分 别是:

  • 1、Server 服务器运行的环境参数
  • 2、Clients 客户端相关信息
  • 3、Memory 服务器运行内存统计数据
  • 4、Persistence 持久化信息
  • 5、Stats 通用统计数据
  • 6、Replication 主从复制相关信息
  • 7、CPU CPU 使用情况
  • 8、Cluster 集群信息
  • 9、KeySpace 键值对统计数量信息

Info 可以一次性获取所有的信息,也可以按块取信息。

# 获取所有信息

> info

# 获取内存相关信息

> info memory

# 获取复制相关信息

> info replication

考虑到参数非常繁多,一一说明工作量巨大,下面我只挑一些关键性的、非常实用和最 常用的参数进行详细讲解。如果读者想要了解所有的参数细节,请参考阅读 Redis 官网文 档。

1、Redis 每秒执行多少次指令?

这个信息在 Stats 块里,可以通过 info stats 看到。

# ops_per_sec: operations per second,也就是每秒操作数

> redis-cli info stats |grep ops instantaneous_ops_per_sec:789

以上,表示 ops 是 789,也就是所有客户端每秒会发送 789 条指令到服务器执行。极 限情况下,Redis 可以每秒执行 10w 次指令,CPU 几乎完全榨干。如果 qps 过高,可以考 虑通过 monitor 指令快速观察一下究竟是哪些 key 访问比较频繁,从而在相应的业务上进 行优化,以减少 IO 次数。monitor 指令会瞬间吐出来巨量的指令文本,所以一般在执行 monitor 后立即 ctrl+c 中断输出。

> redis-cli monitor

2、Redis 连接了多少客户端?

这个信息在 Clients 块里,可以通过 info clients 看到。

> redis-cli info clients

# Clients
connected_clients:124 # 这个就是正在连接的客户端数量 client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

这个信息也是比较有用的,通过观察这个数量可以确定是否存在意料之外的连接。如果 发现这个数量不对劲,接着就可以使用 client list 指令列出所有的客户端链接地址来确定源 头。

关于客户端的数量还有个重要的参数需要观察,那就是 rejected_connections,它表示因为超出最大连接数限制而被拒绝的客户端连接次数,如果这个数字很大,意味着服务器的最 大连接数设置的过低需要调整 maxclients 参数。

> redis-cli info stats |grep reject     
rejected_connections:0

Redis 内存占用多大 ?

这个信息在 Memory 块里,可以通过 info memory 看到。

> redis-cli info memory | grep used | grep human
used_memory_human:827.46K # 内存分配器 (jemalloc) 从操作系统分配的内存总量 
used_memory_rss_human:3.61M # 操作系统看到的内存占用 ,top 命令看到的内存 
used_memory_peak_human:829.41K # Redis 内存消耗的峰值 
used_memory_lua_human:37.00K # lua 脚本引擎占用的内存大小

如果单个 Redis 内存占用过大,并且在业务上没有太多压缩的空间的话,可以考虑集群化了。

复制积压缓冲区多大?

这个信息在 Replication 块里,可以通过 info replication 看到。

> redis-cli info replication |grep backlog repl_backlog_active:0
repl_backlog_size:1048576 # 这个就是积压缓冲区大小 repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制积压缓冲区大小非常重要,它严重影响到主从复制的效率。当从库因为网络原因临 时断开了主库的复制,然后网络恢复了,又重新连上的时候,这段断开的时间内发生在 master 上的修改操作指令都会放在积压缓冲区中,这样从库可以通过积压缓冲区恢复中断的 主从同步过程。

积压缓冲区是环形的,后来的指令会覆盖掉前面的内容。如果从库断开的时间过长,或 者缓冲区的大小设置的太小,都会导致从库无法快速恢复中断的主从同步过程,因为中间的 修改指令被覆盖掉了。这时候从库就会进行全量同步模式,非常耗费 CPU 和网络资源。 如果有多个从库复制,积压缓冲区是共享的,它不会因为从库过多而线性增长。如果实 例的修改指令请求很频繁,那就把积压缓冲区调大一些,几十个 M 大小差不多了,如果很闲,那就设置为几个 M。

> redis-cli info stats | grep sync sync_full:0
sync_partial_ok:0
sync_partial_err:0 # 半同步失败次数

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


二、过期策略

Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。你可以想象Redis 内部有一个死神,时刻盯着所有设置了过期时间的 key,寿命一到就会立即收割。

你还可以进一步站在死神的角度思考,会不会因为同一时间太多的 key 过期,以至于忙不过来。同时因为 Redis 是单线程的,收割的时间也会占用线程的处理时间,如果收割的太过于繁忙,会不会导致线上读写指令出现卡顿。

这些问题 Antirez 早就想到了,所有在过期这件事上,Redis 非常小心。

过期的 key 集合

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个 字典来删除到期的 key除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓 惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期 了就立即删除。定时删除是集中处理,惰性删除是零散处理。

定时扫描策略

Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

  • 1、从过期字典中随机 20 个 key;
  • 2、删除这 20 个 key 中已经过期的 key;
  • 3、如果过期的 key 比率超过 1/4,那就重复步骤 1;

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

设想一个大型的 Redis 实例中所有的 key 在同一时间过期了,会出现怎样的结果?

毫无疑问,Redis 会持续扫描过期字典 (循环多次),直到过期字典中过期的 key 变得稀疏,才会停止 (循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这 种卡顿的另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的 CPU 消耗。

也许你会争辩说“扫描不是有 25ms 的时间上限了么,怎么会导致卡顿呢”?这里打个 比方,假如有 101 个客户端同时将请求发过来了,然后前 100 个请求的执行时间都是 25ms,那么第 101 个指令需要等待多久才能执行?2500ms,这个就是客户端的卡顿时间, 是由服务器不间断的小卡顿积少成多导致的。

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置 一个随机范围,而不能全部在同一时间过期。

# 在目标过期时间上增加一天的随机时间

redis.expire_at(key, random.randint(86400) + expire_ts)

在一些活动系统中,因为活动是一期一会,下一期活动举办时,前面几期的很多数据都可以丢弃了,所以需要给相关的活动数据设置一个过期时间,以减少不必要的 Redis 内存占 用。如果不加注意,你可能会将过期时间设置为活动结束时间再增加一个常量的冗余时间, 如果参与活动的人数太多,就会导致大量的 key 同时过期。

掌阅服务端在开发过程中就曾出现过多次因为大量 key 同时过期导致的卡顿报警现象, 通过将过期时间随机化总是能很好地解决了这个问题,希望读者们今后能少犯这样的错误。

从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在,比如上一节的集群环境分布式锁的算法漏洞就是因为这个同步延迟产生的。


三、LRU

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。 交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。

在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提 供了配置参数 maxmemory 来限制内存超出期望大小。

当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让 用户自己决定该如何腾出新的空间以继续提供读写服务。

以下是一些策略:

  • noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
  • volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
  • volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl越小越优先被淘汰。
  •  volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
  • allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
  • allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
  • volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时 不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘 汰。

LRU 算法

实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照 一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢。

近似 LRU 算法

Redis 使用的是一种近似 LRU 算法,它跟 LRU 算法还不太一样。之所以不使用 LRU算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似LRU 算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和 LRU 算法非常近似的效果。Redis 为实现近似 LRU 算法,它给每个 key 增加了一个额外的小字 段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。

上一节提到处理 key 过期方式分为集中处理和懒惰处理,LRU 淘汰不一样,它的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最 旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。

如何采样就是看 maxmemory-policy 的配置,如果是 allkeys 就是从所有的 key 字典中 随机,如果是 volatile 就从带过期时间的 key 字典中随机。每次采样多少个 key 看的是 maxmemory_samples 的配置,默认为 5。

下面是随机 LRU 算法和严格 LRU 算法的效果对比图:

图中绿色部分是新加入的 key,深灰色部分是老旧的 key,浅灰色部分是通过 LRU 算 法淘汰掉的 key。从图中可以看出采样数量越大,近似 LRU 算法的效果越接近严格 LRU 算法。同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。

淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出 来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余 较旧的 key 列表放入淘汰池中留待下一个循环。


四、懒惰删除

一直以来我们认为 Redis 是单线程的,单线程为 Redis 带来了代码的简洁性和丰富多样 的数据结构。不过 Redis 内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作。

Redis 为什么要懒惰删除(lazy free)?

删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延 迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删 除操作就会导致单线程卡顿。

Redis 为了解决这个卡顿问题,在 4.0 版本引入了 unlink 指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存。

> unlink key OK

如果有多线程的开发经验,你肯定会担心这里的线程安全问题,会不会出现多个线程同时并发修改数据结构的情况存在。

关于这点,我打个比方。可以将整个 Redis 内存里面所有有效的数据想象成一棵大树。 当 unlink 指令发出时,它只是把大树中的一个树枝别断了,然后扔到旁边的火堆里焚烧 (异步线程池)。树枝离开大树的一瞬间,它就再也无法被主线程中的其它指令访问到了,因为主 线程只会沿着这颗大树来访问。

flush

Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。

Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树连根拔起,扔给后台线程慢慢焚烧。

> flushall async

OK

异步队列

主线程将对象的引用从「大树」中摘除后,会将这个 key 的内存回收操作包装成一个任 务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作,所以必须是一个线程安全的队列

不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就 没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样。

AOF Sync 也很慢

Redis 需要每秒一次(可配置)同步 AOF 日志到磁盘,确保消息尽量不丢失,需要调用 sync 函数,这个操作会比较耗时,会导致主线程的效率下降,所以 Redis 也将这个操作移到异步线程来完成。执行 AOF Sync 操作的线程是一个独立的异步线程,和前面的懒惰删除线程不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放 AOF Sync 任务。

更多异步删除点

Redis 回收内存除了 del 指令和 flush 之外,还会存在于在 key 的过期、LRU 淘汰、 rename 指令以及从库全量同步时接受完 rdb 文件后会立即进行的 flush 操作。

Redis4.0 为这些删除点也带来了异步删除机制,打开这些点需要额外的配置选项。

  • 1、slave-lazy-flush 从库接受完 rdb 文件后的 flush 操作
  • 2、lazyfree-lazy-eviction 内存达到 maxmemory 时进行淘汰
  • 3、lazyfree-lazy-expire key 过期删除
  • 4、lazyfree-lazy-server-delrename 指令删除 destKey

redis懒惰处理的细节:https://yq.aliyun.com/articles/205504

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值