第78篇 Redis常见耽误问题

海哥  金牌会员 | 2024-12-8 23:02:47 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 876|帖子 876|积分 2628

使用复杂度高的下令

Redis提供了慢日志下令的统计功能
首先设置Redis的慢日志阈值,只有超过阈值的下令才会被记录,这里的单位是微妙,比方设置慢日志的阈值为5毫秒,同时设置只保留最近1000条慢日志记录:
  1. # 命令执行超过5毫秒记录慢日志
  2. CONFIG SET slowlog-log-slower-than 5000
  3. # 只保留最近1000条慢日志
  4. CONFIG SET slowlog-max-len 1000
复制代码
实行SLOWLOG get 5查询最近5条慢日志
  1. 127.0.0.1:6379> SLOWLOG get 5
  2. 1) 1) (integer) 32693       # 慢日志ID
  3.    2) (integer) 1593763337  # 执行时间
  4.    3) (integer) 5299        # 执行耗时(微秒)
  5.    4) 1) "LRANGE"           # 具体执行的命令和参数
  6.           2) "user_list_2000"
  7.           3) "0"
  8.           4) "-1"
  9. 2) 1) (integer) 32692
  10.    2) (integer) 1593763337
  11.    3) (integer) 5044
  12.    4) 1) "GET"
  13.           2) "book_price_1000"
  14. ...
复制代码
通过查看慢日志记录,就可以知道在什么时间实行哪些下令比力耗时,如果服务哀求量并不大,但Redis实例的CPU使用率很高,很有可能就是使用了复杂度高的下令导致的。
好比常常使用O(n)以上复杂度的下令,由于Redis是单线程实行下令,因此这种环境Redis处理数据时就会很耗时。比方

  • sort:对列表(list)、聚集(set)、有序聚集(sorted set)中的元素进行排序。在最简单的环境下(没有权重、没有模式、没有 LIMIT),SORT 下令的时间复杂度近似于 O(n*log(n))
  • sunion:用于计算两个或多个聚集的并集。时间复杂度可以描述为 O(N),其中 **N **是全部参与运算聚集的元素总数。如果有多个聚集,每个聚集有不同数量标元素参与运算,那么复杂度会是全部这些聚集元素数量标总和。
  • zunionstore:用于计算一个或多个有序聚集的并集,并将结果存储到一个新的有序聚集中。在最简单的环境下,ZUNIONSTORE 下令的时间复杂度是 O(N*log(N)),其中 N 是全部参与计算的聚集中元素的总数。
  • keys * :获取全部的 key 操作;复杂度O(n),数据量越大实行速度越慢;可以使用scan下令替代
  • Hgetall:返回哈希表中全部的字段和;
  • smembers:返回聚集中的全部成员;
解决方案就是,不使用这些复杂度较高的下令,而且一次不要获取太多的数据,每次只管操作少量的数据,让Redis可以实时处理返回
存储大key

如果查询慢日志发现,并不是复杂度较高的下令导致的,比方都是SET、DELETE操作出如今慢日志记录中,那么就要猜疑是否存在Redis写入了大key的环境。
多大才算大

如果一个 key 对应的 value 所占用的内存比力大,那这个 key 就可以看作是 bigkey。

  • String 类型的 value 超过 1MB
  • 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
产生原因


  • 程序筹划不当,好比直接使用 String 类型存储较大的文件对应的二进制数据。
  • 对于业务的数据规模考虑不周到,好比使用聚集类型的时间没有考虑到数据量的快速增长。
  • 未实时清理垃圾数据,好比哈希中冗余了大量的无用键值对。
造成的问题


  • 客户端超时壅闭:由于 Redis 实行下令是单线程处理,然后在操作大 key 时会比力耗时,那么就会壅闭 Redis,从客户端这一视角看,就是很久很久都没有相应。
  • 网络壅闭:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 工作线程壅闭:如果使用 del 删除大 key 时,会壅闭工作线程,这样就没办法处理后续的下令。
  • 持久化壅闭(磁盘IO):对AOF 日志的影响



      • 使用Always 计谋的时间,主线程在实行完下令后,会把数据写入到 AOF 日志文件,然后会调用 fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。因此当使用 Always 计谋的时间,如果写入是一个大 Key,主线程在实行 fsync() 函数的时间,壅闭的时间会比力久,因为当写入的数据量很大的时间,数据同步到硬盘这个过程是很耗时的。



      • 别的两种计谋都不影响主线程


