Redis变慢?深入浅出Redis性能诊断系列文章(二)

打印 上一主题 下一主题

主题 553|帖子 553|积分 1659

(本文首发于“数据库架构师”公号,订阅“数据库架构师”公号,一起学习数据库技术)本篇为Redis性能问题诊断系列的第二篇,本文主要从应用发起的典型命令使用上进行讲解,由于Redis为单线程服务架构,对于一些命令如果使用不当会极大的影响Redis的性能表现,这里也会对不合理的使用方式给出优化解决方案。 一、Redis慢日志功能分析Redis访问变慢,其中有个最基础的方法就是先去看Redis是否有慢日志【就像MySQL的慢SQL一样】。Redis提供了一个简单的慢命令统计记录功能,它会记录有哪些命令在执行时耗时较长。Redis慢日志功能由两个核心参数控制:slowlog-log-slower-than 1000#慢日志命令执行阈值,这里指超过1ms就会被记录【单位为微秒】slowlog-max-len 4096#保留慢日志命令的个数,类似一个先进先出的队列,超过4096个最早的就会被清理Redis的这个慢日志功能比较粗糙简单,有个严重的不足:没有持久化记录能力。由于Redis的慢日志记录都在内存中,不像MySQL会持久化到文件里,那么如果慢日志产生较快,即使设置的slowlog-max-len比较大也会很快被填满,诊断问题时也就不能统计到那个时间段产生的所有慢命令详情。为了避免产生的慢日志被清理,目前一个折中的解决方案是写一个收集程序周期性的将新增慢命令查出并记录到MySQL或者本地文件中,以备事后分析。但是这个频率一般都是分钟级,Redis处理的吞吐能力又太大,在慢命令较多的情况下往往也不能全部记录下来。配置好慢日志相关阈值后,可以执行以下命令查询最近的慢日志记录了:127.0.0.1:6379> SLOWLOG get 51) 1) (integer) 42343   2) (integer) 1653659194 #慢日志产生的时间戳   3) (integer) 73536      #慢日志执行的耗时   4) 1) "KEYS"            #慢日志命令详情      2) "permission::userMenuList:*"   5) "192.168.1.11:20504"  #慢日志命令发起来源IP【4.0及以后版本支持】   6) ""2) 1) (integer) 42342   2) (integer) 1653659194   3) (integer) 73650   4) 1) "KEYS"      2) "userPermission:*"   5) "192.168.1.10:20362"   6) ""3) 1) (integer) 42341   2) (integer) 1653659193   3) (integer) 81505   4) 1) "KEYS"      2) "userRole:*"   5) "192.168.1.13:19926"   6) ""二、几种典型导致Redis变慢的不合理使用方式1.使用keys命令进行正则匹配Keys的正则匹配是阻塞式的、全量扫描过滤,这对于单线程服务的Redis来说是致命的,仅仅几十万个Key的匹配查询在高并发访问下就有可能将Redis打崩溃!这其实就像MySQL的无索引查询大表数据,全表扫描状态下几个并发查询就可能会将数据库堵死。redis> SLOWLOG get 51) 1) (integer) 42343   2) (integer) 1653659194   3) (integer) 73536   4) 1) "KEYS"      2) "Testper::userList:*"   5) "192.168.1.10:20504"   6) ""2) 1) (integer) 42342   2) (integer) 1653659194   3) (integer) 73650   4) 1) "KEYS"      2) "TestuserPermission:*"   5) "192.168.1.11:20362"      6) ""3) 1) (integer) 42341   2) (integer) 1653659193   3) (integer) 81505   4) 1) "KEYS"      2) "TestuserRole:*"   5) "192.168.1.12:19926"   6) ""上述示例中使用Keys来模糊查询某些Key,每次的执行都在70ms以上,严重影响了正常的Redis响应时长和吞吐。针对这种问题的一个解决方案是使用scan代替keys。这是一个查询迭代命令,用于迭代当前数据库中的缓存数据。它是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为Scan命令的游标参数, 以此来延续之前的迭代过程。具体的命令语法这里不再详述。2.大量使用了复杂度较高的命令(1)应用中高频使用了 O(N) 及以上复杂度的命令,例如:SUNION、SORT、ZUNIONSTORE、ZINTERSTORE 聚合类命令。SORT命令的时间复杂度:O(N+M*log(M)), N 为要排序的列表或集合内的元素数量, M 为要返回的元素数量。这种导致Redis请求变慢的原因是,Redis 在操作数据排序时,时间复杂度过高,要花费更多的 CPU计算资源。(2)使用 O(N) 复杂度的命令,但 N 的值非常大,比如hgetall、smembers、lrange、zrange等命令。这种变慢的原因在于,Redis 一次需要返回给客户端的数据过多,需要花费更多时间在数据组装和网络传输中。对于hgetall、smembers这种命令,需要警惕项目刚上线之初hash、set或者list存储的成员个数较少,但是随着业务发展成员数量极有可能会膨胀的非常大,如果仍然采用上述命令不加控制,会极大拖累整个Redis服务的响应时间。针对这两种情况还都可以从资源使用率层面来分析,如果应用程序访问 Redis 的QPS不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。因为Redis 是单线程处理请求的,如果你经常使用以上复杂度较高的命令,那么当 Redis 处理程序请求时,一旦前面某个命令发生耗时较长,就会导致后面的请求发生阻塞排队,对于应用程序来说,响应延迟也会变长。3.存储使用了bigkey在分析慢日志发现很多请求并不是复杂度高的命令,都是一些del、set、hset等的低复杂度命令,那么就要评估是否写入了大key。在往Redis写入数据时,需要为新数据分配内存块,相对应的,当删除数据时,Redis也会释放对应的内存空间。如果一个 key 写入Redis的值非常大,那么在分配内存时就会相对比较耗时。同样的当删除这个 key 时,释放内存也会比较耗时,这种被称为bigKey。当然这个描述仍然比较宽泛,因为Redis中的数据库结构类型比较多,更完善的一些说法可以这么定义:将含有较大数据或含有大量成员、列表数的Key定义为bigkey。我们一般要求研发使用Redis时,对于String类型Value大小不要超过1KB。大Key带来的问题比较多,主要有下面几种情况:

  • 由于大Key的内存分配及释放开销变大,直接影响就是导致应用访问Redis的响应变慢;
  • 删除时会造成较长时间的阻塞并有可能造成集群主备节点切换【4.0之前的版本有这个问题】;
  • 内存占用过多甚至达到maxmemory配置,会造成新写入阻塞或一些不应该被提前删除的Key被逐出,甚至导致OOM发生;
  • 并发读请求因为Key过大会可能打满服务器带宽,如果单机多实例部署则同时会影响到该服务器上的其它服务【假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量】;
  • 运维麻烦,比如RedisCluster的数据跨节点均衡,因为均衡迁移原理是通过migrate命令来完成的,这个命令实际是通过dump + restore + del三个命令组合成原子命令完成,如果是bigkey,可能会使迁移失败,而且较慢的migrate也会阻塞Redis正常请求;
  • 分片集群RedisCluster中的出现严重的数据倾斜,导致某个节点的内存使用过大;
