这Bug只能通过压测发现

news2024/11/24 8:04:45

大家好,我是洋子。之前发布过一篇有关于在性能测试当中发现Bug的文章《因为一个Bug,差点损失了100w》 这篇文章当时还登上了CSDN全站综合热榜TOP1,最近工作在做性能测试时,又发现了几个比较有意思得Bug,本期分享其中的一个,涉及Redis并发场景下的应用

有意思的是,这个Bug因为没有代码语法错误,并发量少的情况下,下游的监控也不会出现报警,所以光靠功能测试是没有办法发现,只能通过压测(性能测试)或者下游的监控报警才能发现

我们先来看一段Go语言实现的代码,这段代码的意思就是先获取(Get) Redis Key 的值,这个值只有true 或者 false 两种情况 ,如果是true 则直接返回,不执行后续代码逻辑,如果是 false 则 先设置(Set) Redis Key 的值为true,再执行后续代码逻辑

	var flag = false

	// 获取锁
	redisCache := redis.NewCache()
	if err := redisCache.Get(SetUserSizeRedisKey, &flag); err != nil {
		ctx.WarningF("request redis to get _user_size_flag fail, err: %s", err)
	}
	// flag = true说明已经有实例在请求了, 直接返回
	// 否则 设置redis锁
	if flag {
		ctx.Notice("request_user_size_flag is true, return")
		return
	} else {
		ctx.Notice("request_user_size_flag is false, request im to flush room user size")
		if err := redisCache.Set(SetUserSizeRedisKey, true, ScriptMaxRunTime); err != nil {
			ctx.WarningF("set request_user_size_flag to redis fail, err: %s", err)
		}
	}
   // ... 后续具体的业务代码逻辑,可忽略

然而单纯利用从Redis 当中获取一个Bool值,以此来充当互斥锁,这种实现方案,在同一时刻只有一个用户请求能满足需求,但是在并发场景会出现无法锁住的情况,如下图,在初始条件下,即Redis Key 还从来没有被Set时(Key不存在时),当3个用户同时从Redis 读取到的值均为False ,就有3个用户同时去Set Redis Key,并且走到后续的代码逻辑

所以并发场景下,“锁”失效了
在这里插入图片描述
"锁"失效了有什么影响,继续给出完整代码逻辑,这段代码其实是定时任务的一部分,在执行期间,会请求下游服务获得相关数据

在并发场景下,“锁”失效了会导致下游的服务压力上涨,假设下游只能抗50QPS,现在QPS 已经到5000了,严重情况下还会出现IO打满,CPU和内存打满,服务宕机等风险

package main

import "time"

var (
	CrontabTime      = 20  // 每20s执行一次脚本
	ScriptMaxRunTime = 150 // 脚本最长运行时间150s
)

func SetUserSize(ctx *gin.Context) {

	var flag = false

	// 获取锁
	redisCache := redis.NewCache()
	if err := redisCache.Get(SetUserSizeRedisKey, &flag); err != nil {
		ctx.WarningF("request redis to get _user_size_flag fail, err: %s", err)
	}
	// flag = true说明已经有实例在请求了, 直接返回
	// 否则 设置redis锁
	if flag {
		ctx.Notice("request_user_size_flag is true, return")
		return
	} else {
		ctx.Notice("request_user_size_flag is false, request im to flush room user size")
		if err := redisCache.Set(SetUserSizeRedisKey, true, ScriptMaxRunTime); err != nil {
			ctx.WarningF("set request_user_size_flag to redis fail, err: %s", err)
		}
	}
	// 以下是定时任务的具体逻辑
	ctx.NoticeF("run time start:%d", util.GetMilliSecond())

	// 执行定时任务前,前置获取相关必要信息
	info, err := GetInfo()
	if err != nil {
		ctx.WarningF("request  info fail, error: %s", err)
		// 获取信息失败,提前释放锁
		_ = redisCache.Del(SetUserSizeRedisKey)
		return
	}

	ctx.NoticeF("run time start req im:%d", util.GetMilliSecond())

	// 定时任务具体逻辑 
	for _, people := range info {
	
		//请求下游
        // ...
	}
	ctx.NoticeF("run time end:%d", util.GetMilliSecond())
}

如何解决请求下游数量超限这个问题呢,有两种解法:第一种是在请求下游前,增加判断当前QPS。第二种是使用Redis 分布式锁setnx

