Golang Go语言中 flate.NewWriter 和 os.(*File).readdir 内存占用奇高

发布于 1周前 作者 bupafengyu 来自 Go语言

Golang Go语言中 flate.NewWriter 和 os.(*File).readdir 内存占用奇高

func (a *Agent) readFileEvents(writer *io.PipeWriter) {
defer writer.Close()
w := bufio.NewWriter(writer)
defer w.Flush()
ioBuff := make([]byte, 131072)
log := a.logger.WithField(“component”, “readFileEvents”)
var buff bytes.Buffer
vw, err := bsonrw.NewBSONValueWriter(&buff)
if err != nil {
a.logger.WithFields(logrus.Fields{
“error”: err.Error(),
}).Error(“Failed to create bson value writer”)
return
}
encoder, err := bson.NewEncoder(vw)
if err != nil {
a.logger.WithFields(logrus.Fields{
“error”: err.Error(),
}).Error(“Failed to create bson encoder”)
return
}

var gzBuff bytes.Buffer
zw, _ := flate.NewWriter(&gzBuff, 3)
handleEvent := func(event *fsnotify.Event) {
    a.logger.Debug("handle event")
    gzBuff.Reset()
    zw.Reset(&gzBuff)
    var entry *EventInfo
    a.metrics.eventsCounter.WithLabelValues(event.Op.String()).Inc()
    log.WithFields(logrus.Fields{
        "filename": event.Name,
        "type":     event.Op.String(),
    }).Debug("event")

    defer buff.Reset()

    fileInfo, err := os.Stat(event.Name)
    if err != nil {
        log.WithFields(logrus.Fields{
            "filename": event.Name,
            "error":    err.Error(),
        }).Debug("Failed to get file info")
        return
    }
    if fileInfo.Mode()&fs.ModeSymlink == fs.ModeSymlink {
        return
    }

    result := a.fileFilter(event.Name)
    if !result {
        return
    }
    file, err := os.Open(event.Name)
    if err != nil {
        log.WithFields(logrus.Fields{
            "error":    err.Error(),
            "filename": event.Name,
        }).Error("Failed to open file")
        return
    }
    defer file.Close()

    entry = eventInfoPool.Get().(*EventInfo)
    defer eventInfoPool.Put(entry)
    entry.ClientId = a.uuid
    entry.FilePath = event.Name

    if _, err = io.CopyBuffer(zw, file, ioBuff); err != nil {
        a.logger.WithFields(logrus.Fields{
            "error":    err.Error(),
            "filename": event.Name,
        }).Error("Failed to read file")
    }
    if err := zw.Close(); err != nil {
        log.WithFields(logrus.Fields{
            "error": err.Error(),
        }).Error("Failed to compress file")
        return
    }
    entry.FileContent = gzBuff.Bytes()

    err = encoder.Encode(entry)
    if err != nil {
        a.logger.WithFields(logrus.Fields{
            "error":    err.Error(),
            "filepath": entry.FilePath,
        }).Error("Failed to serialize object")
        return
    }

    if _, err = w.Write(buff.Bytes()); err != nil {
        a.logger.WithFields(logrus.Fields{
            "error": err.Error(),
        }).Error("Failed to transfer data")
        return
    }

    a.metrics.eventsSentCounter.WithLabelValues(event.Op.String()).Inc()
    a.metrics.fileCounter.Inc()
}
for {
    select {
    case event, ok := <-a.watcherChan:
        if !ok {
            return
        }
        handleEvent(&event)
    case <-a.ctx.Done():
        return
    }
}

}

使用 pprof 发现 flate.NewWriter 方法消耗了大量内存。 这张图是 inuse_space:
inuse_space
这张图是 alloc_space:
alloc_space
这张是 ReadDir: alloc_space of ReadDir

顺便吐槽下 os.(*File).ReadDir 内存占用也很离谱。 有没有大佬提供下优化思路 qaq


更多关于Golang Go语言中 flate.NewWriter 和 os.(*File).readdir 内存占用奇高的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

13 回复

大致看了一下,你这里的 entry 完全没有必要用 sync.Pool 池吧,应该可以直接分配在栈上,不会有什么 GC 压力,反而你这个 EventInfo 带有个大的属性 FileContent 的引用,你把 entry 放进池中的时候并没有把 FileContent 引用清掉,导致放进池中的 entry 仍旧引用着 FileContent 这种可能很大的[]byte, 导致 GC 无法回收。

