记一次内存飙升排查

 

最近排查一个因文件(微信安装包130M+)上传导致内存飙升,被杀死进程的问题,因为这个动作是在大文件上传的时候触发的,而且必现,所以当时第一反应是因文件流忘记关闭导致内存泄露等问题引起,于是把文件上传功能开头到位排查了一遍,没有发现异常,并且导致进程被杀死只发生在测试环境,本地和开发均正常,本地16G内存,开发8G内存,测试2G。

使用pprof排查内存分配使用情况:

go tool pprof -alloc_space http://localhost:9003/debug/pprof/heap?seconds=20

记一次内存飙升排查_第1张图片

可以看到函数分配和占用内存最多的函数是LogMiddleware,

使用list查看函数内部具体分配情况:

(pprof) list LogMiddleware
Total: 2GB
ROUTINE ======================== epshealth-airobot-common/biz/middleware.LogMiddleware in D:\WorkSpace\epshealth-airobot-common\biz\middleware\log_middleware.go
  127.41MB     1.74GB (flat, cum) 87.03% of Total
         .          .     34:func LogMiddleware(c *gin.Context) {
         .          .     35:   c.Header("Content-Type", "application/json; charset=utf-8")
         .          .     36:   blw := bodyLogWriter{bodyBuf: bytes.NewBufferString(""), ResponseWriter: c.Writer}
         .          .     37:   c.Writer = blw
         .          .     38:   var params string
         .   257.08MB     39:   if data, err := c.GetRawData(); err != nil {
         .          .     40:           log.Error(err.Error())
         .          .     41:   } else {
         .          .     42:           ln := len(data)
         .          .     43:           if ln > maxPrintWordNum {
         .          .     44:                   ln = maxPrintWordNum
         .          .     45:           }
  127.41MB   634.03MB     46:           params = strings.ReplaceAll(strings.ReplaceAll(string(data), "\n", ""), " ", "")
         .          .     47:           c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
         .          .     48:   }
         .          .     49:   record := Record{
         .          .     50:           StartTime: time.Now(),
         .          .     51:           IP:        c.ClientIP(),
         .          .     52:           Agent:     c.Request.UserAgent(),
         .          .     53:           URI:       c.Request.RequestURI,
         .          .     54:           Method:    c.Request.Method,
         .          .     55:           Params:    params,
         .          .     56:   }
         .   131.52MB     57:   c.Next() //执行
         .          .     58:
         .          .     59:   //当前用户
         .          .     60:   record.CurrentUserID = common.CurrentUserID(c)
         .          .     61:   record.CurrentUserName = common.CurrentUserName(c)
         .          .     62:   record.CurrentUserType = common.AudienceType(c)
         .          .     63:
         .          .     64:   level := logrus.DebugLevel
         .          .     65:   if c.Request.Header.Get("Sec-WebSocket-Protocol") == "" && c.Writer.Header().Get("Content-Transfer-Encoding") != "binary" { //过滤ws和binary返回
         .          .     66:           result := common.Result{}
         .          .     67:           if err := json.Unmarshal(blw.bodyBuf.Bytes(), &result); err != nil {
         .          .     68:                   log.WithError(err).Errorf("解析response失败,url:%s, %s", record.URI, string(blw.bodyBuf.Bytes()))
         .          .     69:           } else {
         .          .     70:                   if result.Code >= 500 {
         .          .     71:                           level = logrus.ErrorLevel
         .          .     72:                   } else if result.Code > 100 {
         .          .     73:                           level = logrus.WarnLevel
         .          .     74:                   }
         .          .     75:                   if level != logrus.DebugLevel || config.Data.Project.LogResponse || record.Method != "GET" {
         .          .     76:                           record.Response = strings.Trim(blw.bodyBuf.String(), "\n")
         .          .     77:                   }
         .          .     78:           }
         .          .     79:   }
         .          .     80:   record.Code = c.Writer.Status()                   //响应状态
         .          .     81:   record.Latency = time.Now().Sub(record.StartTime) //运行时间
         .          .     82:   errStrings := c.Errors.Errors()
         .          .     83:   if errStrings != nil {
         .          .     84:           record.Error = strings.Join(errStrings, ",")
         .          .     85:   }
         .          .     86:
         .   758.44MB     87:   log.WithField("record", record).Log(level, record.Method+" "+record.URI)
         .          .     88:
         .          .     89:   c.Set("Record", record)
         .          .     90:}
         .          .     91:
         .          .     92:type Record struct {
(pprof)

梳理这段逻辑可以看到中间件里面引用了一个Gin框架内部context对象的一个GetRawData:

// GetRawData return stream data.
func (c *Context) GetRawData() ([]byte, error) {
	return ioutil.ReadAll(c.Request.Body)
}

这里返回了请求体里面的所有字节。

那么那个data的变量就分配了257.08MB,在经过转string操作时,param参数被分配了634.03MB……整个函数一共分配了Total: 2GB。

而测试服务器分配的可用内存恰恰是2G,于是就崩了。

优化截取:

		ln := len(data)
		if ln > maxPrintWordNum {
			ln = maxPrintWordNum
		}
		params = strings.ReplaceAll(strings.ReplaceAll(string(data[:ln]), "\n", ""), " ", "")
		c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))

再次内存监控:

记一次内存飙升排查_第2张图片

部署测试,运行,ok。

问题解决。

你可能感兴趣的:(Go)