FreeBSD服务中的Golang内存泄漏问题

FreeBSD服务中的Golang内存泄漏问题 使用 GoFiber 运行一个 HTTP 服务。在 Linux 机器的终端中运行此代码时,垃圾回收(GC)工作正常,内存会被释放。但在 FreeBSD 上作为启动服务运行时,内存不会被释放,并且会发生泄漏。

image

import (
	"bytes"
	"fmt"
	"runtime"

	"github.com/valyala/fastjson"
	"github.com/xuri/excelize/v2"
)

func convert_json_to_excel(request_data *fastjson.Value, buffer_pointer *bytes.Buffer) error {

	var err error
	// Remove redundant "data" key passed from body
	json_values := request_data.Get("data")
	if json_values == nil {
		err = fmt.Errorf("submitted json body must contain desired excel data in a key named \"data\": %d", err)
		fmt.Println(err)
		return err
	}

	json_values_array, _ := json_values.Array()

	// Initial no longer needed
	json_values.Del("data")

	// Create a working temporary Excel "file"
	excel_file := excelize.NewFile()
	defer excel_file.Close()

	sheet_index, _ := excel_file.NewSheet("Sheet1")

	excel_file.SetActiveSheet(sheet_index)

	//Embolden the first row
	header_style, _ := excel_file.NewStyle(&excelize.Style{
		Font: &excelize.Font{
			Bold: true,
		},
	})
	header_row_opts := excelize.RowOpts{StyleID: header_style}

	stream_writer, _ := excel_file.NewStreamWriter("Sheet1")

	// Widen all used columns based on headers
	header_row, _ := json_values_array[0].Array()
	stream_writer.SetColWidth(1, len(header_row), 30)

	// Iterate through data and write to file
	for row := 0; row < len(json_values_array); row++ {

		active_row, _ := json_values_array[row].Array()
		//fmt.Printf("%s\n", active_row)

		// Streamwriter is very performant, use of it requires type []interface{}
		interface_row := make([]interface{}, len(active_row))
		for cell := 0; cell < len(active_row); cell++ {
			// Free working memory
			json_values_array[row].Del(fmt.Sprintf("%d", cell))

			interface_row[cell] = active_row[cell].Get().MarshalTo(nil)
		}

		first_cell := fmt.Sprintf("A%d", (row + 1))

		if row == 0 {
			stream_writer.SetRow(first_cell, interface_row, header_row_opts)
		} else {
			stream_writer.SetRow(first_cell, interface_row)
		}
	}

	stream_writer.Flush()

	_, err = excel_file.WriteTo(buffer_pointer)
	if err != nil {
		err = fmt.Errorf("write fail: %d", err)
		fmt.Println(err)
		return err
	}

	// Force GC
	runtime.GC()

	return nil
}

更多关于FreeBSD服务中的Golang内存泄漏问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

哦,这很有趣。我原以为你可能有一些独立的、针对BSD的特定代码来让它作为服务运行,但实际上你构建的是相同的代码,而它在BSD上的行为却不同?这或许是一个在主要代码仓库中提交问题的好候选案例。

更多关于FreeBSD服务中的Golang内存泄漏问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


有趣。我看到你传递了指针;我在想,是不是你的调用代码没有释放对这些指针的引用?比如,也许你在等待输入时阻塞了,但阻塞的代码却持有着对 buffer_pointer *bytes.Buffer 或类似东西的引用,而在 Web 版本中,那个指针位于一个 handlerFunc 中,因此当该函数返回时就被释放了。你的 BSD 服务中的 main() 函数是什么样的?

func main() {
    fmt.Println("hello world")
}

感谢回复,

以下是调用 convert_json_to_excel 的处理函数,此服务中的 main() 函数将仅用于初始化 API 路由等:

func handle_excel_v1(fiber_context *fiber.Ctx) error {
	json_values_root, err := fastjson.ParseBytes(fiber_context.Body())
	if err != nil {
		err = fmt.Errorf("parse data error: %d", err)
		fmt.Println(err)
		return nil
	}

	// Buffer is set for convert_json_to_excel to write bytes to
	var excel_data bytes.Buffer

	err = convert_json_to_excel(json_values_root, &excel_data)
	if err != nil {
		err = fmt.Errorf("conversion error: %d", err)
		fmt.Println(err)
		return nil
	}

	if json_values_root.Exists("file_path") {
		file, _ := os.Create(fmt.Sprintf("%s.%s", string(json_values_root.GetStringBytes("file_path")), "xlsx"))
		defer file.Close()

		file.Write(excel_data.Bytes())
		fiber_context.JSON(fiber.Map{
			"success":   true,
			"file_path": string(json_values_root.GetStringBytes("file_path")),
		})
	} else {
		fiber_context.Response().Header.Set("Content-Disposition", "attachment; filename=response.xlsx")
		fiber_context.Write(excel_data.Bytes())
	}
	return nil
}