更多关于Golang Go语言中 flate.NewWriter 和 os.(*File).readdir 内存占用奇高的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个 profile 不是 CPU 的图嘛?

#1 对,entry 没必要用 sync.Pool 。不用 sync.Pool 的话问题依然存在。刚在 handleEvent 函数里加了一行,退出函数前执行 w.Flush(),也没用。
看了下代码,对 gzBuff 底层数组引用的地方只有 entry.FileContent 和 buff,但是函数开始时都执行 Reset 了,不应该还占用呀


#2 这么明显的内存单位怎么能是 CPU 的图🐶



buff.Reset 是复位自己啊,FileContent 又不会 reset,看看以下会输出什么?

func f() {
var buf bytes.Buffer
io.WriteString(&buf, “Hello”)
r := buf.Bytes()
buf.Reset()
fmt.Println(string®)
}

#4 我这句话是有问题…… 不过不用 sync.Pool 的话问题还存在……

你的 readFileEvents 是不是被调用了很多次,导致 writer 被初始化了很多次

#6 不是,readFileEvents 方法是长期运行的,被调用很多次的是 handleEvent 函数。
其实问题出在 gzBuff 和 zw 两个变量上。这两个变量的生命周期和 readFileEvents 方法是相同的。假如随着程序的运行,要处理的文件越来越大,那么 gzBuff 和 zw 这两个变量的底层 byte slice 也会越来越大,而且不会被 GC 回收,byte slice 也不会自动收缩。所以随着运行时间内存使用量会持续增长。

重构了下 handleEvent 函数。这是根据压测结果来看目前性能最好的状态:

下面是三次压测结果,测试 5000 个文件,每个文件大小在 1M 以内。第一次结果是 bytes.Buffer 和 flate.Writer 都不使用 sync.Pool ;第二次压测结果是 bytes.Buffer 使用 Pool,flate.Writer 不使用;第三次是两个都使用 Pool 。

还有一个优化点是使用 bson.Marshal 方法序列化结构体。因为对这个库不是很熟悉,用了 Encoder 之后性能直接回到解放前。还需要再研究下

其实最优解应该是根据文件大小选择最合适的 bytes.Buffer,但是 sync.Pool 不支持这种操作。如果自己手动先 get,判断 buffer 大小再 put 的话,感觉会影响 GC 导致更严重的性能问题

#8 突然发现有个致命 bug,不应该在 compressFile 函数里就把 buff 给 Put,应该在 handleEvent 的 return 语句前面用 defer 给 Put 掉

用了 bson 库封装的 BSONValueWriterPool 对象池之后直接起飞,内存占用直接降到 1.5M 以下,而且内存分配操作次数的平均值为 9 到 10 次。

#11 还有个问题,bsonEncode 里的 buff 不能在当前函数 Put,要在 readFileEvents 里把数据发送到 pipe 之后再 Put 。

在Golang中,使用flate.NewWriteros.(*File).readdir时遇到内存占用奇高的问题,可能涉及几个关键因素。

首先,flate.NewWriter用于创建新的压缩写入器,它依赖于压缩算法(如Deflate)来压缩数据。如果输入数据非常大或压缩级别设置较高(如flate.BestCompression),则可能会消耗大量内存来进行压缩操作。建议根据实际需求调整压缩级别,或在处理大数据时考虑分块压缩。

其次,os.(*File).readdir用于读取目录内容。如果目录中包含大量文件或子目录,该操作可能会分配大量内存来存储读取的文件信息。特别是在递归读取深层目录结构时,内存占用会显著增加。可以考虑使用分页读取(即限制每次读取的文件数量)或按需读取文件信息来优化内存使用。

此外,还需注意以下几点:

  1. 确保没有内存泄漏。检查代码中是否有未关闭的文件句柄或未释放的内存。
  2. 使用性能分析工具(如pprof)来监测内存使用情况,找出内存占用的热点。
  3. 优化数据结构和算法,减少不必要的内存分配。

总之,内存占用高可能由多种因素引起,需结合具体代码和场景进行分析和优化。在优化过程中,关注内存分配、释放以及算法效率是关键。如果问题依然存在,建议查阅官方文档或社区资源,以获取更多帮助。

回到顶部