最近排查一个因文件(微信安装包130M+)上传导致内存飙升,被杀死进程的问题,因为这个动作是在大文件上传的时候触发的,而且必现,所以当时第一反应是因文件流忘记关闭导致内存泄露等问题引起,于是把文件上传功能开头到位排查了一遍,没有发现异常,并且导致进程被杀死只发生在测试环境,本地和开发均正常,本地16G内存,开发8G内存,测试2G。
使用pprof排查内存分配使用情况:
go tool pprof -alloc_space http://localhost:9003/debug/pprof/heap?seconds=20
可以看到函数分配和占用内存最多的函数是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))
再次内存监控:
部署测试,运行,ok。
问题解决。