在FreeBSD上作为服务运行时出现内存泄漏,通常与Go运行时在特定操作系统下的内存管理行为差异有关。以下是几个关键点和可能的解决方案:

1. FreeBSD内存管理差异

FreeBSD的jemalloc与Linux的glibc malloc行为不同,可能导致内存碎片化问题。Go的GC在FreeBSD上可能不会立即将内存返还给操作系统。

// 添加内存监控和调试
import (
    "runtime/debug"
    "runtime"
)

func monitorMemory() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB", m.Alloc/1024/1024)
    fmt.Printf("\tTotalAlloc = %v MiB", m.TotalAlloc/1024/1024)
    fmt.Printf("\tSys = %v MiB", m.Sys/1024/1024)
    fmt.Printf("\tNumGC = %v\n", m.NumGC)
}

2. 显式内存清理

在FreeBSD上需要更积极地清理资源:

func convert_json_to_excel(request_data *fastjson.Value, buffer_pointer *bytes.Buffer) error {
    // 在函数开始时设置内存清理
    debug.SetGCPercent(10) // 更频繁的GC
    
    defer func() {
        debug.SetGCPercent(100) // 恢复默认值
        runtime.GC()
        runtime.GC() // 双重GC确保清理
    }()
    
    // 原有代码...
    
    // 显式清理大对象
    json_values_array = nil
    excel_file = nil
    
    // 强制内存释放
    debug.FreeOSMemory()
    
    return nil
}

3. 使用对象池减少分配

var bufferPool = sync.Pool{
    New: func() interface{} {
        return bytes.NewBuffer(make([]byte, 0, 1024*1024)) // 1MB初始容量
    },
}

func convert_json_to_excel(request_data *fastjson.Value) (*bytes.Buffer, error) {
    buffer := bufferPool.Get().(*bytes.Buffer)
    buffer.Reset()
    
    defer func() {
        buffer.Reset()
        bufferPool.Put(buffer)
    }()
    
    // 使用buffer...
    
    return buffer, nil
}

4. 优化excelize使用

// 创建全局样式池
var stylePool = sync.Pool{
    New: func() interface{} {
        return &excelize.Style{
            Font: &excelize.Font{Bold: true},
        }
    },
}

func convert_json_to_excel(request_data *fastjson.Value, buffer_pointer *bytes.Buffer) error {
    // 从池中获取样式
    headerStyle := stylePool.Get().(*excelize.Style)
    defer stylePool.Put(headerStyle)
    
    // 使用样式...
    header_style, _ := excel_file.NewStyle(headerStyle)
    
    // 更早地释放json数据
    for row := 0; row < len(json_values_array); row++ {
        // 处理数据...
        json_values_array[row] = nil // 显式置空
    }
    json_values_array = nil
    
    return nil
}

5. FreeBSD特定的环境变量设置

在FreeBSD服务启动脚本中添加:

export GODEBUG=madvdontneed=1
export GOGC=50
export GOMEMLIMIT=256MiB

6. 完整的优化版本

func convert_json_to_excel(request_data *fastjson.Value, buffer_pointer *bytes.Buffer) error {
    // FreeBSD上更激进的内存管理
    debug.SetGCPercent(20)
    defer func() {
        debug.SetGCPercent(100)
        runtime.GC()
        debug.FreeOSMemory()
    }()
    
    json_values := request_data.Get("data")
    if json_values == nil {
        return fmt.Errorf("submitted json body must contain desired excel data in a key named \"data\"")
    }
    
    json_values_array, _ := json_values.Array()
    
    // 立即释放原始引用
    request_data = nil
    
    excel_file := excelize.NewFile()
    defer func() {
        excel_file.Close()
        excel_file = nil
    }()
    
    // 使用固定大小的切片避免重新分配
    rowCount := len(json_values_array)
    if rowCount == 0 {
        return nil
    }
    
    // 分批处理数据
    const batchSize = 100
    for start := 0; start < rowCount; start += batchSize {
        end := start + batchSize
        if end > rowCount {
            end = rowCount
        }
        
        // 处理批次数据...
        for row := start; row < end; row++ {
            // 处理逻辑...
            
            // 及时释放
            json_values_array[row] = nil
        }
        
        // 批次处理完后强制GC
        if start%1000 == 0 {
            runtime.GC()
        }
    }
    
    // 最终清理
    json_values_array = nil
    json_values = nil
    
    return nil
}

这些修改应该能缓解FreeBSD上的内存泄漏问题。主要策略包括:更频繁的GC、显式内存释放、使用对象池和分批处理大数据。

回到顶部