限定QPS

先看第一种方案是在请求下游前,判断是否超过最大的QPS,如何获取QPS呢,QPS是在做性能测试时,我们常用的性能指标,指每秒的查询数量,用来衡量系统每秒处理的请求数量

那么要获取QPS,自然要获得当前的秒数,如果是在同一秒请求,我们用当前秒数作为Redis Key ,值初始为0,同一秒内每有一次请求,就把Redis 的值加1,这样就拿到了QPS(见代码当中的GetLimitRequest方法)

now := util.GetSecond()
nowMilli := util.GetMilliSecond() 
res := GetLimitRequest(ctx, now) 
		
// GetLimitRequest 获取当前qps
func GetLimitRequest(now int64) int {
	key := fmt.Sprintf("limit_key_request_service_%v", now)
	redisCache := redis.NewCache(ctx)
	res, _ := redisCache.Incr(key)
	if res > 0 {
		go redisCache.Expire(key, 60)
	}
	return res
}

那QPS超出限额了怎么办,得计算到下1秒还有多少时间,要精确计算的话,我们只能可以获取比秒更小的单位-毫秒进行计算,分别获取下1s的时间(毫秒为单位),以及当前这1s的时间(同样毫秒为单位),两者相减,这样就知道到下1秒还差多少毫秒(对应下面代码的变量gap),让系统sleep gap对应毫秒数,这样就可以使得请求的维持在最大的QPS范围内

完整的代码片段如下

package main

import "time"

var (
	CrontabTime      = 20  // 每20s执行一次脚本
	ScriptMaxRunTime = 150 // 脚本最长运行时间150s
)

func SetUserSize() {
	var flag = false

	// 获取锁
	redisCache := redis.NewCache()
	if err := redisCache.Get(SetUserSizeRedisKey, &flag); err != nil {
		ctx.WarningF("request redis to get _user_size_flag fail, err: %s", err)
	}
	// flag = true说明已经有实例在请求了, 直接返回
	// 否则 设置redis锁
	if flag {
		ctx.Notice("request_user_size_flag is true, return")
		return
	} else {
		ctx.Notice("request_user_size_flag is false, request im to flush room user size")
		if err := redisCache.Set(SetUserSizeRedisKey, true, ScriptMaxRunTime); err != nil {
			ctx.WarningF("set request_user_size_flag to redis fail, err: %s", err)
		}
	}
	ctx.NoticeF("run time start:%d", util.GetMilliSecond())

	// 执行定时任务前,前置获取相关必要信息
	info, err := GetInfo()
	if err != nil {
		ctx.WarningF("request  info fail, error: %s", err)
		// 获取信息失败,提前释放锁
		_ = redisCache.Del(SetUserSizeRedisKey)
		return
	}

	ctx.NoticeF("run time start req im:%d", util.GetMilliSecond())

	// 定时任务具体逻辑,可忽略
	for _, people := range info {

		now := util.GetSecond()
		nowMilli := util.GetMilliSecond() 
		res := GetLimitRequest(ctx, now)
		if res > MaxQps {
			gap := (now+1)*1000 - nowMilli
			if gap > 0 {
				time.Sleep(time.Duration(gap) * time.Millisecond) // 在sleep 期间 不再请求下游
			}
		} else if res == 0 { //异常
			time.Sleep(time.Duration(40) * time.Millisecond)
		}

	}
	
	// 请求下游具体代码逻辑,可忽略
	// ...
	// ...

	// 定时任务执行完毕,主动释放锁
	_ = redisCache.Del(SetUserSizeRedisKey)
	ctx.NoticeF("run time end:%d", util.GetMilliSecond())

}

/ GetLimitRequest 获取当前qps
func GetLimitRequest(now int64) int {
	key := fmt.Sprintf("limit_key_request_service_%v", now)
	redisCache := redis.NewCache(ctx)
	res, _ := redisCache.Incr(key)
	if res > 0 {
		go redisCache.Expire(key, 60)
	}
	return res
}

使用Redis分布式锁

setnx是Redis的一个命令,它代表"Set if Not eXists"。这个命令尝试在Redis中设置一个键值对,但仅当指定的键不存在时才会成功。如果键已经存在,setnx操作将失败

我们可以使用setnx命令来创建Redis分布式锁,分布式锁是一种机制,用于确保在分布式系统中的多个节点或线程不会同时访问或修改共享资源,以避免竞态条件(race conditions)

