Golang 加密/tls 内存泄漏

如何解决Golang 加密/tls 内存泄漏

问题: 为什么 *tls.Conn 即使在变量超出范围后也不会被垃圾收集,并且使用 (*tls.Conn).Close() 方法正确关闭它?下面给出了要重现的完整代码

动机:到目前为止,我已经尝试了 2 个 websocket 库(https://github.com/gorilla/websockethttps://github.com/gobwas/ws)作为长期运行服务(> 24 小时)的一部分,它保持了 ~在其整个生命周期中作为客户端的 10 个 websocket 连接。有时它们会与服务器断开连接,在这种情况下,我会建立新的连接。内存使用量在其整个生命周期中稳步增长,并且从内存配置文件中,它指向留在堆上的底层 *tls.Conn 对象。 (没有被垃圾收集)。

要重现的完整代码

package main

import (
    "crypto/tls"
    "fmt"
    "log"
    _ "net/http/pprof"
    "os"
    "os/signal"
    "runtime"
    "syscall"
    "time"
)

func finalizer(_ interface{}) {
    fmt.Println("finalizer called")
}

func main() {
    // setup interrupt handler
    c := make(chan os.Signal)
    signal.Notify(c,os.Interrupt,syscall.SIGTERM)

    for i := 0; i < 100; i++ {
        go func() {
            for {
                tlsConnectThenCloseAfterWait()
            }
        }()
    }

    <-c
    os.Exit(1)

}

func tlsConnectThenCloseAfterWait() {
    conn,err := tls.Dial("tcp","mail.google.com:443",&tls.Config{})
    if err != nil {
        log.Fatalln("Failed to connect: " + err.Error())
    }
    defer func() {
        err := conn.Close()
        if err != nil {
            log.Fatalln("closing conn Failed")
        }
    }()

    runtime.SetFinalizer(conn,finalizer)
    conn.Write([]byte("hello how are you"))

    timer := time.NewTimer(time.Second)
    <-timer.C
}

GODEBUG=gctrace=1 ./main输出

gc 1 @0.088s 1%: 0+9.0+0 ms clock,0+12/11/0+0 ms cpu,4->5->1 MB,5 MB goal,12 P
gc 2 @0.102s 3%: 0+4.9+0.99 ms clock,0+5.9/6.0/0+11 ms cpu,4->4->1 MB,12 P
gc 3 @0.114s 5%: 0+4.9+1.0 ms clock,0+3.9/10/2.9+12 ms cpu,4->4->2 MB,12 P
gc 4 @0.171s 4%: 0+5.0+0 ms clock,0+1.0/9.9/0+0 ms cpu,4->5->3 MB,12 P
gc 5 @0.196s 4%: 0+5.9+0 ms clock,0+2.9/9.9/0+0 ms cpu,5->7->3 MB,6 MB goal,12 P
gc 6 @0.352s 2%: 1.0+2.0+0 ms clock,12+0/1.9/1.9+0 ms cpu,6->7->4 MB,7 MB goal,12 P
gc 7 @0.365s 2%: 0.99+3.0+0 ms clock,11+3.0/5.0/0+0 ms cpu,7->8->5 MB,8 MB goal,12 P
gc 8 @0.399s 3%: 0+13+0 ms clock,0+18/29/1.0+0 ms cpu,9->11->6 MB,10 MB goal,12 P
gc 9 @1.278s 1%: 1.0+28+0 ms clock,12+9.9/53/0+0 ms cpu,10->13->9 MB,13 MB goal,12 P
gc 10 @1.433s 2%: 1.0+22+0 ms clock,12+45/55/1.0+0 ms cpu,14->16->9 MB,18 MB goal,12 P
gc 11 @1.534s 2%: 0+6.0+0 ms clock,0+4.0/15/3.0+0 ms cpu,16->17->11 MB,19 MB goal,12 P
gc 12 @2.479s 1%: 0+3.0+0 ms clock,0+0/6.0/18+0 ms cpu,20->20->12 MB,22 MB goal,12 P
gc 13 @2.656s 1%: 1.0+10+0 ms clock,12+3.0/30/4.9+0 ms cpu,23->25->16 MB,25 MB goal,12 P
gc 14 @3.737s 1%: 0+6.0+0 ms clock,0+3.0/18/9.0+0 ms cpu,31->33->20 MB,33 MB goal,12 P
gc 15 @4.830s 0%: 0+5.9+0 ms clock,0+5.0/13/16+0 ms cpu,39->40->25 MB,41 MB goal,12 P
gc 16 @6.733s 0%: 0.99+16+0.99 ms clock,11+7.9/47/80+11 ms cpu,50->50->32 MB,51 MB goal,12 P
gc 17 @8.140s 0%: 0.99+21+0 ms clock,11+3.0/59/125+0 ms cpu,64->64->42 MB,65 MB goal,12 P
gc 18 @11.168s 0%: 1.0+28+0 ms clock,12+24/78/97+0 ms cpu,82->82->54 MB,84 MB goal,12 P
gc 19 @14.433s 0%: 0.99+27+0 ms clock,11+9.0/74/146+0 ms cpu,106->106->70 MB,108 MB goal,12 P
gc 20 @18.883s 0%: 0+47+0 ms clock,0+6.0/133/211+0 ms cpu,137->138->91 MB,140 MB goal,12 P
gc 21 @24.437s 0%: 0.99+30+0.99 ms clock,11+15/91/101+11 ms cpu,177->178->118 MB,182 MB goal,12 P
gc 22 @31.872s 0%: 1.0+105+0 ms clock,12+60/317/256+0 ms cpu,230->233->155 MB,236 MB goal,12 P
gc 23 @41.705s 0%: 0+101+0 ms clock,0+15/283/549+0 ms cpu,302->303->200 MB,310 MB goal,12 P
gc 24 @54.302s 0%: 0+92+0 ms clock,0+9.0/278/472+0 ms cpu,390->392->260 MB,400 MB goal,12 P
gc 25 @70.777s 0%: 0+38+0 ms clock,0+4.9/113/321+0 ms cpu,508->508->337 MB,521 MB goal,12 P
gc 26 @92.203s 0%: 0+108+0 ms clock,0+57/326/391+0 ms cpu,658->662->442 MB,675 MB goal,12 P
gc 27 @120.781s 0%: 2.0+99+0 ms clock,24+11/292/529+0 ms cpu,862->864->574 MB,884 MB goal,12 P

从不调用终结器,内存不断增长。

使用 go version go1.15.8 windows/amd64

链接到此处的 github 问题:https://github.com/golang/go/issues/41987

解决方法

您似乎滥用了 runtime.SetFinalizerthe doc 是这样说的:

[第一个参数] 必须是一个指针,指向通过调用 new、获取复合文字的地址或获取局部变量的地址分配的对象。

(我的重点)

如果我将 &conn(而不是 conn 本身)传递给 runtime.SetFinalizer,终结器会被调用并且堆永远不会超过 8 MB:

$ GODEBUG=gctrace=1 ./main
gc 1 @1.789s 0%: 0.041+0.67+0.018 ms clock,0.33+0.31/1.0/2.3+0.14 ms cpu,4->4->1 MB,5 MB goal,8 P
gc 2 @1.874s 0%: 0.044+0.69+0.015 ms clock,0.35+0.20/1.0/2.4+0.12 ms cpu,4->4->2 MB,8 P
gc 3 @1.880s 0%: 0.064+0.79+0.014 ms clock,0.51+0.22/1.1/2.3+0.11 ms cpu,8 P
gc 4 @1.887s 0%: 0.14+1.5+0.080 ms clock,1.1+1.6/2.5/0+0.64 ms cpu,5->5->3 MB,6 MB goal,8 P
gc 5 @1.904s 0%: 0.097+1.1+0.025 ms clock,0.77+1.0/1.9/2.6+0.20 ms cpu,6->6->3 MB,7 MB goal,8 P
gc 6 @1.974s 0%: 0.12+1.7+0.12 ms clock,0.99+1.4/2.9/1.2+0.98 ms cpu,6->7->3 MB,8 P
gc 7 @2.900s 0%: 0.22+1.9+0.025 ms clock,1.8+5.7/2.9/0+0.20 ms cpu,7->7->4 MB,8finalizer called
 MB goal,8finalizer called
 P
finalizer called
finalizer called
finalizer called
finalizer called
finalizer called
finalizer called
--snip--

使用 -gcflags="-m" 编译程序会发现局部变量 conn 被移到了堆中。我不是终结器专家(到目前为止),但我怀疑您对 runtime.SetFinalizer 的滥用会导致保留对 conn 变量的引用,从而使其每个实例都没有资格进行垃圾收集,因此内存泄漏。

我不清楚你为什么要使用终结器;传统观点是finalizers are best avoided

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?