
本标题由AI生成,是标题,不是内容。不得不说,标题起的我很满意
昨天正好是台风天,公司安排居家办公,晚上 8 点钟左右,我正在家里和小朋友玩耍,突然收到一条告警:MySQL CPU 使用率达到 100%
这台风天的也不让人消停啊,打开电脑迅速进入战斗状态

看了下监控,CPU 一直居高不下,直觉上来讲,一般是慢SQL的问题,遂看了下慢SQL分析,查到两个慢SQL。
但是经过分析,发现这两个SQL不是引起问题的原因。为什么呢?因为其中一个是常态化的上线很久的SQL,一分钟才运行一次;另一个是查询一个用户的金币记录,这个表索引非常全,语句也都命中了索引,不能算是慢SQL。
毕竟诡异的是,活跃会话里面很多查询金币记录的SQL,且都是同一个用户的。我第一反应是被刷了,或者用户不耐烦,一直在刷页面。
只好先限流这个SQL,CPU降下来一些,但是也不能一直限流啊,会影响其他正常用户查询。
恰好这时候,有同事说他刚刚在查询大SQL,时间点“勉强”对的上,但是把SQL kill掉之后CPU还是没降下来,非常纳闷。
恢复线上服务要紧,第一时间联系了运维进行升配,升级完成后CPU就降了下来,我观察了10分钟,没有在上升后,就暂时放心下线了。
今天下午,又收到告警CPU超80%啦。
说实话,有点不可思议,昨晚升配直接升了一倍,本来之前正常情况下CPU也很低,升级之后应该是更低的,怎么现在上到80%了?
这次发现,诶,怎么又是昨晚那个熟悉的查询金币记录的SQL,且还是这个用户,真是起猛了。
但是这次,我把监控活跃会话的指标点亮之后,发现活跃会话是跟CPU使用率是强相关的。也就是说:之前认为的CPU高是慢查询SQL引起的,这个结论可能这次是错的,也可能是查询会话太多了。

立马查看MySQL查询请求量和登录对应的服务查询日志,发现这条SQL一直在刷,频率很高,不像是用户自己刷的,倒像是代码死循环了。
review了对应接口的代码,发现确实有问题,特定情况下会陷入死循环,一直查询。
问题代码如下
// 存在死循环风险的钱包记录查询函数
func (l *WalletRecord) Do() (resp WalletRecordRsp, err error) {
pageSize := 10
resp = WalletRecordRsp{}
// 初始化查询参数
nextIndex := l.req.StartIndex
// 主循环:直到获取到足够的记录
for len(resp.Records) < pageSize {
// 如果有下一页索引,则使用它
if resp.NextIndex > 0 {
nextIndex = resp.NextIndex
}
// 查询数据库获取记录列表
var recordList []UserRecord
recordList, resp.NextIndex, err = QueryUserRecord(nextIndex, pageSize)
if err != nil {
return
}
// 遍历并处理每条记录
for _, record := range recordList {
// 过滤某些不需要显示的记录类型
if shouldFilterRecord(record) {
continue
}
// 构造返回结果
item := &WalletRecord{
Type: record.Type,
Amount: amount,
RecordTime: record.RecordTime.Unix(),
}
resp.Records = append(resp.Records, item)
// 如果已经达到页面大小,保存下一个索引并跳出
if len(resp.Records) == pageSize {
resp.NextIndex = record.ID
break
}
}
// 如果返回的记录数少于页面大小,说明已经到最后一页
if len(recordList) < pageSize {
resp.NextIndex = 0
return
}
}
return
}对应的数据库查询函数
// 数据库查询函数
func QueryUserRecord(startIndex uint64, pageSize int) ([]UserRecord, uint64, error) {
var (
recordList []UserRecord
nextIndex uint64
)
// 执行数据库查询
if startIndex == 0 {
// 第一页查询
rows, _ := db.Raw("SELECT * FROM records ORDER BY id DESC LIMIT ?", pageSize).Rows()
defer rows.Close()
for rows.Next() {
data := UserRecord{}
rows.Scan(&data.ID, &data.Type, &data.Amount, &data.RecordTime)
nextIndex = data.ID
recordList = append(recordList, data)
if len(recordList) >= pageSize {
break
}
}
} else {
// 分页查询
rows, _ := db.Raw("SELECT * FROM records WHERE id < ? ORDER BY id DESC LIMIT ?",
startIndex, pageSize).Rows()
defer rows.Close()
for rows.Next() {
data := UserRecord{}
rows.Scan(&data.ID, &data.Type, &data.Amount, &data.RecordTime)
nextIndex = data.ID
recordList = append(recordList, data)
if len(recordList) >= pageSize {
break
}
}
}
return recordList, nextIndex, nil
}大家应该能看出问题,当查询到最后一页,且用户的记录被过滤不满足10条时,会再次查询,然后死循环。
修改起来也简单,判断下nextIndex=0直接返回就好了,因为已经到底了。
MySQL监控上不是所有指标都默认点亮的,这样会漏掉很多信息,我们在排查看监控时,一定要先把指标全部打开,然后慢慢去掉确认没相关性的指标。
还是要积极写单测啊,这种代码,光靠人肉眼CodeReview也是很难看出来的,有单测的话,应该很容易就能发现了。
后面也许考虑接入AI CodeReview来分析这类非常隐晦的bug.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。