Redis异常排查实战:从问题定位到性能提升,助你成为技术领域的佼佼者!

2023-10-16 20:49:07 浏览数 (1)

解决实际问题是学习知识的催化剂

大家好,前段时间,笔者所负责的一个模块出现了访问redis耗时较长的问题,在这个问题排查的过程中,对redis的问题思路和压测、调优进行了一些系统的学习和沉淀,在这里分享给大家

第一个重点,服务排障的基本方法

在岁月静好的一天,正当笔者准备下班工作的时候,突然,告警出现了!

嗯,又是一到下班就会告警!

仔细一看,原来是数据整体处理时间的慢了

既然慢了,就看看具体哪个链路慢了

看来是A模块的B阶段的处理耗时突然慢了

赶紧确认反向查询哪里出了问题,因为B阶段不是A模块的第一个阶段,所以基本排除是模块间的网络通信、带宽等问题

那这里有两个思路:

1.排查这个A模块本身的问题

2.排查数据量的问题

首先排查A模块本身的问题,这里的经验是

横向看基础指标,纵向看代码变更

1.1 先看A模块服务的基础资源数据

内存正常

CPU正常

1.2 再看所在Node节点的负载情况

Node负载正常,而且一般Node有问题,不会单服务出现问题

1.3 再看磁盘占用情况

存储节点一切正常

看起来,CPU、内存、网络IO、磁盘IO几个大的指标没有明显的变化

1.4 那是不是最近发布出现的问题呢?

经对项目成员对齐,A模块近期也没有进行发布

再排查下数据量的问题

2.1 那再看看有没有是不是数据量出现问题?

嗯,上报量确实增加了5倍,既然这样:

扩容、限流、服务降级三板斧搞上

问题解决了!

舒服!下班!

然后,在一个寂寞无人的深夜里,笔者突然惊醒!

问题当时虽然解决了,但其实并没有真的确认根本原因,数据量也许只是一个引子,这里会不会有性能优化的点呢?

在降本增效的大背景,性能优化在服务端更是一个重要的目标

于是,笔者翻身起床,又进一步的研究了一下

既然,问题出在A模块的B阶段

那首先进行细化,到底哪个方法出现了这么就耗时

排查耗时这里也有两个思路

1.业务打点进行排查

2.性能分析工具排查

一般来说,先用业务打点确认大概范围,然后通过性能分析工具精确确认问题点

之所以这样是因为有两个原因,

一是业务代码一般是造成问题的主要原因,在业务代码进行有效打点,可以更快的确认问题范围、变更范围、责任范围,从而可以有明确的责任人去跟进

二是一般来说性能分析工具排查都会定位到一些组件函数、系统函数,所以可能有很多个调用者、先用耗时打点的方式确认范围,可能更小的范围确认调用者,也就是疑似点,这样组合起来会比较

1.先打点确定业务范围

1.1 首先是修改了一下指标上报的代码,在监控面板上查看

1.2 然后是在模块的日志中进行耗时采集和输出:

结果都基本上定位到函数是redis的计数自增逻辑

至于为什么采用两种方法进行确认,是因为,实际业务会比较复杂,很多函数并不是线性调用,获取正确且精确耗时并不容易,需要多种方案去确认

2再用性能分析工具去定位精确函数

这里主要用的pprof工具,详细使用案例可参考后台填坑记——Golang的OOM的问题排查(二)

可以看到,紫色部分就是redis相关命令的调用耗时,基本上占了总耗时的大头

Double check!

事实上,这里笔者只是拿了一个函数作为例子,在这个场景下,基本上所有的redis相关的命令都慢到秒级,也就是说,Redis可用性出现了问题。

你看,我就说这个班没有白加

我们把问题从“数据量突增”转换到“Redis可用性”上来

嗯,这里我擅长!

毕竟,我们都喜欢把问题规约到以前解决的问题中(并没有)

