V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
sealinfree
V2EX  ›  Go 编程语言

求教 os.readfile 内存溢出的问题

  •  
  •   sealinfree · 2022-11-29 20:53:09 +08:00 · 1564 次点击
    这是一个创建于 507 天前的主题,其中的信息可能已经有所发展或是发生改变。

    求教一个内存溢出的问题 pprof 显示在 os.readfile 上有大量内存占用无法释放 请问我下面的写法具体哪里有问题 已经尝试很多办法,都没效果 恳请赐教

    var GMap=map[string]string
    
    func DomainMapLoadFromFile() {
    	GMap=make(map[string]string,10)
    	//尝试解决内存溢出
    	fileStr := ReadAll("DataMap", "CacheMap")
    	contentStrArr := strings.Split(*fileStr, "\n")
    	contentLen := len(contentStrArr)
    	contentArr := make([]string, contentLen)
    	copy(contentArr, contentStrArr)
    	var wg sync.WaitGroup
    	wg.Add(contentLen)
    	for i := range contentArr {
    		go func(content string) {
    			infoArr := strings.Split(content, "|")
    			var deviceId int64
    			l := len(infoArr)
    			if l == 2 {
    				data1 = infoArr[0]
    				data2 = infoArr[1]
    			} else {
    				fmt.Println("不符合长度 5-6 的数据,content:" + content)
    				wg.Done()
    				return
    			}
    			GMap[data1]=data2
    			wg.Done()
    		}(contentArr[i])
    	}
    	wg.Wait()
    }
    
    func ReadAll(fileName, dirName string) *string {
    	content, _ := os.ReadFile(GetFilePathPWD(fileName, dirName))
    	contentStr := string(content)
    	return &contentStr
    }
    
    第 1 条附言  ·  2022-11-29 22:24:42 +08:00
    问题已解决,使用 3 楼提供的字符串深拷贝 strings.clone 解决了,感谢!
    解决该类型问题的关键字,deepcopy ,有需求的朋友,可以 github 一下,如果需要结构体等复杂类型的深拷贝,可以使用 github.com/antlabs/deepcopy ,测试了三个 go 的深拷贝包,这个最快
    24 条回复    2022-12-02 15:21:26 +08:00
    Maboroshii
        1
    Maboroshii  
       2022-11-29 21:09:45 +08:00
    怎么复现的?
    sealinfree
        2
    sealinfree  
    OP
       2022-11-29 21:15:00 +08:00
    @Maboroshii #1 线上系统 pprof 比较了 6 个小时间隔的内存数据,这部分增长了 3G 的内存
    yin1999
        3
    yin1999  
       2022-11-29 21:20:05 +08:00   ❤️ 1
    ReadAll 里面应该返回 string ,这不会增加开销,用指针反而会增加 GC 的开销。用 copy 拷贝数组,应该不会拷贝字符串吧,strings.Split 函数返回的子串都是对原字符串的引用。尝试在向里面的匿名函数传递数组元素时,使用 strings.Clone() 拷贝一份子串
    yin1999
        4
    yin1999  
       2022-11-29 21:21:51 +08:00   ❤️ 1
    var GMap=map[string]string

    func DomainMapLoadFromFile() {
    GMap=make(map[string]string,10)
    //尝试解决内存溢出
    fileStr := ReadAll("DataMap", "CacheMap")
    contentStrArr := strings.Split(*fileStr, "\n")
    var wg sync.WaitGroup
    wg.Add(contentLen)
    for i := range contentStrArr {
    go func(content string) {
    infoArr := strings.Split(content, "|")
    var deviceId int64
    l := len(infoArr)
    if l == 2 {
    data1 = infoArr[0]
    data2 = infoArr[1]
    } else {
    fmt.Println("不符合长度 5-6 的数据,content:" + content)
    wg.Done()
    return
    }
    GMap[data1]=data2
    wg.Done()
    }(strings.Clone(contentStrArr[i]))
    }
    wg.Wait()
    }

    func ReadAll(fileName, dirName string) string {
    content, _ := os.ReadFile(GetFilePathPWD(fileName, dirName))
    contentStr := string(content)
    return contentStr
    }
    sealinfree
        5
    sealinfree  
    OP
       2022-11-29 21:23:48 +08:00
    @yin1999 #3 感谢指点,我改下试试
    yin1999
        6
    yin1999  
       2022-11-29 21:25:09 +08:00   ❤️ 2
    也可以选择在
    data1 = infoArr[0]
    data2 = infoArr[1]
    这里克隆两个字符串
    data1 = strings.Clone(infoArr[0])
    data2 = strings.Clone(infoArr[1])
    应该是 GMap 长期持有子串,造成整个字符串无法被 GC ,尝试在长期持有子串的地方克隆一下子串
    sealinfree
        7
    sealinfree  
    OP
       2022-11-29 21:28:11 +08:00
    @yin1999 #6 有道理!感谢!受教了,我改好上线跑一段时间看看
    sealinfree
        8
    sealinfree  
    OP
       2022-11-29 22:22:09 +08:00
    @yin1999 问题解决,内存稳定了,感谢
    swulling
        9
    swulling  
       2022-11-29 22:30:30 +08:00 via iPhone
    嗯,这应该是经典的子串内存泄漏问题。

    字符串的子串不回收会导致整个字符串不回收。
    rrfeng
        10
    rrfeng  
       2022-11-29 22:58:12 +08:00 via Android
    问题解决了,那只好吐槽一下这段代码了…
    1. 一次读完整个文件有内存爆炸的风险,建议使用 bufio 逐行处理
    2. 每行放到一个 goroutine 里切分还要加锁,真不如顺序处理快……这样写又复杂又慢又容易错
    sealinfree
        11
    sealinfree  
    OP
       2022-11-30 01:26:24 +08:00
    @rrfeng 谢谢指导,因为数据最终要放到内存作为常驻缓存,所以逐行读取和一次性读取区别不大;切分处理速度比顺序处理快 6 倍,配置是 12 核心 24 线程虚拟机,也是优化过才成了这个样子,第一版是流式顺序处理,载入一次 1G 文件要 1 分钟多,无法忍受,改为多线程变成 10-16 秒了
    sealinfree
        12
    sealinfree  
    OP
       2022-11-30 01:27:08 +08:00
    @swulling 是的,之前不明白此处该使用深拷贝,这次学习了
    rrfeng
        13
    rrfeng  
       2022-11-30 06:34:44 +08:00 via Android
    @sealinfree
    一次读取你要用掉 2 倍内存
    处理速度这个我不太相信,要不把顺序处理的代码也贴一下,还有文件内容
    lysS
        14
    lysS  
       2022-11-30 10:29:32 +08:00
    楼上怎么一唱一和的。。。

    有几点:
    1. 代码有多处基础的语法错误
    2. ReadAll 为什么要返回 str ptr ?
    3. map 并发操作不安全
    4. 你这个需求可以流式处理,不需要首先就 load 所有数据到内存
    5. GetFilePathPWD 可以 path.Join ,当然可能你有特殊的需求
    6. 值拷贝通常比指针引用占用更多的内存。

    当然你这确实可能存在溢出的问题,大概是这样:一个很大的字符串 str ,只把它的一部分放进 map[1]=str[0:3]后,导致这个大的 str 的其他部分不能被回收。我不太清楚 gogc 对这种情况是咋做的。
    如果上面假设成立,解决办法也很简单,用[]byte 从文件读,存入 map 的时候 map[string(data1)] = string(data2)
    sealinfree
        15
    sealinfree  
    OP
       2022-11-30 23:13:27 +08:00
    @lysS 1 、截取了片段代码,部分是为了表达清楚逻辑构造的。
    2 、最开始返回的 str ,但是内存溢出,就不断尝试调整
    3 、实际使用的是带分区读写锁的 map ,还是为了逻辑简单直接构造了一个 map
    4 、服务器内存比较大,直接读取实测速度更快,就改为这个版本了
    5 、该函数底层就是 path.Join ,只是因为路径比较复杂,要动态获取,单独封装了一个函数
    6 、指针引用再搭配分区 map ,会产生一些不可预料的修改错误,所以还是用值传递比较多了

    最后,使用 string()转换不能解决溢出问题,使用 strings.clone 深拷贝才可以
    sealinfree
        16
    sealinfree  
    OP
       2022-11-30 23:14:21 +08:00
    @lysS 感谢这么多较真的朋友!
    lysS
        17
    lysS  
       2022-12-01 09:42:31 +08:00
    @sealinfree string([]byte) 就是重新分配的内存,无论是 header 还是数据本身;和之前不存在任何引用关系
    sealinfree
        18
    sealinfree  
    OP
       2022-12-01 10:05:16 +08:00
    @lysS 好的,那回头我再测试下
    macscsbf
        19
    macscsbf  
       2022-12-01 13:13:13 +08:00
    我想知道是不是因为你用 map 存储着 content 的引用导致了这个 content 一直没被释放掉
    darknoll
        20
    darknoll  
       2022-12-01 20:01:39 +08:00
    var GMap=map[string]string
    这句能编译过?
    sealinfree
        21
    sealinfree  
    OP
       2022-12-01 21:49:39 +08:00
    @darknoll 更正 var GMap=map[string]string{}
    sealinfree
        22
    sealinfree  
    OP
       2022-12-01 21:50:13 +08:00
    @macscsbf 存的都是值,没有使用指针
    macscsbf
        23
    macscsbf  
       2022-12-02 08:45:56 +08:00
    @sealinfree data1 和 data2 都是 content 的子串,本质就是指针指向了 content
    sealinfree
        24
    sealinfree  
    OP
       2022-12-02 15:21:26 +08:00
    @macscsbf 哦,此处应该用 strings.clone 复制一份
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2697 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 1970ms · UTC 15:12 · PVG 23:12 · LAX 08:12 · JFK 11:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.