go 1.21 버전, gin 을 이용해 파일 업로드 관련 핸들을 작성해 테스트를 하던 도중, 팟에서 용량이 없어서 죽는 현상을 발견했다.
원인을 쭉 찾아보니 gin context parameter bind 과정 중, multipartForm data 요청이 왔다면, 파일에 대해서 아래의 코드를 수행하게 되는데, 임시파일로 먼저 저장하고, 파일핸들을 io.ReadCloseSeeker 로 반환해준다
if n > maxFileMemoryBytes {
if file == nil {
file, err = os.CreateTemp(r.tempDir, "multipart-")
if err != nil {
return nil, err
}
}
numDiskFiles++
if _, err := file.Write(b.Bytes()); err != nil {
return nil, err
}
if copyBuf == nil {
copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses
}
// os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it.
type writerOnly struct{ io.Writer }
remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf)
if err != nil {
return nil, err
}
fh.tmpfile = file.Name()
fh.Size = int64(b.Len()) + remainingSize
fh.tmpoff = fileOff
fileOff += fh.Size
if !combineFiles {
if err := file.Close(); err != nil {
return nil, err
}
file = nil
}
} else {
fh.content = b.Bytes()
fh.Size = int64(len(fh.content))
maxFileMemoryBytes -= n
maxMemoryBytes -= n
}
form.File[name] = append(form.File[name], fh)
}
// src/mime/multipart/formdata.go
당연한 생각으로 사용한 임시파일에 대해서 자동으로 삭제를 할 것 같은데, 디버깅해봐도 아래 이슈에서 해결했다고 하지만, 리퀘스트가 끝났을때 자동으로 파일을 삭제하는 부분이 호출되지 않았다.
https://github.com/golang/go/issues/20253
그래서 해결책으로는 직접 임시파일 삭제부분을 defer RemoveAll 로 잊지않고 항상 삭제해주는 방법이 있거나
form := c.MultipartForm()
defer form.RemoveAll()
files := form.File["file"]
for _, file := range files{
defer file.Close()
....
file.Read()
....
}
두번째 방법으로는 업로드시 임시파일 용량을 서버에서 차지하고 있을 이유는 없다면, 위 원래 구현된 multiparForm 파싱코드를 참고해 파일을 저장하지않도록 작성해 처리해야할 것 같다
_, param, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
...
multipartReader := multipart.NewReader(ctx.Request.Body, param["boundary"])
...
for{
part, err := multipartReader.NextPart()
...
if part.FileName() != ""{
return part // part 는 io.ReadCloser로 처리가능하고, 업로드한 파일의 내용
}
}
댓글