一次接口压测超时问题的调查过程(如何对接口进行压力测试)
mhr18 2024-11-02 11:54 27 浏览 0 评论
1、背景
作者所在项目组为第三方提供身份管控组件,组件中有个高频接口GetToken,主要作用是验证用户登录后颁发的token是否有效。文中涉及到的redis集群都为redis cluster部署方式
2、问题
近期某业务线报了一个bug,说在压测业务线某个接口时并发压到100之后99%报接口超时,业务线查看日志发现是因为GetToken接口超时。
3、接口逻辑
GetToken接口的逻辑并不复杂,是通过go语言开发的微服务,主要流程如下:
1、将收到的token做base64解密得到实际token
2、连接redis
3、根据token拼出key
4、用key到redis中获取value
5、如果拿到value就反序列化成对象返回
6、拿不到value说明token已过期,返回错误
4、调查过程
最开始接到这个bug的第一感觉是一定是业务线哪块配置错了,因为这个GetToken我们组已识别它为高频接口,测试已经压测过,至少可以达到1000qps。
4.1 第一次尝试
因为坚信代码没有问题,所以第一时间去检查了业务线的配置项,发现redis地址给配了两个一样的redis节点地址:
cache=redis://:password@10.41.xx.xx:16379,10.41.xx.xx:16379
问了下ops发现使用的是redis集群,三个ip,每个ip三个端口,相当于9节点。
因为这边使用的第三方组件go-redis作为redis客户端,也是支持redis集群的,所以让ops把全部9个节点都填上,替换之前的配置:
cache=redis://:password@10.41.xx.xx:16379,10.41.xx.xx:16380,10.41.xx.xx:16381,10.41.xx.xx:16379,10.41.xx.xx:16380,10.41.xx.xx:16381,10.41.xx.xx:16379,10.41.xx.xx:16380,10.41.xx.xx:16381
但是问题依旧。
4.2 第二次尝试
既然第一次尝试没有成功,那就从头开始吧。先复现问题。
因为测试休假了,我把她的jmeter测试脚本要过来,方便自己测试,发现并发量可以到60,到70就会有部分接口超时,大于80之后基本就99%超时了。
- 并发40的成功率是100%,qps 13.54
- 并发80的成功率是0.38%,qps 15.31
- 并发60的成功率是100%,qps 13.41
结合redis监控发现,不管并发为10,50还是100,达到redis的流量一直稳定在116左右,流量并没有被打到redis。
本地启动服务,使用apache ab test压一下我们自己的接口,发现能到700多qps
将本地服务的redis配置换成业务线的redis集群,发现并发100时,qps只有13。
此时我的思路已经怀疑到了业务线的k8s网络和redis集群本身的问题上了。
4.3 第三次尝试
我在调查问题的同时,让ops测试一下redis集群是否有问题,结果ops用redis-benchmark测试发现集群没有任何问题,qps轻松上几万
所以再试下网络是否有问题,最简单的方法是直接去组件所在的pod执行下redis-benchmark,发现网络也没有任何问题
一切似乎回到了原点。
4.4 第四次尝试
经过了一天的休整,我有了新的思路,既然想证明是redis集群的问题,那就使用之前压测用过的redis集群试下,结果令人惊喜,换了个集群之后果然qps可以上1000了。虽然还没有找到根本原因,但是我已经可以将问题返给业务线了。业务线得知结果后也在积极调查问题。但是心里却一直放不下,为什么两个集群看起来都没什么问题,一个可以另一个就不行呢?
4.5 第五次尝试
我想到了redis客户端是不是不支持业务线的集群配置方式,查了下代码发现目前使用的是go-redis v6,还是项目3年之前启动时使用的版本,最新的版本已经到v8了,是否是版本不匹配的原因呢?
所以我用两个版本分别写了连接redis集群,并获取key的测试代码,果然不出所料,v6的代码使用业务线的redis集群并发获取key,一次需要3000ms级别,而v8的代码只需要10ms级别,换回以前压测过的redis集群,v6和v8就都一样快了。
最后确认下业务线的redis版本为redis6,之前我们自己压测时使用的是redis5,说明go-redis v6 还可以适配redis5集群,但是不能适配redis6集群。
最后业务线那边将redis集群换成单机模式发现也是可以的。
5、解决方案
最终的解决方案有三种:
1、将redis集群降到redis5
2、将我们引用的go-redis升级到v8
3、单独给我们的组件搭一个单机redis
方案1的缺点是有可能会影响其他使用redis集群的组件
方案2的缺点是需要改我们组件的代码,因为v6到v8有不兼容升级,改代码之后需要回归测试
方案3的缺点是业务方不能对redis进行统一维护
6、刨根问底
只知其然不知其所以然,我们还是没有找到go-redis v6不能兼容redis v6集群的根本原因。
我编写测试代码连接redis v6的集群环境。
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"sync"
"time"
"github.com/go-redis/redis"
)
func main() {
go func() {
http.ListenAndServe("0.0.0.0:18005", nil)
}()
n := 100
c := redis.NewUniversalClient(redisUniversalOptionsV6())
f := func() {
wg := sync.WaitGroup{}
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
start := time.Now()
val := c.Get("3").String()
fmt.Printf("span=%dms,res:%v\n", time.Since(start).Milliseconds(), val)
}()
wg.Done()
}
wg.Wait()
}
for {
f()
time.Sleep(1 * time.Second)
}
}
func redisUniversalOptionsV6() *redis.UniversalOptions {
return &redis.UniversalOptions{
Addrs: []string{"xxx:16381"},
Password: "xxx",
DialTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxRedirects: 8,
PoolSize: 10,
PoolTimeout: 30 * time.Second,
IdleTimeout: time.Minute,
IdleCheckFrequency: 100 * time.Millisecond,
}
}
go mod 里将 go-redis包替换为本地包
replace github.com/go-redis/redis => ../github.com/go-redis/redis
将本地包切换为v6.15.9
cd ../github.com/go-redis/redis git checkout v6.15.9
查看pprof发现没有什么特别异常的地方,只是觉得getConn不应该耗时这么多
所以到代码中查看defaultProcess方法,发现有个方法是通过command命令获取Slot和Node信息
获取command info
这里redis客户端有一个优化,就是将拿到的command info放缓存中,从而不必每次都向服务端发送command命令
这里使用了go-redis自己封装的once.Do,经过日志分析,发现时间大部分消耗在这里
里面实际执行了传入的函数,耗时并不在传入的函数,而是上锁耗时最严重
既然加锁严重耗时,那么一定是测试代码的并发都走到了这里,导致冲突严重
如果正常情况下通过o.done变量的作用下,Once.Do只会执行一次,所以这里一定是出现了异常,能出现异常的地方只能是err不为nil,打印下err发现是这个错误
我使用redis-cli分别连接redis5集群和redis6集群并分别执行command命令,发现确实redis5集群返回的切片是6个元素,而redis6集群返回的切片是7个元素
接下来就好办了,首先找到注册进来的函数即c.fn,是创建cmdsInfoCache对象时传入的
找到调用newCmdsInfoCache的地方
发现传入的是ClusterClient的cmdsInfo方法,它的主要工作是在所有redis实例地址中循环调用command命令,收集command信息
继续追下去发现就是在解析command结果的时候报的错
最终发现slice长度如果不等于6就会报错。
查看最新的代码已经兼容redis6和redis7集群了,说明官方已经修复了这个问题,但是需要使用go-redis v8版本
7、总结
这个问题改起来非常容易,但是调查时会让人一时蒙住,一般情况下并不会遇到类似问题,但是由此总结出来的调查思路很重要。在排查问题时,需要尽可能的发现可能的故障环节,然后依次排除,结合替换和实验两大方法就能够更容易的定位到问题的根本原因。
其实官方在2020年6月就修改了这个bug,我们的项目是在2019年引用的go-redis之后就再没升级过,直到遇到这个问题才发现go-redis的这个bug,由此总结出的另一个经验是,定期升级依赖包还是有必要的,特别是一些常用中间件有了大版本升级,否则极有可能踩到不必要的坑里。
相关推荐
- Java面试题及答案总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Redis、Linux、SpringBoot、Spring、MySQ...
- Java面试题及答案最全总结(2025春招版)
-
大家好,我是Java面试分享最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Spring...
- Java面试题及答案最全总结(2025版持续更新)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 蚂蚁金服面试题(附答案)建议收藏:经典面试题解析
-
前言最近编程讨论群有位小伙伴去蚂蚁金服面试了,以下是面试的真题,跟大家一起来讨论怎么回答。点击上方“捡田螺的小男孩”,选择“设为星标”,干货不断满满1.用到分布式事务嘛?为什么用这种方案,有其他方案...
- 测试工程师面试必问的十道题目!全答上来的直接免试
-
最近参加运维工程师岗位的面试,笔者把自己遇到的和网友分享的一些常见的面试问答收集整理出来了,希望能对自己和对正在准备面试的同学提供一些参考。一、Mongodb熟悉吗,一般部署几台?部署过,没有深入研究...
- 10次面试9次被刷?吃透这500道大厂Java高频面试题后,怒斩offer
-
很多Java工程师的技术不错,但是一面试就头疼,10次面试9次都是被刷,过的那次还是去了家不知名的小公司。问题就在于:面试有技巧,而你不会把自己的能力表达给面试官。应届生:你该如何准备简历,面试项目和...
- java高频面试题整理
-
【高频常见问题】1、事务的特性原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。一致性或可串性:事务的执行使得数据库从一种正确状态转换成另一种正确状态隔离性:在事务正确提交之前,不允许把该...
- 2025 年最全 Java 面试题,京东后端面试面经合集,答案整理
-
最近京东搞了个TGT计划,针对顶尖青年技术天才,直接宣布不设薪资上限。TGT计划面向范围包括2023年10月1日到2026年9月30日毕业的海内外本硕博毕业生。时间范围还...
- idGenerator测评
-
工作中遇到需要生成随机数的需求,看了一个个人开发的基于雪花算法的工具,今天进行了一下测评(测试)。idGenerator项目地址见:https://github.com/yitter/IdGenera...
- 2024年开发者必备:MacBook Pro M1 Max深度体验与高效工作流
-
工作机器我使用的是一台16英寸的MacBookProM1Max。这台电脑的表现堪称惊人!它是我用过的最好的MacBook,短期内我不打算更换它。性能依然出色,即使在执行任务时也几乎听不到风扇的...
- StackOverflow 2022 年度调查报告
-
一个月前,StackOverflow开启了2022年度开发者调查,历时一个半月,在6月22日,StackOverflow正式发布了2022年度开发者调查报告。本次报告StackO...
- 这可能是最全面的SpringDataMongoDB开发笔记
-
MongoDB数据库,在最近使用越来越广泛,在这里和Java的开发者一起分享一下在Java中使用Mongodb的相关笔记。希望大家喜欢。关于MongoDB查询指令,请看我的上一篇文章。SpringD...
- Mac M2 本地部署ragflow
-
修改配置文件Dockerfile文件ARGNEED_MIRROR=1//开启国内镜像代理docker/.envREDIS_PORT=6380//本地redis端口冲突RAGFLOW_IMA...
- 别再傻傻分不清!localhost、127.0.0.1、本机IP,原来大有讲究!
-
调试接口死活连不上?部署服务队友访问不了?八成是localhost、127.0.0.1、本机IP用混了!这三个看似都指向“自己”的东西,差之毫厘谬以千里。搞不清它们,轻则调试抓狂,重则服务裸奔。loc...
- 我把 Mac mini 托管到机房了:一套打败云服务器的终极方案
-
我把我积灰的Macmini托管到机房了,有图有真相。没想到吧?一台在家吃灰的苹果电脑,帮我省了大钱!对,就是控制了自己的服务器,省了租用云服务器的钱,重要数据还全捏在自己手里,这感觉真爽。你可...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis结构 (53)
- redis 订阅 (54)
- redis 登录 (62)
- redis 面试 (58)
- redis问题 (54)
- 阿里 redis (67)
- redis的缓存 (57)
- lua redis (59)
- redis 连接池 (64)