广告
返回顶部
首页 > 资讯 > 前端开发 > node.js >怎么排查goroutine泄漏问题
  • 713
分享到

怎么排查goroutine泄漏问题

2024-04-02 19:04:59 713人浏览 安东尼
摘要

这篇文章主要介绍“怎么排查Goroutine泄漏问题”,在日常操作中,相信很多人在怎么排查goroutine泄漏问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么排查go

这篇文章主要介绍“怎么排查Goroutine泄漏问题”,在日常操作中,相信很多人在怎么排查goroutine泄漏问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么排查goroutine泄漏问题”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

golang 中创建 goroutine 是一件很容易的事情,但是不合理的使用可能会导致大量 goroutine  无法结束,资源也无法被释放,随着时间推移造成了内存的泄漏。避免 goroutine 泄漏的关键是要合理管理 goroutine 的生命周期,通过导出  runtime 指标和利用 pprof 可以发现和解决 goroutine 泄漏问题。

笔者维护了一个通过 ssh 连接到目标机器并执行命令的服务,这是一个内部小服务,平时没有问题的时候一般也不会关注。大约 4  个月前,最后一次更新的时候,增加了一个任务计数器并且导出到 prometheus监控起来。近期发现这个计数器在稳步增加。

怎么排查goroutine泄漏问题

第一反应是,好事!调用量稳步增长了!!但是一想不对啊,这内部小服务哪儿来这么多调用量。于是再看看 goroutine 的监控情况(这个数据从  runtime.NumGoroutine()获取的)

怎么排查goroutine泄漏问题

goroutine 的数量也是稳步增加的,单位时间请求量增加,goroutine 数量也增进,没毛病。但是又再转念一想,内部小服务,不可能不可能。

于是再看一下所有请求在 mm 系统的视图:

怎么排查goroutine泄漏问题

可以看出,每 5 分钟请求量在 2000 左右,平均下来每分钟 400 的请求量,上面 prometheus 监控图中,每个曲线是一个实例,实际上部署了  4 个实例,因此 400 还要除以 4 得到单个实例(曲线)的请求量应该在 100/min 左右,在服务刚启动的时候该计数器也确实在 100/min  左右,随着时间推移慢慢泄漏了。

Goroutine 泄漏 (Goroutine leak)

虽然心里想着 99%是泄漏了,但是也要看点详细的信息。之前在服务里已经启用了 net/Http/pprof,因此直接请求 pprof 暴露出来的 HTTP  接口。

先看一下导出的 goroutine 摘要:

怎么排查goroutine泄漏问题

有 1000+个 goroutine 处于同一个状态,简单看是等待读数据,再看下导出的 goroutine 详情:

怎么排查goroutine泄漏问题

不看不知道,一看吓一跳,详情里有 goroutine 阻塞的时间超过了 20w 分钟(4 个月)……

可以肯定是 goroutine 泄漏无疑了。为什么会泄漏?只有顺着 pprof 导出的 goroutine 信息去排查了。处于 io wait  状态最多的这 1000 多 goroutine 的调用栈都打出来了,根据这段调用栈内容来看,找到对应代码的位置,从 ssh.Dial 开始一直到某个地方进行  io.ReadFull 便阻塞住了。

这个服务进行 ssh 连接使用的是 golang.org/x/crypto/ssh 这个包。先看一下在这个服务里调用 ssh.Dial 的地方:

clientConfig := &ssh.ClientConfig{     ...     Timeout: 3 * time.Second,     ... } // connet to ssh client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", s.Host, 36000), clientConfig)

看起来是没啥问题的,毕竟传入了一个 Timeout 参数,不应该会阻塞。接着继续看下去发现了一些问题。直接来到调用栈中阻塞的地方(先不看 library  和 runtime,这两个一般没问题),是在进行 SSH Handshake 的第一个步骤,交换 SSH 版本这步。

// Sends and receives a version line.  The versionLine string should // be US ASCII, start with "SSH-2.0-", and should not include a // newline. exchangeVersions returns the other side's version line. func exchangeVersions(rw io.ReadWriter, versionLine []byte) (them []byte, err error) {     ...     if _, err = rw.Write(append(versionLine, '\r', '\n')); err != nil {         return     }      them, err = readVersion(rw)     return them, err }  // maxVersionStringBytes is the maximum number of bytes that we'll // accept as a version string. RFC 4253 section 4.2 limits this at 255 // chars const maxVersionStringBytes = 255  // Read version string as specified by RFC 4253, section 4.2. func readVersion(r io.Reader) ([]byte, error) {     versionString := make([]byte, 0, 64)     var ok bool     var buf [1]byte      for length := 0; length < maxVersionStringBytes; length++ {         _, err := io.ReadFull(r, buf[:]) // 阻塞在这里         if err != nil {             return nil, err         }         ...     }      ...     return versionString, nil }