那么对于已经写入的数据,如何分析找出里面的bigkey进行优化呢?可以通过Redis官方客户端redis-cli的bigkeys参数来定位大Key分布。shell> redis-cli -h 127.0.0.1 -p 18708 -a xxxx --bigkeys -i 0.01[00.00%] Biggest string found so far 'urlcount:www.guprocessorSuccessMid' with 1 bytes[00.01%] Biggest string found so far 'TestDomain:www:config:scheduler' with 3847 bytes[00.03%] Biggest string found so far 'TestDomain:www:config:scheduler' with 211306 bytes[00.88%] Biggest set    found so far 'specialTestJobSet:www' with 20 members[01.69%] Biggest list   found so far 'TestDomain:www:urlList' with 9762 items[07.13%] Biggest list   found so far 'TestDomain:bx:urlList' with 457676 items[07.39%] Biggest set    found so far 'specialTestJobSet:www' with 100 members[13.99%] Biggest string found so far 'TestDomain:wwwe:config:scheduler' with 540731 bytes[18.74%] Biggest set    found so far 'TestJobSet' with 300 members[58.09%] Biggest string found so far 'TestDomain:wwwrt:config:scheduler' with 739024 bytes[64.19%] Biggest string found so far 'TestDomain:bx:config:scheduler' with 1335468 bytes -------- summary -------Sampled 62522 keys in the keyspace!Total key length in bytes is 2471471 (avg len 39.53)Biggest list found 'TestDomain:bx:urlList' has 457676 itemsBiggest string found 'TestDomain:bx:config:scheduler' has 1335468 bytesBiggest set found 'TestJobSet' has 300 members 208 lists with 2408539 items (00.33% of keys, avg size 11579.51)0 hashs with 0 fields (00.00% of keys, avg size 0.00)62283 strings with 32642667 bytes (99.62% of keys, avg size 524.10)0 streams with 0 entries (00.00% of keys, avg size 0.00)31 sets with 1354 members (00.05% of keys, avg size 43.68)0 zsets with 0 members (00.00% of keys, avg size 0.00)从输出结果我们可以看到,每种数据类型所占用的最大长度或含有最多成员的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小及成员数量。其实,使用这个命令的原理就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、HLEN、LLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、集合类型(Hash、List、Set、ZSet)的成员个数.注意,使用该--bigkeys进行大key的统计时要注意:

  • 对于集合类型的Hash、List、Set、ZSet仅仅统计的是包含的成员个数,个数多并代表占用的内存大,仅仅是个参考;
  • 对于高并发访问的集群,使用该命令会造成QPS增加,带来额外的性能开销,建议在业务低峰或者从节点进行扫描。
那针对 bigkey 导致延迟的问题,有什么好的解决方案呢?1)对大Key进行拆分如将一个含有数万成员的HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。特别是在RedisCluster架构下中,大Key的拆分对各节点间的内存平衡能够起到显著作用。2)优化使用删除Key的命令。Redis自4.0起提供了UNLINK命令,该命令可以替换DEL,能够以非阻塞的方式放到后台线程中缓慢逐步的清理大Key所占用的内存块,从而减轻了对Redis的影响;Redis 6.0 以上版本,建议开启 lazy-free 机制(配置参数:lazyfree-lazy-user-del = yes,6.2版本之后默认开启了),这样在 DEL删除大Key时,释放内存的动作也是在后台线程中执行的;3)尽量不写入大Key首先评估使用其他的存储形式,比如文档性数据库 MongoDB等;如果还无法避免使用BigKey,可以将大Key进行压缩后存储,并尽量根据业务精简Value的内容;建议单个Key的大小不要超过1K;4.不合理使用批处理命令网上有不少关于批量处理的一些优化,使用mget、mset代替多次的get、set等,减少网络IO开销以此提高redis的处理效率,特别是对于一些php短连接效果尤其明显。但是对于这些批量处理命令原生的mget、mset,非原生命令如pipeline,一定要注意控制单次批量操作的元素个数,否则会阻塞其它请求命令!建议控制在500以内。针对该种场景的优化方案:<ul>降低使用 O(N) 以上复杂度的命令,对于数据的计算聚合操作等可以适当的放在应用程序侧处理;
使用O(N) 复杂度的命令时,保证 N 尽量的小(推荐 N
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

尚未崩坏

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

标签云

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