分布式锁的主要目的是确保在分布式系统中,只有一个客户端(或线程)能够成功获得锁,以执行关键任务,而其他客户端必须等待

setnx的使用方式是,客户端通常会使用setnx命令尝试创建一个带有唯一标识的锁,然后在锁上设置一个过期时间,以防止锁被永久占用。当客户端不再需要锁时,可以使用del命令来释放锁

对于上面的并发问题,我们还可以使用SetNX来解决

func SetUserSize(ctx *gin.Context) {

	ExpireTime:= int64(3)
	client := redis.NewCache(ctx)
	key := fmt.Sprintf("set_locker_%s", "param_ex")
	res, err := client.SetNX(ctx, key, time.Now().Unix(), ExpireTime) //创建redis 分布式锁,ExpireTime过期时间为3秒
	if err != nil {
		ctx.WarningF("SetQuestionStatus get lock fail, err: %v", err)
		errno.ErrRet(ctx, errno.ErrCallCacheFail)
		return
	}
	if res != true {
		errno.ErrRet(ctx, errno.ErrSetInfo)
		return
	}
	defer client.Del(ctx, key) //执行完删除Redis分布式锁,让其他线程能正常获取锁,避免永久等待

	//... 执行后续逻辑
}

用一张图片再来对比一下两种实现方案的区别,使用Redis分布式锁能帮助解决高并发下互斥任务的问题,但需要注意设置过期时间,避免永久锁住资源
在这里插入图片描述
下一期我会继续分享压测中发现的性能问题以及排查、调优实战解决方案,欢迎星标【测试开发Guide】公众号,及时获取最新推文

《测试开发面试宝典》已发布,现在订阅免费加入CSDN测试社区(内含测开面试录音,简历案例库,学习资源等多种重磅福利)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1123576.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Office技巧(持续更新)