看逻辑是在给对端发送完自己的版本信息后,等待对端回复,但是一直没有收到回复。但是为什么会没回复,为什么没有超时,刚开始看到这里的我是懵逼的。我只能想到既然这些都阻塞在等待对端回复上,那么一定有对应的连接存在,我先看看机器上的连接有什么问题。

TCP 连接的半打开状态 (TCP half-open)

在机器上执行了一下 netstat 命令看了下连接数。

# netstat -anp|grep :36000|awk '{print $6}'|sort|uniq -c  2110 ESTABLISHED       1 LISTEN      41 TIME_WAIT

有大量处于 ESTABLISHED 的进程,数量和 goroutine 数能大致对上。先把注意力放到这些连接上,选其中一两个看看有什么问题吧。

接着便发现,有些连接,在本机有 6 个连接:

怎么排查goroutine泄漏问题

但是,对端一个也没有(图上那一个连接是我登录到目标机器的 ssh 连接):

怎么排查goroutine泄漏问题

google 查了下,发现这种情况属于 TCP 半打开状态,出现这种情况应该是建立连接后对端挂掉了或者其他网络无法连通的原因,而连接又没有启动  KeepAlive,导致一端无法发现这种情况,继续显示 ESTABLISHED  的连接,而另一端在机器挂掉重新启动后便不存在这条链接了。现在要确认一下是否真的没用启用 KeepAlive:

# ss -aeon|grep :36000|grep -v time|wc -l 2110

全部没开&hellip;&hellip;这里不带 KeepAlive 的连接数和上面 netstat 显示出来状态为 ESTABLISHED  状态的连接数一致,实际上在执行这两条命令的间隙肯定有新请求进来,这两个数字对上不能说完全匹配,只能说大多数是没有开启的。这里能 Get 到点就行。

再看一下 ssh.Dial 的逻辑,建立连接用的是 net.DialTimeout,而现网发生泄漏的版本是用 go1.9.2 编译的,这个版本的  net.DialTimeout 返回的 net.Conn 结构体的 KeepAlive 是默认关闭的(go1.9.2/net/client.go )。

golang.org/x/crypto/ssh 包在调用 net.DialTimeout 时不会显式启用 KeepAlive,完全依赖于当前 go  版本的默认行为。在最新版的 go 里面已经把建立 TCP 连接时启动 KeepAlive 作为默认行为了,于是这里我把代码迁移到 go1.13.3  重新编译了一次发到现网了,以为问题就尘埃落定了。

SSH 握手阻塞 (SSH Handshake hang)

实际上不是的。用 go1.13.3 编译的版本,运行一段时间后,用 pprof 看 goroutine 情况,还是存在不少处于 IO wait  状态的,并且看调用栈还是原来的味道(SSH handshake 时交换版本信息)。再看一下机器上的连接情况:

# netstat -anp|grep :36000|awk '{print $6}'|sort|uniq -c      81 ESTABLISHED       1 LISTEN       1 SYN_SENT      23 TIME_WAIT # ss -aeon|grep :36000|grep time|wc -l 110 # ss -aeon|grep :36000|grep -v time|wc -l 1 # ss -aeon|grep :36000|grep -v time LISTEN     0      128         100.107.1x.x6:36000                    *:*      ino:2508898466 s

不带 KeepAlive 那个连接是本机监听 36000 端口的 sshd,其他都带上了,那没什么问题。说明这些阻塞住的应该不是因为 TCP  半打开导致阻塞的,选其中一个 IP 出来看看。

怎么排查goroutine泄漏问题

用 telnet 可以连上,但是无法断开连接。说明 TCP 连接是可以建立的,对端却因为一些不可知的原因不响应。再看看这个 IP 的连接存在多久了

# netstat -atnp|grep 10.100.7x.x9 tcp        0      0 100.107.1x.x6:8851        10.100.7x.x9:36000         ESTABLISHED 33027/ssh_tunnel_se # lsof -p 33027|grep 10.100.7x.x9 ssh_tunne 33027  MQq   16u  IPv4 3069826111      0t0        TCP 100-107-1x-x6:8504->10.100.7x.x9:36000 (ESTABLISHED) # ls -l /proc/33027/fd/16 lrwx------ 1 mqq mqq 64 Dec 23 15:44 /proc/33027/fd/16 -> Socket:[3069826111]

