在岁月静好的一天,正当笔者准备下班工作的时候,突然,告警出现了!
嗯,又是一到下班就会告警!
看来是A模块的B阶段的处理耗时突然慢了
赶紧确认反向查询哪里出了问题,因为B阶段不是A模块的第一个阶段,所以基本排除是模块间的网络通信、带宽等问题
那这里有两个思路:
首先排查A模块本身的问题,这里的经验是
横向看基础指标,纵向看代码变更
1.1先看A模块服务的基础资源数据
内存正常
CPU正常
1.2再看所在Node节点的负载情况
Node负载正常,而且一般Node有问题,不会单服务出现问题
1.3再看磁盘占用情况
存储节点一切正常
看起来,CPU、内存、网络IO、磁盘IO几个大的指标没有明显的变化
1.4那是不是最近发布出现的问题呢?
经对项目成员对齐,A模块近期也没有进行发布
再排查下数据量的问题
2.1那再看看有没有是不是数据量出现问题?
嗯,上报量确实增加了5倍,既然这样:
问题解决了!
舒服!下班!
然后,在一个寂寞无人的深夜里,笔者突然惊醒!
问题当时虽然解决了,但其实并没有真的确认根本原因,数据量也许只是一个引子,这里会不会有性能优化的点呢?
在降本增效的大背景,性能优化在服务端更是一个重要的目标
于是,笔者翻身起床,又进一步的研究了一下
既然,问题出在A模块的B阶段
那首先进行细化,到底哪个方法出现了这么就耗时
排查耗时这里也有两个思路
一般来说,先用业务打点确认大概范围,然后通过性能分析工具精确确认问题点
之所以这样是因为有两个原因,
一是业务代码一般是造成问题的主要原因,在业务代码进行有效打点,可以更快的确认问题范围、变更范围、责任范围,从而可以有明确的责任人去跟进
二是一般来说性能分析工具排查都会定位到一些组件函数、系统函数,所以可能有很多个调用者、先用耗时打点的方式确认范围,可能更小的范围确认调用者,也就是疑似点,这样组合起来会比较
1.先打点确定业务范围
1.1首先是修改了一下指标上报的代码,在监控面板上查看
1.2然后是在模块的日志中进行耗时采集和输出:
结果都基本上定位到函数是redis的计数自增逻辑
至于为什么采用两种方法进行确认,是因为,实际业务会比较复杂,很多函数并不是线性调用,获取正确且精确耗时并不容易,需要多种方案去确认
2再用性能分析工具去定位精确函数
Doublecheck!
你看,我就说这个班没有白加
我们把问题从“数据量突增”转换到“Redis可用性”上来
嗯,这里我擅长!
毕竟,我们都喜欢把问题规约到以前解决的问题中(并没有)
既然是Redis的问题,我们就看看到底如何排查Redis服务
首先,我们的问题是:Redis服务请求的回包慢
这里的思路是:Redis服务本身问题——Redis数据存储问题——请求Redis的问题
一般来说业务出现问题的可能性>服务本身出现问题的可能性
单由于,业务模块没有太多变动,所以这次先查服务本身
1.Redis服务本身问题——Redis所在节点网路延迟问题确认
按照理论上来讲,应该首选确认是Redis服务本身的问题,还是节点网络的问题
可以看到,事实上,即使到物理硬件层,网络的延迟还是有的但不大,但加上Redis所在机器上的带宽限制和网桥性能等问题,这个问题可能会到达不可忽略的地步
事实上,在这个案例中,基本排除是节点网络的问题,
一是,当数据量下降的时候,redis的回包耗时减少。
二是,Redis服务是集群内服务,通过监控发现,内网带宽并没有突破限制
2.Redis服务本身问题——Redis自身服务网路延迟问题确认
对于单实例:这里有两个比较经典的命令
2.1redis-cli-h127.0.0.1-p6379--intrinsic-latency60
即在Redisserver上测试实例的响应延迟情况
可以看到,还是响应还是挺快的
不过,这个是一个瞬时速度,需要现场抓,所以在复现问题上来说,不是那么的好用,所以可以稍微调整下命令
2.2redis-cli-h127.0.0.1-p6379--latency-history-i1
可以看到,也没啥问题
2.3吞吐量(使用infostats)
具体含义如下:
#从Rdis上一次启动以来总计处理的命令数total_commands_processed:2255#当前Redis实例的OPS,redis内部较实时的每秒执行的命令数instantaneous_ops_per_sec:12#网络总入量total_net_input_bytes:34312#网络总出量total_net_output_bytes:78215#每秒输入量,单位是kb/sinstantaneous_input_kbps:1.20#每秒输出量,单位是kb/sinstantaneous_output_kbps:2.62其实看到这里,相信很多同学会发现,这个吞吐量要是有时序图好了,嗯,事实上,这也就是为啥很多服务要配置Prometheus的原因:
对于多实例:还要考虑主从同步的问题
使用infoReplication命令
不过一般用上了主从同步这一套,基本上业务就会比较重了,运维同学也会在早期建立起监控
回到问题,这里服务没有用主从同步的方式,所以,这里的疑似点排除
3.Redis服务本身问题——CPU、Memory、磁盘IO
可以看到CPU比较高,快到了90%
这里补充一个知识点:
cpu这里,除了上面的内容外:
used_memory_rss_human:表示目前的内存实际占用——表示当前的内存情况
used_memory_peak_human:表示内存峰值占用——表示曾经的内存情况(主要是用来抓不到现场的时候查问题用的)
这里其实隐含了一个知识点:
作为内存型数据库,磁盘也是一个关键点:这里包含了两个方面(1.持久化2.内存交换)
查询的信息有这个几个:
还有一个比较特殊:latest_fork_usec,这个基本上是跟宿主机的关系比较大,如果耗时较久,一般会出现在ARM等机器上
内存交换这里其实也是一个关键点:
一是最大内存限制maxmemory,如果不设这个值,可能导致内存超过了系统可用内存,然后就开始swap,最终可能导致OOM
二是内存驱逐策略maxmemory-policy,如果设了maxmemory这个值,还需要让系统知道,内存按照什么样策略来释放
这里补充个知识点Reids4.0之后可以将驱逐策略放在后台操作,需要这样设置
lazyfree-lazy-evictionyes三是驱逐数:evicted_keys,这个可以通过infostats查看,即采用驱逐策略真正剔除的数据数目
四是内存碎片率,在上面的引用已经给出了,内存碎片率低的情况下可能导致swqp
你看,这里其实是内存和磁盘IO的联动点
回到问题
从上面的截图可以看到,除了CPU外,基本指标是正常的(maxmemory虽然没设,但内存远没到限制)
那么再来查查Redis数据存储的问题
1.Redis数据存储的问题——key的总数
命令为infokeyspace,主要是redis实例包含的键个数。
这里可以看到,总key的数目是没有超过限制的,问题点不在这
2.Redis数据存储的问题——Bigkey、内存大页
总之就是查询和删除容易造成堵塞,所以要专门看一下,当然这里还有一个关联的知识点:
内存大页:
内存页是用户应用程序向操作系统申请内存的单位,常规的内存页大小是4KB,而Linux内核从2.6.38开始,支持了内存大页机制,该机制允许应用程序以2MB大小为单位,向操作系统申请内存
具体命令:redis-cli-h127.0.0.1-p6379--bigkeys-i0.01
可以看到,没有bigkey,内存大页也没开启,问题点也不在这里
3.Redis数据存储的问题——key集中过期
看不清的话,就点上面的链接
不过,这里与这个问题应该关系不大,因为如果是过期问题,不会仅出现在数据量大的情况发生,应该是“周期性出现”,所以问题点也不在这里
既然不是数据本身的问题,那再看看是不是访问的问题
1.请求Redis的问题——客户端连接数、阻塞客户端的数
这里可能会奇怪,为什么查这个数据,一般业务服务链接redis的请求不是通过客户端,嗯,就是因为问题很少可能出现在这里,所以先查这里
连接数430个、阻塞数0个,没有超过限制,所谓问题不在这里
阻塞的经典函数包括:BLPOP,BRPOP,BRPOPLPUSH
2.请求Redis的问题——慢命令
即查看请求的命令耗时多久
如下图:
第一个命令是指保留慢命令的条数:128
第二个命令是慢命令的标准1000毫秒
第三个命令是查看慢命令的top2和top3
第一个值是id
第四个值是执行的命令
3.请求Redis的问题——缓存未命中
这里主要是看infostats的两个值:
不过这里并不是这个问题的重点,通过梳理业务逻辑得知,并没有未命中就去持久化数据库再去查询的逻辑
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很忙,但似乎并没有很高的服务产出——对,这句话用在工作上有时也挺合适
一般遇到这个情况,我们就要仔细分析下,到底Redis的CPU消耗在哪里了
这里就要仔细分析下Redis的服务架构了
Rrdis6.0之前,主要采用的单线程模式,为什么采取单线程
官方的回答是:
核心意思是:CPU并不是制约Redis性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以Redis核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式
使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
回到正题
对于文件事件来说结构图是下图
单线程处理逻辑是:
图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络I/O和命令处理都是单线程,所以看起来这里会有挺多的网络IO在里面,那这里会不会有坑呢?
指出:
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。
提高网络IO性能,典型的实现比如使用DPDK来替代内核网络栈的方式
使用多线程充分利用多核,典型的实现比如Memcached。
协议栈优化的这种方式跟Redis关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:
可以充分利用服务器CPU资源,目前主线程只能利用一个核
多线程任务可以分摊Redis同步IO读写负荷
咦,这里是不是就是问题的关键呢?
笔者负责的Redis的版本是5.0
且QPS这里也比较符合
而Redis6.0以前的单线程网络IO模型的处理具体的负载在哪里呢?虽然Redis利用epoll机制实现IO多路复用(即使用epoll监听各类事件,通过事件回调函数进行事件处理),但I/O这一步骤是无法避免且始终由单线程串行处理的,且涉及用户态/内核态的切换,即:
那如何确认这里的猜测呢?
按照先Demo确认——再模拟线上服务的方式
既然是网络IO多,那怎么减少网络IO呢?
两个方案:一个是pipeline、一个是Lua脚本
这里
这里就会比较麻烦,因为一到线上,数据链路就会比较长
我们先简化一下服务链路
从质量同学的角度上看:压测讲究的事是全链路模拟,不然就无法做上线前的最后的质量守护
两个角度都没有问题,事实上,笔者认为,只有从两个角度看问题,才能更好的平衡质量和效率
1.首先将kaf装在了kafka的一个服务中
2.然后抓取线上的数据30条(之所以抓的这么少,是因为线上就是hotkey的问题,这里模拟的就是大量相似数据访问的场景)
3.使用kaf给到10000次的生产数据
catxxx-test|kafproducekv__0.111-n10000-bqapm-tencent-cp-kafka:9092
结果,压力不够
foriin{0..8};docatxxx-test|kafproducekv__0.111-n10000-p${i}-bqapm-tencent-cp-kafka:9092;done
结果,压力还是不够
那就将kaf装在每一个kafka的服务中
结果:
有效果,但Redis的CPU还是不是很高
看下监控
啊,服务Y的CPU一直也不高,看来,服务Y并没有感受到压力
给服务Y加协程,CPU上来了
但内存提前满了
加内存
这里其实也是一个资源调优的经验,事实上,一个服务的内存和CPU的比例关系需要结合线上的负载来看,而且要定期看,不然也会导致资源浪费
好的,CPU和内存都调整了,搞起!
终于,Redis的CPU上去了!
看下OPS
嗯,OPS低、CPU高,问题复现了!
果然:hotkey其实就会导致cpu变高,而这时,因为大量的cpu都在数据切换和存储上,导致其他的请求比较慢
现象对上了!
那如何解决呢?
也是三板斧:
1.如果是多实例的话,就是经典的读写分离!
2.如果是单实例的话,就使用pipeline批量写入
3.如果pipeline无法满足业务的话,就在业务服务只加一层缓存