第二个重点,Redis服务排障的基本方法

既然是Redis的问题,我们就看看到底如何排查Redis服务

其实,这里有很多的总结和文章,笔者这里主要是结合一个实际的问题,对思路进行再次整理

首先,我们的问题是:Redis服务请求的回包慢

这里的思路是:Redis服务本身问题——Redis数据存储问题——请求Redis的问题

一般来说业务出现问题的可能性>服务本身出现问题的可能性

单由于,业务模块没有太多变动,所以这次先查服务本身

1.Redis服务本身问题——Redis所在节点网路延迟问题确认

按照理论上来讲,应该首选确认是Redis服务本身的问题,还是节点网络的问题

这里引用kevine一篇文章的一段话

  • 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况 网络和通信导致的固有延迟: 客户端使用TCP/IP连接或Unix域连接连接到Redis,在1 Gbit/s网络下的延迟约为200 us,而Unix域Socket的延迟甚至可低至30 us,这实际上取决于网络和系统硬件;在网络通信的基础之上,操作系统还会增加了一些额外的延迟(如线程调度、CPU缓存、NUMA等);并且在虚拟环境中,系统引起的延迟比在物理机上也要高得多 结果就是,即使 Redis 在亚微秒的时间级别上能处理大多数命令,网络和系统相关的延迟仍然是不可避免的

可以看到,事实上,即使到物理硬件层,网络的延迟还是有的但不大,但加上Redis所在机器上的带宽限制和网桥性能等问题,这个问题可能会到达不可忽略的地步

事实上,在这个案例中,基本排除是节点网络的问题,

一是,当数据量下降的时候,redis的回包耗时减少。

二是,Redis服务是集群内服务,通过监控发现,内网带宽并没有突破限制

2.Redis服务本身问题——Redis自身服务网路延迟问题确认

对于单实例:这里有两个比较经典的命令

2.1 redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60

即在 Redis server 上测试实例的响应延迟情况

可以看到,还是响应还是挺快的

不过,这个是一个瞬时速度,需要现场抓,所以在复现问题上来说,不是那么的好用,所以可以稍微调整下命令

2.2 redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1

查看一段时间内 Redis 的最小、最大、平均访问延迟:

可以看到,也没啥问题

2.3 吞吐量(使用info stats)

具体含义如下:

代码语言:javascript复制
# 从Rdis上一次启动以来总计处理的命令数
total_commands_processed:2255
# 当前Redis实例的OPS,redis内部较实时的每秒执行的命令数
instantaneous_ops_per_sec:12
# 网络总入量
total_net_input_bytes:34312
# 网络总出量
total_net_output_bytes:78215
# 每秒输入量,单位是kb/s
instantaneous_input_kbps:1.20
# 每秒输出量,单位是kb/s
instantaneous_output_kbps:2.62

其实看到这里,相信很多同学会发现,这个吞吐量要是有时序图好了,嗯,事实上,这也就是为啥很多服务要配置Prometheus的原因:

(图来源于网络)

对于多实例:还要考虑主从同步的问题

主要关注:master_link_down_since_seconds、master_last_io_seconds_ago、master_link_status等指标

使用info Replication命令

不过一般用上了主从同步这一套,基本上业务就会比较重了,运维同学也会在早期建立起监控

回到问题,这里服务没有用主从同步的方式,所以,这里的疑似点排除

3. Redis服务本身问题——CPU、Memory、磁盘IO

其实从前面排障可知:内存、CPU、IO、磁盘应该是最基本的指标,这里之所以先查网络IO,是因为IO的疑点最大,其他的基本上可以通过 info memory;info CPU;info Persistence命令来查看,这里有一个详细的表格供大家参考,回到问题,查看的信息如下:

可以看到CPU比较高,快到了90%

这里补充一个知识点:

cpu 这里,除了上面的内容外:

还有一个场景绑定固定cpu核心的设置,在redis6.0上有,有兴趣的同学可以搜一下

memory这里,主要关注三个指标:

used_memory_rss_human:表示目前的内存实际占用——表示当前的内存情况

used_memory_peak_human:表示内存峰值占用——表示曾经的内存情况(主要是用来抓不到现场的时候查问题用的)

mem_fragmentation_ratio:这里引用kevine一篇文章的一段话

内存碎片率( mem_fragmentation_ratio )指标给出了操作系统( used_memory_rss )使用的内存与 Redis( used_memory )分配的内存的比率 mem_fragmentation_ratio = used_memory_rss / used_memory 操作系统负责为每个进程分配物理内存,而操作系统中的虚拟内存管理器保管着由内存分配器分配的实际内存映射 那么如果我们的 Redis 实例的内存使用量为1 GB,内存分配器将首先尝试找到一个连续的内存段来存储数据;如果找不到连续的段,则分配器必须将进程的数据分成多个段,从而导致内存开销增加,具体的相关解释可参考这篇文章:Redis内存碎片的产生与清理 内存碎片率大于1表示正在发生碎片,内存碎片率超过1.5表示碎片过多,Redis 实例消耗了其实际申请的物理内存的150%的内存;另一方面,如果内存碎片率低于1,则表示Redis需要的内存多于系统上的可用内存,这会导致 swap 操作。内存交换到磁盘将导致延迟显著增加 理想情况下,操作系统将在物理内存中分配一个连续的段,Redis 的内存碎片率等于1或略大于1

这里其实隐含了一个知识点

作为内存型数据库,磁盘也是一个关键点:这里包含了两个方面(1.持久化 2.内存交换)

持久化是一个比较容易忽略的问题,但其实在集群模式下,持久化可能也会从侧面发现问题,这里可以关注如下几个点:

查询的信息有这个几个:

还有一个比较特殊:latest_fork_usec,这个基本上是跟宿主机的关系比较大,如果耗时较久,一般会出现在ARM等机器上

具体参考

内存交换这里其实也是一个关键点:

这里主要关注的点是:maxmemory、maxmemory-policy、evicted_keys

一是最大内存限制maxmemory,如果不设这个值,可能导致内存超过了系统可用内存,然后就开始swap,最终可能导致OOM

二是内存驱逐策略maxmemory-policy,如果设了maxmemory这个值,还需要让系统知道,内存按照什么样策略来释放

这里补充个知识点Reids4.0之后可以将驱逐策略放在后台操作,需要这样设置

代码语言:javascript复制
lazyfree-lazy-eviction yes

三是驱逐数:evicted_keys,这个可以通过info stats查看,即采用驱逐策略真正剔除的数据数目

四是内存碎片率,在上面的引用已经给出了,内存碎片率低的情况下可能导致swqp

你看,这里其实是内存和磁盘IO的联动点

回到问题

从上面的截图可以看到,除了CPU外,基本指标是正常的(maxmemory虽然没设,但内存远没到限制)

那么再来查查Redis数据存储的问题

1. Redis数据存储的问题——key的总数

命令为info keyspace,主要是redis实例包含的键个数。

这里单实例建议控制在1kw内;单实例键个数过大,可能导致过期键的回收不及时。 之所以先查这个是因为,这里很可能是一个容易被人忽略的点,可能每个业务的量不大,但最后一个业务成为压死骆驼的最后一根稻草,所以有问题先排查这里

这里可以看到,总key的数目是没有超过限制的,问题点不在这

2. Redis数据存储的问题——Bigkey、内存大页

所谓大key,就是耗时久的key,具体定义,嗯,这里是bigkey的危害:

Redis 阻塞 :因为 Redis 单线程特性,如果操作某个 Bigkey 耗时比较久,则后面的请求会被阻塞。 内存空间不均匀 :比如在 Redis cluster 或者 codis 中,会造成节点的内存使用不均匀。 过期时可能阻塞 :如果 Bigkey 设置了过期时间,当过期后,这个 key 会被删除,假如没有使用 Redis 4.0 的过期异步删除,就会存在阻塞 Redis 的可能性,并且慢查询中查不到(因为这个删除是内部循环事件)。 导致倾斜 :某个实例上正好保存了 bigkey。bigkey 的 value 值很大(String 类型),或者是 bigkey 保存了大量集合元素(集合类型),会导致这个实例的数据量增加,内存资源消耗也相应增加。实例的处理压力就会增大,速度变慢,甚至还可能会引起这个实例的内存资源耗尽,从而崩溃。

总之就是查询和删除容易造成堵塞,所以要专门看一下,当然这里还有一个关联的知识点:

内存大页:

内存页是用户应用程序向操作系统申请内存的单位,常规的内存页大小是 4KB,而Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存

由于系统采取的COW(写时复制)的方案、如果频繁写请求操作的是一个 bigkey,那主进程在拷贝这个 bigkey 内存块时,涉及到的内存大页会更多,时间也会更久,从而延迟比较久

具体命令:redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

可以看到,没有bigkey,内存大页也没开启,问题点也不在这里

3. Redis数据存储的问题——key集中过期

除了当数据库用,基本上redis里面的key都会设置过期时间,这时有可能存在集中过期导致负载过高的问题

这里补一个知识点:过期策略和驱逐策略的区别

看不清的话,就点上面的链接

回到主线,结论就是redis的定时过期策略虽然会有时间上限限制,但依然在大量过期的情况下出现延时高的情况

不过,这里与这个问题应该关系不大,因为如果是过期问题,不会仅出现在数据量大的情况发生,应该是“周期性出现”,所以问题点也不在这里

既然不是数据本身的问题,那再看看是不是访问的问题

1. 请求Redis的问题——客户端连接数、阻塞客户端的数

这里可能会奇怪,为什么查这个数据,一般业务服务链接redis的请求不是通过客户端,嗯,就是因为问题很少可能出现在这里,所以先查这里

连接数430个、阻塞数0个,没有超过限制,所谓问题不在这里

阻塞的经典函数包括: BLPOP, BRPOP, BRPOPLPUSH

2. 请求Redis的问题——慢命令

即查看请求的命令耗时多久

如下图:

第一个命令是指保留慢命令的条数:128

第二个命令是慢命令的标准 1000毫秒

第三个命令是查看慢命令的top 2和top 3

第一个值是id

第二个值是执行的时间点

第三个值是执行时间

第四个值是执行的命令

这里对几个慢命令的时间进行了查询,发现与redis回包慢的时间不相符,而且并没有太慢,故问题也不在这里

当然这里如果有问题,可以参考

3. 请求Redis的问题——缓存未命中

这里主要是看info stats的两个值:

不过这里并不是这个问题的重点,通过梳理业务逻辑得知,并没有未命中就去持久化数据库再去查询的逻辑

4. 请求Redis的问题——Hotkey

反过来,会不会是访问了某个点太多次了,在redis4.0.3之后,可以查hotkey的情况

当然,要先把内存逐出策略设置为allkeys-lfu或者volatile-lfu,否则会返回错误:

具体可以参考

这里有个小细节,笔者负责的模块是redis4.0.0,刚好没有hotkey监控,然后笔者尝试升级了redis到5.0.0

依然也没有,最后发现还需要升级业务服务的redis的组件库(pakeage)

还好,笔者的负责的服务自己构建了一个热度统计

嗯,看起来hotkey的问题确实存在,好,我们继续

经过以上三个方面的排查:

我们发现:CPU高、hotkey明显

这里隐含了一个点:与CPU相对的是OPS并没有很高

也就是说,虽然Redis很忙,但似乎并没有很高的服务产出——对,这句话用在工作上有时也挺合适

0 人点赞