执行这个命令的时间是 24 日 17 时 25 分,已经阻塞一天多了。那这里的问题就是应用层没有超时控制导致的。再回过去看 ssh.Dial  的逻辑,Timeout 参数在 SSH handshake 的时候并没有作为超时控制的参数使用。net.Conn 的 IO 等待在 linux 下是用非阻塞  epoll_pwait 实现的,进入等待的 goroutine 会被挂起直到有事件进来,超时是通过设置 timer 唤醒 goroutine  进行处理的,暴露出来的接口便是 net.Conn 的 SetDeadline 方法,于是重写了 ssh.Dial 的逻辑,给 SSH

handshake 阶段添加超时:

// DialTimeout starts a client connection to the given SSH server. Differ from // ssh.Dial function, this function will be timeout when doing SSH handshake. // total timeout = ( 1 + timeFactor ) * config.Timeout // refs: https://GitHub.com/cybozu-go/cke/pull/81/files func DialTimeout(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {     conn, err := net.DialTimeout(network, addr, config.Timeout)     if err != nil {         return nil, err     }      // set timeout for connection     timeFactor := time.Duration(3)     err = conn.SetDeadline(time.Now().Add(config.Timeout * timeFactor))     if err != nil {         conn.Close()         return nil, err     }      // do SSH handshake     c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)     if err != nil {         return nil, err     }      // cancel connection read/write timeout     err = conn.SetDeadline(time.Time{})     if err != nil {         conn.Close()         return nil, err     }     return ssh.NewClient(c, chans, reqs), nil }

用这个函数替换了 ssh.Dial 后,编译上线,看下连接情况,恢复正常了。(恢复到一个小服务应该有的样子)

# netstat -anp|grep :36000|awk '{print $6}'|sort|uniq -c       3 ESTABLISHED       1 LISTEN      86 TIME_WAIT

到这里会发现,其实本文解决的问题是对端如果出现各种异常了,如何及时关闭连接,而不是去解决对端的异常问题。毕竟 SSH  都异常了,谁还能上去查问题呢。现网服务器数量巨大,运行情况各不相同,因此出现异常也属情理之中,一一解决不太现实。

结尾

刚开始发现泄漏的时候到机器上 top 看了下,当时被 50G 的 VIRT  占用给吓着了,在咨询了组内大佬(zorro)的后,实际上这个值大多数时候都不用关心,只需关心 RES 占用即可。因为 RES 是实际占用的物理内存。

怎么排查goroutine泄漏问题

只看这一个时间点的 VIRT 和 RES  也是看不出到底有多少是泄漏的。只能和不同的时间点的内存占用对比,比如解决问题以后的版本,运行了三四天的情况下,VIRT 占用是 3.9G,而 RES 只占用了  16M。这样比下来看,还是释放了不少内存。或者说可以见得泄漏的那些 goroutine 占据了多少内存。

在 golang 中创建 goroutine 是一件很容易的事情,但是不合理的使用可能会导致大量 goroutine  无法结束,资源也无法被释放,随着时间推移造成了内存的泄漏。

避免 goroutine 泄漏的关键是要合理管理 goroutine 的生命周期,通过 prometheus/client_golang 导出  runtime 指标和利用 net/http/pprof 可以发现和解决 goroutine 泄漏问题。

到此,关于“怎么排查goroutine泄漏问题”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

--结束END--

本文标题: 怎么排查goroutine泄漏问题

