首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >台风天MySQL CPU两度“爆表”:从升配到揪出死循环代码的24小时追凶记

台风天MySQL CPU两度“爆表”:从升配到揪出死循环代码的24小时追凶记

原创
作者头像
不做虫子
发布2025-09-25 23:02:19
发布2025-09-25 23:02:19
1070
举报

本标题由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了对应接口的代码,发现确实有问题,特定情况下会陷入死循环,一直查询。

问题代码如下

代码语言:javascript
复制
// 存在死循环风险的钱包记录查询函数
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
}

对应的数据库查询函数

代码语言:javascript
复制
// 数据库查询函数
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开始
  • 暂时解决危机
  • 又起风波
  • 翻页引起的死循环
  • 反思
    • 监控层面
    • 代码层面
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档