Golang程序内存占用过高如何优化

Golang程序内存占用过高如何优化 你好。

我正在尝试构建一个小型的HTTP API,用于接收文件并对其进行一些操作。我能够成功接收文件的base64编码,解码并将其保存到磁盘,但出于某些原因,我的程序在之后没有释放内存。

以下是服务器的完整代码:

package main

import (
	"encoding/json"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"encoding/base64"
	"os"
	"io/ioutil"
	"path"
	"os/exec"
	"fmt"
)

const progName =  "dummy"

type Request struct {
	FileBase64 string `json:"base64,omitempty"`
	FileName string `json:"filename,omitempty"`
	Auth    string `json:"auth,omitempty"`
}

type Reply struct {
	Success bool
	Status  string
}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/put", processFile).Methods("POST")
	log.Fatal(http.ListenAndServe("localhost:8000", router))
}

func processFile(w http.ResponseWriter, r *http.Request) {
	var cmd Request
	_ = json.NewDecoder(r.Body).Decode(&cmd)
	log.Printf("request received")

	// decode incoming data
	dec, err := base64.StdEncoding.DecodeString(cmd.FileBase64)
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot decode base64:", err)
		reply := Reply{false, "cannot decode base64"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("request decoded")

	// create temp directory
	// we need it to dump the input file and run AppImage on it.
	tempDir, err := ioutil.TempDir("", progName+"-")
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot create temp directory:", err)
		reply := Reply{false, "cannot create temp directory"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	defer os.RemoveAll(tempDir)
	log.Printf("created temp dir")

	// create output file
	zipFile, err := os.Create(path.Join(tempDir, cmd.FileName))
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot create output file:", err)
		reply := Reply{false, "cannot create output file"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	defer zipFile.Close()
	log.Printf("output file created")

	// write content
	written, err := zipFile.Write(dec)
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot write output file:", err)
		reply := Reply{false, "cannot write output file"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("content written")

	// flush buffer
	err = zipFile.Sync()
	if err != nil {
		log.Println(r.RemoteAddr, "-", "error while flushing buffer:", err)
		reply := Reply{false, "error while flushing buffer"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("buffer flushed")

	reply := Reply{true, fmt.Sprintf("written %d bytes", written)}
	json.NewEncoder(w).Encode(reply)
}

为了向API发送文件,我使用以下命令:

(echo -n '{"filename": "test.zip", "base64": "'; base64 500M.bin; echo '"}') | curl -H "Content-Type: application/json" -d @-  http://127.0.0.1:8000/put

当使用500MB文件进行测试时,服务器组件会占用大量内存,更重要的是,当函数processFile退出时,它不会释放这些内存。

对于我做错了什么,有什么想法吗?

非常感谢。


更多关于Golang程序内存占用过高如何优化的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

谢谢,我已经添加了指令,但我的程序仍然没有释放内存。你知道可能是什么原因吗?

更多关于Golang程序内存占用过高如何优化的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你在 processFile 函数中缺少对 r.Body.Close() 的调用。

实际上,应该使用 defer r.Body.Close() 😊

另外:你没有检查错误。你不应该这样做。请检查所有的错误…(未来的你会感谢你的)

你如何测量内存使用情况?

你强制运行过垃圾回收吗?

即使垃圾回收器收集并清理了已使用的内存,它也只会在特定时间后(如果有的话)归还给操作系统。

我从未因此遇到过问题,但经常听说 Go 运行时不太愿意将内存归还给操作系统……

这是 Go 语言的本质特性

Go 内存分配器会预留一大块虚拟内存区域作为分配用的内存池。这个虚拟内存是特定 Go 进程独有的;这种预留操作不会剥夺其他进程的内存。

要查找分配给 Go 进程的实际内存量,请使用 Unix top 命令并查看 RES(Linux)或 RSIZE(Mac OS X)列。

https://golang.org/doc/faq#Why_does_my_Go_process_use_so_much_virtual_memory

实际上,文档指出您不需要关闭 r.Body。我仍然认为这是一种良好的风格,但如果您忘记了,也不会造成问题

图标

http package - net/http - Go Packages

http 包提供了 HTTP 客户端和服务器的实现。

服务器将关闭请求体。…

问题出在您的代码中,dec 变量存储了整个解码后的文件内容(500MB),这个变量在函数结束时才会被垃圾回收。由于Go的垃圾回收不是立即执行的,内存看起来没有被立即释放。此外,在处理大文件时,一次性将整个文件加载到内存中会导致高内存占用。

以下是优化后的代码,通过流式处理避免将整个文件内容存储在内存中:

package main

import (
	"encoding/json"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"encoding/base64"
	"os"
	"io"
	"path"
	"fmt"
)

const progName = "dummy"

type Request struct {
	FileBase64 string `json:"base64,omitempty"`
	FileName   string `json:"filename,omitempty"`
	Auth       string `json:"auth,omitempty"`
}

type Reply struct {
	Success bool
	Status  string
}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/put", processFile).Methods("POST")
	log.Fatal(http.ListenAndServe("localhost:8000", router))
}

func processFile(w http.ResponseWriter, r *http.Request) {
	var cmd Request
	if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
		log.Println(r.RemoteAddr, "-", "cannot decode JSON:", err)
		reply := Reply{false, "cannot decode JSON"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("request received")

	tempDir, err := os.MkdirTemp("", progName+"-")
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot create temp directory:", err)
		reply := Reply{false, "cannot create temp directory"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	defer os.RemoveAll(tempDir)
	log.Printf("created temp dir")

	zipFilePath := path.Join(tempDir, cmd.FileName)
	zipFile, err := os.Create(zipFilePath)
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot create output file:", err)
		reply := Reply{false, "cannot create output file"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	defer zipFile.Close()
	log.Printf("output file created")

	decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
	written, err := io.Copy(zipFile, decoder)
	if err != nil {
		log.Println(r.RemoteAddr, "-", "cannot write output file:", err)
		reply := Reply{false, "cannot write output file"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("content written")

	if err := zipFile.Sync(); err != nil {
		log.Println(r.RemoteAddr, "-", "error while flushing buffer:", err)
		reply := Reply{false, "error while flushing buffer"}
		json.NewEncoder(w).Encode(reply)
		return
	}
	log.Printf("buffer flushed")

	reply := Reply{true, fmt.Sprintf("written %d bytes", written)}
	json.NewEncoder(w).Encode(reply)
}

主要改进:

  1. 使用 base64.NewDecoder 直接从请求体流式解码base64数据,避免将整个文件内容加载到内存。
  2. 使用 io.Copy 将解码后的数据直接写入文件,减少内存缓冲。
  3. 替换 ioutil.TempDiros.MkdirTemp(因为 ioutil 已弃用)。

这样,内存占用将显著降低,因为数据是流式处理而不是全部加载到内存中。垃圾回收也会更有效地管理内存。

回到顶部