今天中午,离谱告诉我投票问卷的的获取投票数据接口基本显示超时(10s),然后我看到后自己用apifox测了一下,有7s、11s甚至14s(复盘发现最高甚至有2min),于是先去看了一下有多少数据量,发现有八百多条,当时是觉得应该是循环太多了,没处理好,而且也因为潜意识觉得只是超时,可能问题不大,多试几次是可以拿到数据的,所以当时认为还不是很急。
没几分钟,青鸟在群里@我说,服务器mysql要爆了(mysql的cpu的占用会时不时蹦上>100%,据说甚至有190%),随时处于被kill的状态,因此情况就紧急起来,因为那个数据库关系精弘全部服务,一旦kill了,所有服务都会停止。。。
因为mysql的cpu占用变化很大,怀疑是某个接口的sql写的有问题,而且当时是投票问卷的发布时间,可以把排查接口限制为获取问卷
、提交答卷
、统一验证
和获取投票数据
。
综上,最后确认是获取投票数据
的问题,应该是循环太多反复向数据库读取数据的问题。
这块码是基本 CV复用之前用管理端获取选项数据的接口,所以较为陌生,重新查阅了一下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| func GetSurveyStatistics(c *gin.Context) {
questionMap := make(map[int]models.Question) for _, question := range questions { questionMap[question.ID] = question }
optionCounts := make(map[int]map[int]int) for _, sheet := range answersheets { for _, answer := range sheet.Answers { options, err := service.GetOptionsByQuestionID(answer.QuestionID) if err != nil { c.Error(&gin.Error{Err: errors.New("获取选项信息失败原因: " + err.Error()), Type: gin.ErrorTypeAny}) utils.JsonErrorResponse(c, code.ServerError) return } question := questionMap[answer.QuestionID] if err != nil { c.Error(&gin.Error{Err: errors.New("获取选项信息失败原因: " + err.Error()), Type: gin.ErrorTypeAny}) utils.JsonErrorResponse(c, code.ServerError) return } if question.QuestionType == 1 || question.QuestionType == 2 { answerOptions := strings.Split(answer.Content, "┋") for _, answerOption := range answerOptions { option, err := service.GetOptionByQIDAndAnswer(answer.QuestionID, answerOption) if err == gorm.ErrRecordNotFound { if optionCounts[question.ID] == nil { optionCounts[question.ID] = make(map[int]int) } optionCounts[question.ID][0]++ continue } else if err != nil { c.Error(&gin.Error{Err: errors.New("获取选项信息失败原因: " + err.Error()), Type: gin.ErrorTypeAny}) utils.JsonErrorResponse(c, code.ServerError) return } if optionCounts[question.ID] == nil { optionCounts[question.ID] = make(map[int]int) } optionCounts[question.ID][option.SerialNum]++ } } for _, option := range options { if optionCounts[question.ID] == nil { optionCounts[question.ID] = make(map[int]int) } if _, exists := optionCounts[question.ID][option.SerialNum]; !exists { optionCounts[question.ID][option.SerialNum] = 0 } }
} }
}
|
再较为仔细地审阅了当时写的代码,首先发现循环多次向数据库获取相同的数据(假设答卷有800份数据,一个问卷一道多选题19个选项,一个请求起码向数据库打了800✖️19=15200个请求),所以一开始就打算先把几个重复拿取的数据缓存进redis中(GetOptionsByQuestionID
和GetOptionByQIDAndAnswer
),再把这个新的包上了生产环境后,mysql的cpu终于下来了(最高也就20%),同时请求响应时间从14秒优化到了1秒内(虽然14秒本身就是不正常的数据)。
后面,肉会表示可以先把数据先存进map,甚至可以不需要用redis,这次明显是我考虑不周的问题(QAQ)。修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| func GetSurveyStatistics(c *gin.Context) { questionMap := make(map[int]models.Question) optionsMap := make(map[int][]models.Option) optionAnswerMap := make(map[int]map[string]models.Option) optionSerialNumMap := make(map[int]map[int]models.Option) for _, question := range questions { questionMap[question.ID] = question optionAnswerMap[question.ID] = make(map[string]models.Option) optionSerialNumMap[question.ID] = make(map[int]models.Option) options, err := service.GetOptionsByQuestionID(question.ID) if err != nil { c.Error(&gin.Error{Err: errors.New("获取选项信息失败原因: " + err.Error()), Type: gin.ErrorTypeAny}) utils.JsonErrorResponse(c, code.ServerError) return } optionsMap[question.ID] = options for _, option := range options { optionAnswerMap[question.ID][option.Content] = option optionSerialNumMap[question.ID][option.SerialNum] = option } } optionCounts := make(map[int]map[int]int) for _, sheet := range answersheets { for _, answer := range sheet.Answers { options := optionsMap[answer.QuestionID] question := questionMap[answer.QuestionID] if _, initialized := optionCounts[question.ID]; !initialized { counts := ensureMap(optionCounts, question.ID) for _, option := range options { counts[option.SerialNum] = 0 } } if question.QuestionType == 1 || question.QuestionType == 2 { answerOptions := strings.Split(answer.Content, "┋") questionOptions := optionAnswerMap[answer.QuestionID] for _, answerOption := range answerOptions { if questionOptions != nil { option, exists := questionOptions[answerOption] if exists { ensureMap(optionCounts, answer.QuestionID)[option.SerialNum]++ continue } } ensureMap(optionCounts, answer.QuestionID)[0]++ } } } } } func ensureMap(m map[int]map[int]int, key int) map[int]int { if m[key] == nil { m[key] = make(map[int]int) } return m[key] }
|
反思一下,这个问题虽说是cv之前代码,但确实造成了比较严重的生产事故,我应该在拿来使用的时候重新审查一遍代码,在问题发现前排查出来(也可能当时觉得这块写的很抽象,不想再细看,觉得能用就行),反正今后写码以此为鉴,避免再出现这种情况。