暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis阻塞的6大类场景分析与总结

IT那活儿 2023-02-23
1048

点击上方“IT那活儿”公众号,关注后了解更多内容,不管IT什么活儿,干就完了!!!

Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。当redis用于高并发场景时,这个线程就变的非常重要。如果出现阻塞,即使是时间很短,对应用也可能产生很大的影响。本文将一一分析导致阻塞的原因。


不合理地使用API或数据结构

一般情况redis执行命令速度非常快,但也存在一些例外,比如对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢。这个问题就是典型的不合理使用API和数据结构。对于高并发的场景我们应该避免在大对象上执行算法复杂度超过O(n)的命令。
1.1 发现慢查询
Slowlog get (n)指令检查
127.0.0.1:6379> SLOWLOG GET 3
1- 1) (integer) 14 # 唯一性(unique)的日志标识符
   2) (integer) 1522808219 # 被记录命令的执行时间点,以 UNIX 时间戳格式表示
   3) (integer) 16 # 查询执行时间,以微秒为单位
   4) 1) "keys"                                           # 执行的命令,以数组的形式排列
      2) "*"                                             # 这里完整的命令是 "keys *"
2- 1) (integer) 13
   2) (integer) 1522808215
   3) (integer) 7
   4) 1) "set"
      2) "name"
      3) "baicai"
3- 1) (integer) 12
   2) (integer) 1522808198
   3) (integer) 101
   4) 1) "set"
      2) "age"
      3) "25"

1.2 检查大对象
Redis>get bigkeys

[root@ecs-7e58 add-nomal-key]# redis-cli --bigkeys -h 127.0.0.1 -p 6379

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest zset found so far '"zset_32_4769"' with 10 members
[00.00%] Biggest set    found so far '"set_32_1808"' with 10 members
[00.00%] Biggest list   found so far '"list_32_3402"' with 10 items
[00.00%] Biggest string found so far '"string_32_1957"' with 32 bytes
[00.00%] Biggest hash found so far '"hash_32_1481"' with 10 fields

-------- summary -------

Sampled 50000 keys in the keyspace!
Total key length in bytes is 604470 (avg len 12.09)

Biggest list found '"list_32_3402"' has 10 items
Biggest hash found '"hash_32_1481"' has 10 fields
Biggest string found '"string_32_1957"' has 32 bytes
Biggest set found '"set_32_1808"' has 10 members
Biggest zset found '"zset_32_4769"' has 10 members