本文链接: https://www.lsjlt.com/news/85239.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 怎么排查goroutine泄漏问题
    这篇文章主要介绍“怎么排查goroutine泄漏问题”,在日常操作中,相信很多人在怎么排查goroutine泄漏问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么排查go...
    99+
    2022-10-19
  • linux内存泄漏问题怎么排查
    要排查Linux中的内存泄漏问题,可以按照以下步骤进行:1. 监控内存使用情况:使用工具如top、free或htop等监控系统的实时...
    99+
    2023-10-21
    linux
  • Java内存泄漏问题排查与解决
    前言 Java 最牛逼的一个特性就是垃圾回收机制,不用像 C++ 需要手动管理内存,所以作为 Java 程序员很幸福,只管 New New New 即可,反正 Java 会自动回收过...
    99+
    2022-11-13
  • golang内存泄漏怎么排查
    在 Go 语言中,内存泄漏通常是由于不正确地使用或管理指针和引用导致的。以下是一些排查内存泄漏的常用方法:1. 使用 go buil...
    99+
    2023-10-21
    golang
  • 怎么排查Javascript内存泄漏
    这篇文章主要讲解了“怎么排查Javascript内存泄漏”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么排查Javascript内存泄漏”吧!如何判断我的应用发生了内存泄漏为了证明螃蟹的听...
    99+
    2023-07-02
  • java内存泄漏怎么排查
    Java内存泄漏是指在程序运行过程中,不再使用的对象仍然占用着内存空间,导致内存无法被回收。以下是一些常见的排查内存泄漏的方法:1....
    99+
    2023-08-31
    java
  • java堆外内存泄漏怎么排查
    在Java中,堆外内存通常是通过直接内存(Direct Memory)分配的。直接内存是一种不受Java堆内存管理的内存分配方式,它...
    99+
    2023-10-27
    java
  • 怎么解决内存泄漏问题
    本篇内容介绍了“怎么解决内存泄漏问题”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!问题排查首先确定内存泄漏问题出现的时间,发现在该时间点的上...
    99+
    2023-06-16
  • drawimage内存泄漏问题怎么解决
    解决drawImage内存泄漏问题的方法如下:1. 及时释放资源:使用完image对象后,可以调用`image = null;`来手...
    99+
    2023-09-05
    drawimage
  • java内存泄漏问题怎么解决
    这篇文章主要介绍“java内存泄漏问题怎么解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“java内存泄漏问题怎么解决”文章能帮助大家解决问题。1、概念Java中的内存泄露是指不再使用的对象的内存...
    99+
    2023-06-30
  • Java的内存泄漏问题怎么解决
    本篇内容介绍了“Java的内存泄漏问题怎么解决”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一问题的提出Java的一个重要优点就是通过垃圾收...
    99+
    2023-06-03
  • java怎么检查内存泄漏
    本篇内容介绍了“java怎么检查内存泄漏”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!内存泄漏场景长生命周期的对象持有短生命周期对象的引用就...
    99+
    2023-06-30
  • 怎么在Android中利用LeakCanary对内存泄漏进行排查
    今天就跟大家聊聊有关怎么在Android中利用LeakCanary对内存泄漏进行排查,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。在 build.gralde 里加上依赖, 然后sy...
    99+
    2023-05-31
    android 内存泄漏 leakcanary
  • 【经验总结 】Node怎么排查内存泄漏?思路分享
    内存泄漏类型内存泄漏主要分为:全局性泄漏局部性泄漏其实不管是全局性内存泄漏还是局部性的内存泄漏,要做的都是尽可能缩小排除范围。全局性内存泄漏全局性内容泄漏出现一般高发于:中间件与组件中,这种类型的内存泄漏排查起来也是最简单的。很遗憾我在 2...
    99+
    2023-05-14
    Node.js 前端
  • java线上问题怎么排查
    要排查Java线上问题,可以采取以下步骤: 收集问题信息:收集问题的现象、出现的频率、受影响的用户、相关日志信息等。 查看日...
    99+
    2023-10-27
    java
  • 怎么排查Spring Boot内存泄露
    本篇内容主要讲解“怎么排查Spring Boot内存泄露”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么排查Spring Boot内存泄露”吧!为了更好地实现对项目的管理,我们将组内一个项目迁...
    99+
    2023-06-16
  • android内存溢出和内存泄漏问题怎么解决
    Android内存溢出和内存泄漏是常见的问题,可以通过以下方法来解决:1. 使用内存分析工具:可以使用Android Studio自...
    99+
    2023-08-26
    android
  • java内存逃逸问题怎么排查
    Java内存逃逸问题通常是指在程序中创建的对象在其作用域之外仍然被引用,导致无法被垃圾收集器回收。以下是一些排查Java内存逃逸问题...
    99+
    2023-10-23
    java
  • web开发怎么排查超时问题
    这篇文章主要讲解了“web开发怎么排查超时问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“web开发怎么排查超时问题”吧!一气呵成,bug 侧漏var&n...
    99+
    2022-10-19
  • 怎么快速排查Linux硬件问题
    这篇文章给大家分享的是有关怎么快速排查Linux硬件问题的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。1.快速诊断设备、模块和驱动程序故障排查的第一步通常是显示Linux服务器上安装的硬件列表。你可以使用ls命令...
    99+
    2023-06-28
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作