1. Word 1.1 标题设置为多级列表 选住一级标题,之后进行“定义新的多级列表” 1.2 图片和表的题注自动排序 正常插入题注后就可以了。如果一级标题是 “汉字序号”,那么需要对题注进行修改: 从原来的 图 { STYLEREF 1 \s }-{ SEQ 图 \* A…

Android之Monkey源码分析(第十三篇:触摸事件流程分析)

前言 前面讲了一些monkey作者的设计思想(有的我没写完,还没发布,惨),这篇来点实际的,monkey程序是如何发起一个触摸事件的呢? 本例子中假设使用的命令是: adb shell monkey -p com.…

公众号留言功能怎么使用?如何开启?

为什么公众号没有留言功能?根据要求,自2018年2月12日起,新申请的微信公众号默认无留言功能。有些人听过一个说法:公众号粉丝累计到一定程度或者原创文章数量累计到一定程度就可以开通留言功能。其实这个方法是2018年之前才可以&am…

每日一题 2316. 统计无向图中无法互相到达点对数(中等,图连通分量)

题目很简单,只要求出每个连通分量有多少个节点即可首先通过建立一个字典来表示每个节点的邻接关系遍历每个节点,并通过邻接关系标记在当前连通分量内的所有的点,这样就可以知道一个连通分量内有多少个点在这里我陷入了一个误区,导…

【Linux】32条指令带你玩转 Linux !

目录 1,whoami 2,who 3,pwd 4,ls 1,ls 2,ls -l 3,ls -a 4,ls -al 5,ls -d 6,ls -ld 5,clear 6,cd 1,cd 2&…

看得懂的——数据库中的“除”操作

通过一个例子来解释数据库中的“除”操作 R➗S其实就是判断关系R中X各个值的象集Y是否包含关系S中属性Y的所有值 求解步骤 第一步 找出关系R和关系S中相同的属性,即Y属性。在关系S中对Y做投影(即将Y列取出);所得结果如下&#x…

掌握测评补单技术对Shopee、Lazada店铺有什么好处?

虾皮(Shopee)、lazada作为东南亚地区最大的电商平台之一,吸引了众多卖家加入其平台,竞争激烈。在如此庞大的市场中,如何优化你的shopee、lazada店铺商品再结合自养号测评,提高曝光率和销售能力成为关键。本文将分享一些有效的方法…

解决dirsearch扫描工具pkg_resources模块警告问题

一、pkg_resources模块问题 ┌──(kali㉿kali)-[~/桌面/XXX/dirsearch-master] └─$ python dirsearch.py -h /home/kali/XX/XXXX/dirsearch-master/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io…

VMware Workstation安装ESXi和vCenter(8.0)

一、环境准备 虚拟机:VMware Workstation 17 Pro ESXi:ESXi-8.0U2-22380479-standard vCenter:VMware-VCSA-all-8.0.2-22385739.iso 主要是内存设置,因为vCenter需要14Gb内存,所以这个至少16Gb。 硬盘需要2块&…

【漏洞复现】蓝凌EIS智慧协同平台saveImg接口存在任意文件上传

漏洞描述 蓝凌智慧协同平台满足组织企业在知识、协同及项目管理系统中建设等需求。该平台在saveImg接口处存在任意文件上传,攻击者可通过该漏洞上传Weshell控制服务器。 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,…

65%更小的APK和70%更少的内存:如何优化我的Android App的内存

65%更小的APK和70%更少的内存:如何优化我的Android App的内存 (Note: This is a translation of the provided title) 为什么应用程序内存很重要? 使用最少的内存的高效应用程序可以提升性能,节省设备资源并延长电池寿命。它们提供流畅的用…

学生台灯选什么光源好?适合学生的护眼台灯推荐

现在的台灯可以说是孩子必不可少的一个学习灯具了,几乎每家每户都会备着一台。不过台的好坏也有区别,相对而言,以前所用的白炽灯、日光灯、节能灯等等传统台灯已经是不适合孩子使用的了,目前而言最好的是LED灯。下面小编为大家推荐…

【消费战略】解读100个食品品牌|意面突起,“空刻”的品类心智占位!

空刻意面,一个开创意大利面速食化的新消费品牌,凭借着核心大单品意大利面,在过去短短的几年中,获得不俗的市场成绩和品牌影响力,占领了空刻意面的消费心智: 2019年,AIRMETER氢刻意面上线天猫旗舰…

急需一个可以短视频配音的手机App~

很多小伙伴视频拍摄得很好,但不想用自己的声音做解说旁白,怎么办?这时候就急需一个可以配音的手机App!今天给大家推荐一款一款专注于文字转语音的智能语音合成工具——悦音配音。声音堪比真人发声的配音服务平台,里面拥…

在 Windows 10/11 上恢复已删除文件的 9 种简单方法

本教程讨论永久丢失数据的原因以及在 Windows 10/11 上恢复已删除文件的不同方法: 数据是提供给系统的任何形式的信息。它可以是从密码到记事本文件的任何内容。数据是当今世界的关键要素,因为它使我们的生活变得轻松。 我们每天都变得越来越依赖数据&…

如何禁止别人调试自己的前端代码?

在前端开发中,无法完全禁止别人对自己的前端代码进行调试,因为前端代码在客户端执行,而客户端环境是可被用户访问和控制的。然而,可以采取一些措施来增加代码的安全性和复杂度,使得调试和代码分析变得更加困难。以下是…

橡胶软管在气密测试时如何保持管口的形状不变

在汽车整机或者零部件中,我们经常能看到各种各样的橡胶软管,主要负责导通各种冷却液、油等,是汽车正常工作不可分割的一部分。为了确保软管在使用前具有良好的密封性能,出厂前的气密性测试可以检测软管是否存在漏气或渗漏的问题&a…

新零售系统主要功能有哪些?新零售系统开发公司推荐

新零售系统是一套全面的数字化解决方案,旨在帮助实体零售店提升运营效率、优化用户体验并实现持续增长。以下是新零售系统的主要功能: l 用户画像:系统通过收集和分析顾客的行为、偏好、购买历史等数据,构建出完整的用户画像。这…

智加科技与东风柳汽达成深度合作 自动驾驶重卡计划2024年初量产交付

(2023年10月19日,苏州)全球领先的重卡自动驾驶技术公司智加科技与东风柳汽宣布,双方共同开发的自动驾驶重卡H7计划2024年初实现量产交付。未来,双方将携手推出安全可靠、高性价比、性能卓越的自动驾驶重卡产品&#xf…

Java编译多个目录下的文件

编译单个目录下的Java文件 javac -d <放置输出类文件的目录> <源文件的目录>/*.java 例如&#xff0c;在src/com/thb目录下有两个java文件&#xff1a; 执行编译命令javac -d D:\temp\outputdata src/com/thb/*.java可以编译这个目录下的两个java文件&#xff…