前情提要
记录下 dlv 的远程调试,建议不要在代码里加 fmt 去调试。不谈 goland 啥的远程调试,本文章目前只写 dlv 的命令行配合远端调试。
一些前提须知
符号链接路径
1 | package main |
上面代码你编译了后,在其他机器上运行,panic 的堆栈信息会是你机器上的路径信息,路径信息是保留的,例如下面的是我在 windows 上交叉编译仍到 Linux 上执行的:
1 | panic: runtime error: invalid memory address or nil pointer dereference |
可以通过下面的编译选项去掉(项目路径也就是$PWD
显示的不要带空格,否则会编译报错):
1 | go build -gcflags="all=-trimpath=$PWD -N -l" -asmflags "all=-trimpath=$PWD" main.go |
使用上面的参数编译完后的,这里注意下下面的 D:/Install/Go
,后面文章会用到。
1 | panic: runtime error: invalid memory address or nil pointer dereference |
dlv 命令行远端调试
很多时候线上机器都是 Linux ,源码在本地,而且机器上不一定会有 golang,也就是说 dlv debug main.go
满足的条件实际上并不多。这里主要讲下 dlv exec
和 dlv attach
。
exec 是用 dlv 运行编译完的二进制文件,golang 关闭 cgo 编译的就是静态编译了,运行机器上有无 golang 都能运行。attach 是调试一个已经运行的进程。
这里我是在linux上运行一个编译好的 gin-demo,然后在目标机器上准备一个 dlv 的二进制文件,用 dlv attach
开一个 server,然后我们在本地有源码的 windows 上 dlv connect
连上去调试。
这里以我本地的 windows 和 远端的 Linux 做测试。windows 上和 linux 上都已经安装了 dlv 了,并把路径加到 PATH 里了,windows 我下了 git bash。
demo
根据实际开发流程来,windows 上项目编辑文件 main.go
,代码随便写的,不要吐槽。
1 | package main |
1 | go mod init test |
然后推送到代码仓库上,手动或者 robot 触发 CI 构建
ci 的编译
Dockerfile 如下,为了避免 dlv 调试出现 Warning: debugging optimized function
,我们需要在 -gcflags=
里加 -N -l
,为了防止变量被 Dockerfile
解析,$PWD
全部换成了pwd
。${LDFLAGS}
是注入一些 version 信息之类的,可以看我文章git工作流下golang项目的version信息该如何处理
1 |
|
然后编译完了,模拟下 CD 部署在 Linux 机器上运行
1 | docker run --name t1 --rm -p 8080:8080 test |
这里我用容器演示,虽然容器的 pid namespaces 隔离了,但是实际上容器里的所有进程还是会在宿主机上有 pid 对应的。
1 | $ ps aux | grep gin-dem[o] |
Linux 上的准备
拷贝 Linux 的 dlv 可执行文件放到目标机器的 PATH 下,推荐按照 LFS 的规范放 /usr/local/bin/ 下,可以先通过 go install 安装,1.16 后者高于 1.16 版本:
1 | export GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,https://goproxy.io,direct |
http 接口之类的 server 类调试
比如我这个 demo 就是要调试接口内部的,也就是运行后触发的主要运行段是在接口内,所以我们不需要 dlv run
, 而是下面这样 attach 执行
1 | dlv attach $(pgrep gin-demo) --listen=:2345 --headless=true --log=true \ |
$(pgrep gin-demo)
也可以换成具体的 pid 。
非接口类服务容器内调试
可能我们的服务一开始就要打断点,也就是 main 开始之类的地方就打断点,用 attach 就不现实了。我们的思路是先把容器起来,然后把 dlv 拷贝进去,dlv exec
执行二进制文件
如果是 docker-compose
可以把容器的命令替换了,类似下面
1 | ... |
k8s 的话也和上面差不多,然后把探针啥的给取消或者 initDelay 调久点。下面我用 docker run
模拟容器下操作,需要添加 ptrace
权限,否则 dlv 执行会报错 could not launch process: fork/exec /gin-demo: operation not permitted
:
1 | $ docker run --name t1 --rm -tid --cap-add=SYS_PTRACE --entrypoint sh test |
容器里执行 dlv ,注意添加 --allow-non-terminal-interactive
1 | docker exec -ti t1 \ |
,然后在另一个 tty 窗口我们利用 socat 转发成宿主机的端口,没有就安装下。开启 socat 转发:
1 | socat -d -d TCP-LISTEN:2345,reuseaddr,fork,bind=0.0.0.0 TCP:172.17.0.2:2345 |
本机上开始调试
和 os 无关,我这里是 windows 而已,dlv 也要放在 PATH 里,或者绝对路径运行 dlv。要在源码目录运行,因为 docker build
里容器编译的时候去掉源码的前缀路径,所以 dlv 调试按照链接找文件都会按照相对路径找,所以我们需要在windows 上的源码路径里执行。
1 | dlv connect 192.168.2.111:2345 |
连上之后,没有 Linux 的(dlv)
这样的 format,这是 dlv 引用的库在 windows 上的 bug,只要出现了Type 'help' for list of commands.
说明成功连上了。
执行下 sources
看下链接路径:
1 | /usr/local/go/src/... |
前面的是 golang 的内部包,最后面的是我们项目路径的,因为我们 build 的时候 trim 掉前缀路径了,所以看着都是相对路径。/usr/local/go
是 golang 的 docker 镜像里的 GOROOT
,我们本地 windows 的 go root 可以通过 cmd 或者 git bash 里使用下面的命令查看:
1 | $ go env GOROOT |
源码包已经相对路径存在了,但是 golang 的自带包还没有,我们需要配置映射路径,接着在 dlv 的交互里执行下面的:
1 | config substitute-path /usr/local/go D:\Install\Go |
然后我们在 main.go:10
打个断点(也可以包名.方法内的相对第几行打断点),然后 c
执行:
1 | b main.go:10 |
然后我们触发下请求,例如在 Linux 上直接用 curl 触发请求:
1 | curl localhost:8080/ping |
命令会阻塞住,因为 server 端没回复,我们回到我们的 windows 上,界面有打印下面的。
1 | > main.main.func1() main.go:10 (hits goroutine(33):1 total:2) (PC: 0x891be0) |
我们打印下 count 的值
1 | p count |
然后 c,让代码 continue 。然后能看到我们的 Linux 上的 curl 命令收到 server 端的请求了。
dlv 里常用的命令如下:
1 | config max-string-len 1000 # 配置打印变量的输出长度,防止被折叠显示 |
dlv 运行的时候会读取配置文件路径,像上面的config max-string-len 1000
和 config substitute-path /usr/local/go D:\Install\Go
都可以配置在文件里。