之前在 golang 群里有人问过为什么程序会莫名其妙的 hang 死然后不再响应任何请求。单核 cpu 打满。

这个特征和我们公司的某个系统曾经遇到的情况很相似,内部经过了很长时间的定位分析总结,期间还各种阅读 golang 的 runtime 和 gc 代码,最终才定位到是业务里出现了类型下面这样的代码:

package main

import "runtime"

func main() {
var ch = make(chan int, 100)
go func() {
for i := 0; i < 100; i++ {
ch <- 1
if i == 88 {
runtime.GC()
}
}
}() for {
// the wrong part
if len(ch) == 100 {
sum := 0
itemNum := len(ch)
for i := 0; i < itemNum; i++ {
sum += <-ch
}
if sum == itemNum {
return
}
}
} }

在 main goroutine 里循环判断 ch 里是否数据被填满,在另一个 goroutine 里把 100 条数据塞到 ch 里。看起来程序应该是可以正常结束的对不对?

感兴趣的话你可以自己尝试运行一下。实际上这个程序在稍微老一些版本的 golang 上是会卡死在后面这个 for 循环上的。原因呢?

使用:

GODEBUG="schedtrace=300,scheddetail=1" ./test1

应该可以看到这时候 gcwaiting 标记为 1。所以当初都怀疑是 golang gc 的 bug。。但最终折腾了半天才发现还是自己的代码的问题。

因为在 for 循环中没有函数调用的话,编译器不会插入调度代码,所以这个执行 for 循环的 goroutine 没有办法被调出,而在循环期间碰到 gc,那么就会卡在 gcwaiting 阶段,并且整个进程永远 hang 死在这个循环上。并不再对外响应。

当然,上面这段程序在最新版本的 golang 1.8/1.9 中已经不会 hang 住了(实验结果,没有深究原因)。某次更新说明中官方声称在密集循环中理论上也会让其它的 goroutine 有被调度的机会,那么我们选择相信官方,试一下下面这个程序:

package main