10000 lists with 100000 items (20.00% of keys, avg size 10.00)
10000 hashs with 100000 fields (20.00% of keys, avg size 10.00)
10000 strings with 320000 bytes (20.00% of keys, avg size 32.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
10000 sets with 100000 members (20.00% of keys, avg size 10.00)
10000 zsets with 100000 members (20.00% of keys, avg size 10.00)


CPU饱和
单线程的redis处理命令时只能使用一个CPU。而CPU饱和是指redis把单核CPU使用率跑到接近100%。使用top命令很容易识别出对应redis进程和CPU使用率。CPU饱和度是非常危险的,将导致redis不能正常响应业务,严重影响系统吞吐量和应用的稳定性。
对于此类情况,首先判断当前redis的并发量是否达到极限,可以使用redis-cli -h   -p –stat 获取当前redis的使用情况,该命令每秒输出一行信息,运行效果如下:
[root@localhost src]# redis-cli -h 127.0.0.1 --stat
------- data ------ --------------------- load -------------------- - child -
keys              mem      clients    blocked         requests                    connections
3789785 3.62G        601 0 8867956733 (+1) 555884
3789785 3.62G        601 0 8867956734 (+63904) 555884
3789785 3.62G        601 0 8867956733 (+62091) 555884
3789785 3.62G        601 0 8867956734 (+66999) 555884
3789785 3.62G        601 0 8867956398 (+65987) 555884
3789785 3.62G        601 0 8867956733 (+69876) 555884

以上输出是一个接近饱和的redis实例的统计信息,它每秒平均处理6万+的请求。对于这种情况,优化很难达到明显效果,这就需要做集群化水平扩展来分摊OPS压力。但是如果只有几百或几千OPS的redis实例CPU饱和度就接近100%,这种现象就不正常,有可能使用了高算法复杂度的命令。
还有一种情况是过度的内存优化,这种情况有些隐蔽,需要我们根据info commandstats统计信息分出命令不合理开销时间,例如下面的耗时统计:
Cmdstat_hset:calls =198757512,usec=27021957243,usec_per_call=135.95
查看这个统计可以发现hset命令算法复杂度只有O(1)位平均耗时却达到了135微秒,显然不合理,正常耗时应该在10微秒以下。

根本原因为实例为了追求低内存消耗,过度放宽ziplist使用条件(修改了hash-max-ziplist-entries和hash-max-ziplist-value配置)。进程内的hash对象平均存储着上万个元素,而针对ziplist的操作算法复杂度在O(n)到O(n^2)之间。虽然用ziplist编码后hash结构内存占用会变小,但是操作变得更慢且更消耗CPU。


持久化阻塞

对于开启了持久化功能的redis节点,需要排查是否有持久化导致的阻塞。持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、hugepage写操作阻塞。
  • fork阻塞
    fork操作发生在RDB和AOF重写时,redis主线程调用fork操作产生共享内存的子进程,由子进程完成持久化文件的重写工作。如果fork操作本身耗时过长,必然后导致主线程的阻塞。

    可以执行info stats命令获取到latest_fork_usec指标,表示redis最近一次fork操作耗时,如果耗时很大,超过1秒,则需要做出优化调整,如避免使用过大的内存实例和规避fork缓慢的操作系统等。

  • aof刷盘阻塞

    当我们开启AOF刷盘功能时,文件刷新的方式一般是采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行fsync操作完成。这种阻塞方式行为主要是硬盘压力引起。可以查看redis日志识别出这种情况。也可以查看info persistence统计中的AOF_DELAYED_FSYNC指标,每次发生fdatasync阻塞主线程时会累加。

  • hugepage写操作阻塞

    子进程在执行重写其间利用linux写时复制技术降低内存的开销,因此只有写操作时redis才复制要修改的内存页。对于开启透明大页操作操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。比如简单的incr命令也会出现在慢查询中。


CPU竞争

4.1 进程竞争
Redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响redis的吞吐量。可以通过top,sar,ps等命令定位CPU消耗的时间点和具体进程。
4.2 绑定cpu
部署redis时为了充分利用多核CPU,通常一台机器部署多个实例。
常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。这个优化技巧正常情况下没有问题。
但是存在例外情况:当redis父进程创建子进程进行rdb/aof重写时,如果做了CPU绑定,会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90%以上,父进程与子进程将产生激烈的CPU竞争,极大影响redis稳定性。

因此对于开启了持久化或参与复制的主节点不建议绑定CPU


内存交换

内存交换(swap)对于redis来说是非常致命的,redis保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把redis使用的部分内存换出硬盘,由于内存与硬盘读写的速度并几个数量级,会导致发生交换后的redis性能急剧下降。

识别redis内存交换的检查方法如下:

  • 查询redis进程号
    redis-cli -p 6379 info server |grep process_id
    或者
    ps -ef|grep redis
  • 根据进程号查询内存交换信息
    cat proc/4476/smaps|grep -i swap

如果交换量都是0kb或者个别的是4KB,则是正常现象,说明redis进程内存没有被交换,预防内存交换的方法有: 

  • 保证机器有充足的物理可用内存。
  • 设置redis最大可用内存,防止极端情况下redis内存不可控增长。
  • 降纸swap优化级。如:echo 10>/proc/sys/vm/swappiness。

网络问题

网络问题经常是redis阻塞的问题点。常见的网络问题主要有:连接拒绝、网络延迟,网卡软中断等。
6.1 拒绝连接
当出现网络闪断或者连接数溢出时,客户端会现在无法连接redis的情况。需要分三种情况
1)网络闪断
一般发生在网络割接或者带宽耗尽的情况,对于网络闪断的识别比较因难,需要向上层运维支持。比如交换机日志等。也可借助wareshake,tcpdump等工具进行抓包分析。
2)Redis连接拒绝
Redis通过maxclients参数控制客户端最大连接数,默认是10000.当redis连接数大于maxclients时会拒绝新的连接进入。Info stats的rejected_connections统计指标记录所有被拒绝连接的数量:
Redis-cli -p 6379 info stats |grep rejected_connections
Rejected_connections:10

Redis使用多路复用IO模型可支撑大量连接,但不代表可以无限连接。客户端访问redis时尽量采用NIO长连接或者连接池的方式。
3)连接溢出
这是指操作系统或者redis客户端在连接时的问题。这个问题一般有两种原因:进程限制、backlog队列溢出。
  • 进程限制
Redis连接模型如下:

客户端能成功连接上Redis服务需要操作系统和redis的限制都通过才可以成功。操作系统一般会对进程使用的资源做限制,其中一项是对进程可打开最大文件数控制,通过ulimit -n 查看,默认是1024。由于linux系统对TCP连接也定义为一个文件句柄,因此对于支撑大量连接的redis需要调整ulimit值。一般建议为65535,防止too many open files错误。

  • Backlog队列溢出
系统对于特定端口的TCP连接使用backlog队列保存。Redis默认的长度为511,通过tcp-backlog参数设置。如果redis在高并发场景防止缓慢连接占用,可适当增大这个设置,但必须大于操作系统允许值才能生效。当redis启动时如果tcp-backlog设置大于操作系统允许值将以系统值为准。
系统的backlog默认值为128,使用echo 511>/proc/sys/net/core/somaxconn命令进行修改。可以通过netstat -s 命令获取因backlog队列溢出造成的连接拒绝统计,如下:
1031 times the listen queue of a socket overflowed
6.2 网络延迟

网络延迟取决于客户端到redis服务器之间的网络环境主要包括它们之间的物理拓扑和带宽占用情况

常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房。

Redis提供了测量机器之间网络延迟的工具,在redis-cli 命令后加入参数进行测试:

  • --latency:持续进行测试,分别统计:最小值,最大值,平均值,采样次数。
  • --latency-history:同latency,但15s统计一次,可通过-i参数设置开始时间。
  • --latency-dist :用统计图形式展示延迟统计,每1秒进行采样一次。
其次要综合机器网卡带宽,交换机带宽,专线带宽等进行综合分析。
6.3 网卡软中断
网卡软中断是指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。
网卡软中断瓶颈一般出现在网络高流量吞吐的场景。Linux在内核2.6.35以后支持receive packet steering(RPS)实现了在软件层面模拟硬件的多队列网卡功能。


END


本文作者:鲁伟锋(上海新炬中北团队)

本文来源:“IT那活儿”公众号

文章转载自IT那活儿,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论