大 key 造成的壅闭问题还会进一步影响到主从同步和集群扩容。
怎样发现 bigkey?


  • 使用 Redis 自带的 --bigkeys 参数来查找:这个下令会扫描(Scan) Redis 中的全部 key ,会对 Redis 的性能有一点影响,最好选择在从节点上实行该下令,因为主节点上实行时,会壅闭主节点。而且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,必要我们根据具体的业务环境来进一步判断。
  • Redis 自带的 SCAN 下令:SCAN 下令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 STRLEN、HLEN、LLEN等下令返回其长度或成员数量。


  • 借助开源工具分析 RDB 文件:这种方案的条件是Redis 采取的是 RDB 持久化。网上有现成的工具:


    • redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
    • rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。

怎样处理 bigkey?


  • 删除大 key:删除大 key 时建议采取分批次删除和异步删除的方式进行;


    • 因为删除大 key释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作体系必要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身必要一定时间,而且会壅闭当前释放内存的应用程序。



    • 所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增长,相应地就会造成 Redis 主线程的壅闭,如果主线程发生了壅闭,其他全部哀求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。

  • 分割 bigkey:将一个 bigkey 分割为多个小 key。比方,将一个含有上万字段数量标 Hash 按照一定计谋(好比二次哈希)拆分为多个 Hash。
  • 手动清理:Redis 4.0+ 可以使用 UNLINK 下令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 下令结合 DEL 下令来分批次删除。
  • 采取合适的数据结构:比方,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
  • 开启 lazy-free(惰性删除/耽误释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采取异步方式耽误释放 key 使用的内存,将该操作交给单独的子线程处理,制止壅闭主线程。
集中过期

Redis的过期计谋采取 定期过期+懒惰过期两种计谋:

  • 定期过期:Redis内部维护一个定时任务,默认每秒进行10次(也就是每隔100毫秒一次)过期扫描,从过期字典中随机取出20个key,删除过期的key,如果过期key的比例还超过25%,则继续获取20个key,删除过期的key,循环往复,直到过期key的比例降落到25%或者这次任务的实行耗时超过了25毫秒,才会退出循环
  • 懒惰过期:只有当访问某个key时,才判断这个key是否已过期,如果已经过期,则从实例中删除
Redis的定期删除计谋是在Redis主线程中实行的,也就是说如果在实行定期删除的过程中,出现了必要大量删除过期key的环境,那么在业务访问时,必须等这个定期删除任务实行结束,才可以处理业务哀求。此时就会出现,业务访问延时增大的问题,最大耽误为25毫秒。
为了只管制止这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,制止同一时刻过期。
伪代码可以这么写:
  1. # 在过期时间点之后的5分钟内随机过期掉
  2. redis.expireat(key, expire_time + random(300))
复制代码
实例内存达到上限

生产中会给内存设置上限maxmemory,当数据内存达到 maxmemory 时,便会触发redis的内存镌汰计谋
那么当实例的内存达到了maxmemory后,就会发现之后每次写入新的数据,就似乎变慢了。导致变慢的原因是,当Redis内存达到maxmemory后,每次写入新的数据之前,会先根据内存镌汰计谋先踢出一部门数据,让内存维持在maxmemory之下。
而内存镌汰计谋就决定这个踢出数据的时间长短:

  • 最常使用的一般是allkeys-lru或volatile-lru计谋,Redis 内存镌汰时,会使用随机采样的方式来镌汰数据,它是随机取 5 个值 (此值可配置) ,然后镌汰一个最少访问的key,之后把剩下的key暂存到一个池子中,继续随机取出一批key,并与之前池子中的key比力,再镌汰一个最少访问的key。以此循环,直到内存降到maxmemory之下。
  • 如果使用的是allkeys-random或volatile-random计谋,那么就会快很多,因为是随机镌汰,那么就少了比力key访问频率时间的消耗了,随机拿出一批key后直接镌汰即可,因此这个计谋要比上面的LRU计谋实行快一些。
但以上这些镌汰计谋的逻辑都是在访问Redis时,真正下令实行之前实行的,也就是它会影响真正必要实行的下令。
别的,如果此时Redis实例中有存储大key,那么在镌汰大key释放内存时,这个耗时会更加久,耽误更大
AOF持久化

同步持久化

当 Redis 直接记录 AOF 日志时,如果有大量的写操作,而且配置为同步持久化
  1. appendfsync always
复制代码
即每次发生数据变更会被立即记录到磁盘,而且Always写回计谋是由主进程实行的,而写磁盘比力耗时,性能较差,所以偶然会壅闭主线程。
AOF重写

  • fork 出一条子线程来将文件重写,在实行 BGREWRITEAOF 下令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器实行的全部写下令。
  • 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的全部内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态同等。
  • 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
壅闭就是出如今第2步的过程中,将缓冲区中新数据写到新文件的过程中会产生壅闭
fork耗时

生成RDB和AOF重写都必要父进程fork出一个子进程进行数据的持久化,在fork实行过程中,父进程必要拷贝内存页表给子进程,如果整个实例内存占用很大,那么必要拷贝的内存页表会比力耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被壅闭住,无法处理任何哀求,如果此时CPU资源告急,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。
Redis 在进行 RDB 快照的时间,会调用体系函数 fork() ,创建一个子线程来完成临时文件的写入,而触发条件正是配置文件中的 save 配置。当达到配置时,就会触发 bgsave 下令创建快照,这种方式是不会壅闭主线程的,而手动实行 save 下令会在主线程中实行,壅闭主线程。
除了因为备份的原因生成RDB之外,在【主从复制】第一次建立连接全量复制时,主节点也会生成RDB文件给从节点进行一次全量同步,这时也会对Redis产生性能影响。
要想制止这种环境,必要规划好数据备份的周期,建议在从节点上实行备份,而且最好放在低峰期实行。如果对于丢失数据不敏感的业务,那么不建议开启AOF和AOF重写功能。
集群扩容

Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,必要人工介入。
在扩缩容的时间,必要进行数据迁徙。而 Redis 为了包管迁徙的同等性,迁徙全部操作都是同步操作。
实行迁徙时,两端的 Redis 均会进入时长不等的壅闭状态,对于小Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时间会触发集群内的故障转移,造成不必要的切换。
总结


  • 使用复杂度高的下令,实行下令时就会耗时
  • 存储大key:如果一个key写入的数据非常大,Redis在分配内存、删除大key时都会耗时,而且持久化AOF的写回计谋是always时会影响Redis性能
  • 集中过期:Redis的自动过期的定时任务,是在Redis主线程中实行的,最差的环境下会有25ms的壅闭
  • 实例内存达到上限时,镌汰计谋的逻辑都是在访问Redis时,真正下令实行之前实行的,也就是它会影响真正必要实行的下令。
  • fork耗时:生成RDB和AOF重写都必要父进程fork出一个子进程进行数据的持久化,如果整个实例内存占用很大,那么必要拷贝的内存页表会比力耗时
额外总结大key的影响:
如果一个key写入的数据非常大,Redis在分配内存、删除大key时都会耗时。
当实例内存达到上限时,在镌汰大key释放内存时,内存镌汰计谋的耗时会更加久,耽误更大
AOF持久化时,使用always机制,这个操作是在主线程中实行的,如果写入是一个大 Key,主线程在实行 fsync() 函数的时间,壅闭的时间会更久。
生成RDB和AOF重写时会fork出一个子进程进行数据的持久化,父进程必要拷贝内存页表给子进程,如果整个实例内存占用很大,那么必要拷贝的内存页表会比力耗时。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

海哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表