import (
"fmt"
"io"
"log"
"net/http"
"runtime"
"time"
) func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
go server()
go printNum()
var i = 1
for {
// will block here, and never go out
i++
}
fmt.Println("for loop end")
time.Sleep(time.Second * 3600)
} func printNum() {
i := 0
for {
fmt.Println(i)
i++
}
} func HelloServer(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "hello, world!\n")
} func server() {
http.HandleFunc("/", HelloServer)
err := http.ListenAndServe(":12345", nil) if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

运行几秒之后 curl 一发:

curl localhost:12345

感觉还是不要再相信官方了。研究研究之后不小心写出了这样的 bug 怎么定位比较好。首先分析一下这种类型 bug 发生时的程序特征:

1. 卡死在 for 循环上
2. gcwaiting=1
3. 没有系统调用

由于没有系统调用,不是系统调用导致的锅,所以我们没有办法借助 strace 之类的工具看程序是不是 hang 在系统调用上。而 gcwaiting=1 实际上并不能帮我们定位到问题到底出现在哪里。

然后就剩卡死在 for 循环上了,密集的 for 循环一般会导致一个 cpu 核心被打满。如果之前做过系统编程的同学应该对 perf 这个工具很了解,可以使用:

perf top

对 cpu 的使用情况进行采样,这样我们就可以对 cpu 使用排名前列的程序函数进行定位。实际上 perf top 的执行结果也非常直观:

  99.52%  ffff                     [.] main.main
0.06% [kernel] [k] __do_softirq
0.05% [kernel] [k] 0x00007fff81843a35
0.03% [kernel] [k] mpt_put_msg_frame
0.03% [kernel] [k] finish_task_switch
0.03% [kernel] [k] tick_nohz_idle_enter
0.02% perf [.] 0x00000000000824d7
0.02% [kernel] [k] e1000_xmit_frame
0.02% [kernel] [k] VbglGRPerform

你看,我们的程序实际上是卡在了 main.main 函数上。一发命令秒级定位。

妈妈再也不用担心我的程序不小心写出死循环了。实际上有时候我的一个普通循环为什么变成了死循环并不是像上面这样简单的 demo 那样好查,这时候你还可以用上 delve,最近就帮 jsoniter 定位了一个类似上面这样的 bug:

https://github.com/gin-gonic/gin/issues/1086

从 perf 定位到函数,再用 pid attach 到进程,找到正在执行循环的 goroutine,然后结合 locals 的打印一路 next。

问题定位 over。

如何定位 golang 进程 hang 死的 bug的更多相关文章

  1. 内核futex的BUG导致程序hang死问题排查

    https://mp.weixin.qq.com/s/sGS-Kw18sDnGEMfQrbPbVw 内核futex的BUG导致程序hang死问题排查 原创: 王领先 58架构师 今天   近日,Had ...

  2. 一次进程hang住问题分析。。。

    这两天有同学使用数据校验工具时发现进程hang住了,也不知道什么原因,我简单看了看进程堆栈,问题虽然很简单,但能导致程序hang住,也一定不是小问题.简单说明下程序组件的结构,程序由两部分构成,dbc ...

  3. 关于多线程情况下Net-SNMP v3 版本导致进程假死情况的跟踪与分析

    1.问题描述 在使用net-snmp对交换机进行扫描的时候经常会出现进程假死的情况(就是进程并没有死掉,但是看不到它与外界进行任何的数据交互).这时候不知道进程内部发生了什么,虽然有日志信息,但进程已 ...

  4. [转]gdb结合coredump定位崩溃进程

    [转]gdb结合coredump定位崩溃进程 http://blog.sina.com.cn/s/blog_54f82cc201013tk4.html Linux环境下经常遇到某个进程挂掉而找不到原因 ...

  5. 分析java进程假死状况

    摘自: http://www.myexception.cn/internet/2044496.html 分析java进程假死情况 1 引言 1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常 ...

  6. 通过 profiling 定位 golang 性能问题 - 内存篇 原创 张威虎 滴滴技术 2019-08-02

    通过 profiling 定位 golang 性能问题 - 内存篇 原创 张威虎 滴滴技术 2019-08-02

  7. 关于用strace工具定位vrrpd进程有时会挂死的bug

    只做工作总结备忘之用. 正在烧镜像,稍总结一下进来改bug遇到的问题. 一个项目里要用到L3 switch的nat,vrrp功能,但实地测试中偶然出现write file挂死的情况,但不是必现.交付在 ...

  8. debug实战:进程Hang+High CPU

    最近几周都在解决程序不稳定的问题,具体表现为程序(多进程)时不时的Hang住,同时伴随某个进程的High CPU.跟踪下来,基本都是各种死锁引起的.这里选取一个典型的场景进行分析. 1.抓dump分析 ...

  9. 使用perf工具导致系统hang死的原因

    [perf工具导致系统hang住的原因是触发了低版本kernel的bug] 今天在测试服务器做压测,运行perf record做性能分析时,系统再次hang住了,这次在系统日志中记录了一些有用的信息, ...

随机推荐

  1. php 对接微信接口 {"errcode":41001,"errmsg":"access_token missing hint

    这里是针对所有token微信都有这种机制 1.token被多次访问无效 访问微信接口->得到token,缓存起来2小时内有效,期间2小时内每次都取缓存即可,不必每次都去微信那边兑换 问题:缓存期 ...

  2. mysql 大小写不敏感

    lower-case-table-names=1 变量lower-case-table-names的取值 取值范围有三个,分别是0.1.2. 1. 设置成0:表名按你写的SQL大小写存储,大写就大写小 ...

  3. Running MYSQL 5.7 By Bash On Ubuntu On Windows:ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

    root@PC-RENGUOQIANG:/usr/sbin# /etc/init.d/mysql start * Starting MySQL database server mysqld [ OK ...

  4. 【小实现】css after+border实现标签半菱形

    <!DOCTYPE html> <html lang="en"> <head> <style> .span-line-begin { ...

  5. elementui---表格拖动排序的问题

    刚刚用elementui的表格,需要用到一个拖动排序的需求,简单弄了下,使用 Sorttable 来做还是挺快的,但是发现一个问题,拖动排序显示不正常. <el-table :data=&quo ...

  6. scrapy入门案例

    一. 新建项目(scrapy startproject) 在开始爬取之前,必须创建一个新的Scrapy项目.进入自定义的项目目录中,运行下列命令: scrapy startproject scrapy ...

  7. FineReport简单部署

    一.部署方式 1.官网发布包部署 2.自定义tomcat部署 二.发布包部署 1.下载一个发布包:https://www.finereport.com/product/download 解压后打开bi ...

  8. 锐捷交换机如何配置远程管理地址(telnet)

    基本命令如下: hostname(config)#username admin password 123456  ------>telnet 登录账号为admin密码为123456 hostna ...

  9. Ubuntu部署Docker容器环境

    1.首先切换到root用户 2.安装网卡报错 解决办法,删除锁住的文件: 再次安装成功. 4.ubuntu下面安装:apt-get install openssh-server  安装远程工具 5.设 ...

  10. Java之布尔运算

    对于布尔类型boolean,永远只有true和false两个值. 布尔运算是一种关系运算,包括以下几类 比较运算符:>,>=,<,<=,==,!= 与运算 && ...