Go 第三方库引起的线上问题、如何在线线上环境进行调试定位问题以及golang开发中各种问题精华整理总结。
01 前言
在使用 Go 语言进行 Web 开发时,我们往往会选择一些优秀的库来简化 HTTP 请求的处理。其中,go-resty 是一个被广泛使用的 HTTP 客户端。正是因为使用了 go-resty 库,才有今天想要分享的主题:go-resty 在处理 HTTP 307/308 重定向状态码时的问题及其解决方案。
02 HTTP 状态码
相信大家都听过 HTTP 302 状态码,但是否都了解过 HTTP 307/308 状态码。让我们分别看看 HTTP 302、307 和 308 状态码的定义及其之间的区别:
-
HTTP 302:
名称:Found 或 Temporary Redirect。
描述:这是一个临时重定向,意味着请求的资源已经临时移动到了一个新的 URL。此状态码表示原始的请求方法应该保留使用于新的 URL。但在实际应用中,很多客户端,特别是浏览器,可能会将 POST 转换为 GET。
应用:在实际应用中,很多时候 302 重定向被用于 URL 短链接、登录状态验证等场景。 -
HTTP 307:
名称:Temporary Redirect。
描述:和 HTTP 302 类似,HTTP 307 也表示资源已经临时移动到另一个 URL。但与 302 的主要区别是,HTTP 307 明确规定客户端必须不改变请求方法进行重定向。例如,如果原始请求是 POST,那么重定向请求也必须是 POST。
应用:由于 307 更加明确,如果需要确保请求方法在重定向时不被更改,应使用 307 而不是 302。 -
HTTP 308:
名称:Permanent Redirect。
描述:与 HTTP 301 (Permanent Redirect) 类似,HTTP 308 表示请求的资源已经被永久移动到了新的 URL。但与 301 的区别在于,308 明确规定客户端必须保持原始请求的方法不变。例如,POST 请求在 308 重定向后仍应为 POST。
应用:当资源永久移动且希望确保请求方法不被更改时,应使用 308。
主要区别:
永久性 vs. 临时性:302 和 307 是临时重定向,而 308 是永久重定向。
请求方法的保留:302 不保证原始请求方法在重定向后仍会被保留;307 和 308 保证在重定向后请求方法不变。
03 项目背景
由于后台服务多区域部署,需要涉及到2个以上的环境数据打通,当请求到达 A 区域时,发现数据在 B 区域,会使用 HTTP 307 进行临时重定向,跳转到 B 区域。
正常请求是上面这样子的,下面来看看 go-resty 引起的问题:
当多个请求同时访问区域 A 时,存在请求 A 需要 307 重定向,而请求 B 也同时访问了区域 A,此时,由于 go-resty 重用缓存区导致的重定向请求 A 拿到了 请求 B的数据。
04 issue
go-resty 最新版本是tag:v2.7.0,这个版本已经是2021年11月份发布的,而在2022年5月份,就有人在 github 上提出了这个 issue,并进行了修复,最终也合并到了 master 主分支了,然而至今并没有发布新版本,这就导致线上出了这个重定向问题。
修复错误重用缓冲区导致的重定向请求正文与源请求正文不匹配
https://github.com/go-resty/resty/pull/568
下面,我们来看看这个 mr 修复了哪里的代码
可以看到,原本直接通过 bytes.NewBuffer(r.bodyBuf.Bytes()) 直接返回了,修复的方式是通过 io.Copy 来复制一个新的缓存区,解决重用的问题。
最终提交的代码也编写了单元测试来验证结果是否准确:
func TestPostRedirectWithBody(t *testing.T) {
ts := createPostServer(t)
defer ts.Close()
targetURL, _ := url.Parse(ts.URL)
t.Log("ts.URL:", ts.URL)
t.Log("targetURL.Host:", targetURL.Host)
c := dc()
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := c.R().
SetBody([]byte(strconv.Itoa(newRnd().Int()))).
Post(targetURL.String() + "/redirect-with-body")
assertError(t, err)
assertNotNil(t, resp)
}()
}
wg.Wait()
}
05 总结
当线上环境出现 307 重定向请求且并发量急剧变大时,同一时间出现碰撞的概率就变大了,就出现了串数据的情况,这是非常严重的问题。
总结Golang编程时常见的错误和问题:
unrecognized import path "golang.org/x/…
golang 在 github 上建立了一个镜像库,下载github上的镜像库放入GOPATH下即可
mkdir -p $GOPATH/src/golang.org/x
cd $GOPATH/src/golang.org/x
git clone https://github.com/golang/sync.git
git clone https://github.com/golang/crypto.git
git clone https://github.com/golang/sys.git
go包管理代理网址无法访问:proxy.golang.org
解决:
go env -w GOPROXY=https://goproxy.cn
verifying module: invalid GOSUMDB: malformed verifier id
解决:
关闭包验证
go env -w GOSUMDB=off
设置go env -w GOPROXY=***warning: go env -w GOPROXY=… does not override conflicting OS environment variable
解决方案:
unset GOPROXY
再重新设置即可
类型不匹配
在 Golang 中,类型的匹配是至关重要的。关于 Golang 类型匹配的一个经典错误是在比较两个不同类型的变量。例如,将字符串类型的变量与整数类型的变量进行比较。要避免这种错误,应该始终确保比较的是相同类型的变量。或者,在进行类型转换时,需确保变量类型符合要求。
空指针错误
在 Golang 中,指向空值的指针是非常常见的错误。这通常在代码中的资源管理方面出现问题,如打开和关闭文件,连接到数据库等。当处理指针变量时,应该始终确保其指向一个有效的对象。此外,对于指针变量的使用应该小心谨慎,以避免出现在编程过程中不同的指针指向同一个对象的情况。
内存分配错误
内存分配是计算机程序所必需的一项任务,但是在 Golang 中,这个任务可能会出现问题。最常见的错误是未能释放内存。在使用 Golang 中的 new() 或 make() 函数的时候,应该注意释放内存,以确保程序不会因为内存泄漏而崩溃。
大规模并发处理问题
在 Golang 中,大规模的并发处理是其一个显著的特点,但这也带来了一些问题。并发程序的错误处理比串行程序更为棘手,因为开发者需要在多个并发执行的地方确定错误。在开发 Golang 并发程序时,应该设计清晰的错误管理方案,并充分利用 Golang 提供的资源管理功能进行并发控制。
死锁
死锁是一个常见的 Golang 错误。大多数死锁发生在程序未能释放锁或加锁太多的情况下。更糟糕的是,用户可能需要重新启动程序来解决死锁的问题。为避免死锁,可以使用并发控制的技术确保子程序无法同时访问相同的资源。
文件处理问题
在 Golang 中,文件处理是经常进行的任务。常见的文件处理错误包括文件无法打开,读取和写入错误。为避免这些错误,应该选择一个完全合适的文件操作函数,并检查文件路径是否正确。同时,应该在解析文件时检查文件的格式,以确保文件中的数据已正确转换。
数组和切片的越界问题
在 Golang 中,数组和切片的越界访问是另一个可能的错误。由于数组和切片的索引是从 0 开始计数的,所以如果开发者在索引时错误地写入负数、超出边界的正数或者不同于整数的索引,就可能出现越界错误。为避免这个问题,开发者可以使用循环来遍历数组和切片,并始终确保数组和切片的索引值是有效的。
总结
以上是 Golang 中常见的错误。在 Golang 的学习过程中,这些错误可能会使新手们不知所措,但只要有足够的耐心和学习态度,并遵循最佳实践,就可以轻松地避免这些错误。
最近线上有个服务突然卡死了,看起来是进程启动了,但是请求接口则反应连接不上,看错误日志也没有什么异常信息,对于这种线上服务卡死的问题,一直没有丰富经验来排查。看网上一些人各自gdb,systemtap之类的工具调试线上问题,很是羡慕。
我先是用 strace -p pid 来看问题,发现有时候又处于wait futex,有时候有正常的刷刷刷一大堆信息。按照我之前的经验,出现wait futex,一般是等待channel出现,如果一直处于wait futex,则说明进程卡死在某个channel上了。但是现在这个卡死的进程并没有一直处于wait futex,说明可能不是卡死在某个channel上,至少还有其他goroutine在正常活动中。
进程没有卡死在wait futex,但是又不能监听端口处理请求,说明要么是在启动阶段,还未到监听端口这一步就出现死锁卡死了,要么就是监听端口的那部分代码出问题了。
以前我只用过strace -p 来判断是否死锁了,更早之前还试过用gdb调试python服务,但是go的服务,该用什么工具,还不太了解,理论上用strace和gdb也能排查到一些问题的,但是这两个工具都无法识别goroutine,调试起来很难。
后来同事介绍了 dlv,也就是delve这款工具,专门为go语言打造的调试工具,可以识别goroutine等。搜了下官方文档和网上的各自介绍文章,写得都不太实用,好多文章就是玩具级别的介绍,实际生产环境中使用很多细节都没说明。但是只能硬着头皮看一点说明介绍就试用一下,不过这种边解决问题边学习的方法,效率还是很高的。最终也是依靠dlv这款工具定位到了死锁之处以及引起问题所在。
安装:
参照官方文档,先执行
go install github.com/go-delve/delve/cmd/dlv@latest
这样子会安装 dlv 到你的GOPATH目录下,里面会生成一个dlv的可执行文件。然后将这个dlv上传到服务器上就可以使用了。
当然也可以先拉取代码,然后在代码目录执行 go build,生成可执行文件。
attach和coredump:
有了dlv之后,第一步就是要让dlv可以接触上卡死的进程,也就是使用 attach指令把dlv附着在卡死的进程上。
首先,使用 ps -ef | grep xxxx 查看卡死的进程的pid,假设卡死的进程pid=1234,然后执行 ./dlv attach 1234 就 可以附着到卡死的进程上了。如果进程是使用非当前用户启动的话,则要加上 sudo 才行。
连接上进程之后,不管三七二十一,要赶紧导出coredump,因为一旦出现什么问题,导致进行死掉或者卡死状态解除了,就无法继续排查调试了。有了coredump,则可以一直慢慢来调试。
在dlv里,执行 dump ~/sample.core , 就会把当前整个进程的状态都记录下来,那样子就可以下载coredump文件,在自己的开发机上逐步调试问题所在,又同时不影响线上服务。
在自己的开发机上查看coredump文件,还需要提供和线上运行程序一致的二进制文件。比如你线上运行的是 v0.22 版本,要么你就直接从线上下载这个二进制文件,要么你可以把代码切换到v0.22,然后进行编译,同时编译参数不能有任何的变化。只有保证二进制文件与coredump对应的完全一致,才可以使用。
假设二进制文件名问:sample_bin,则在开发机上执行 dlv core sample_bin sample.core 就能加载到coredump文件进行排查问题了。
attach和coredump的区别,主要是attach是附着到一个运行中的实例中,是可以执行函数调用,设置断点等操作的,而coredump就只能静态的去查看栈帧、内存等信息了。因此,如果线上出现了类似卡死的故障,先是保存好coredump,如果可以的话,就直接在线上attach调试,这样子会更容易排查问题。
排查:
排查故障,主要用到的就几个指令:
bt,查看栈帧
list,查看当前栈帧运行到的代码
grs,查看当前所有的goroutine列表
gr 4,切换到第四个groutine
print xxx,打印当前栈帧中的变量值
args,打印函数参数值
locals,打印当前栈帧的local 变量
总结处理过程:
grs 查看goroutine列表
优先切换到 gr 1
执行bt指令看栈帧,查找到业务相关代码
frame xxx,切换到指定栈帧
list 查看当前栈帧运行到的代码
print xxx 查看变量值
遇到的问题:
线上调试的时候,想执行 list 查看当前栈帧的代码,出现了类似 Command failed: open /path/to/the/mainfile.go: no such file or directory 的错误。即使下载了coredump文件和二进制到自己的开发机上进行调试,也会出现这个问题,因为二进制是另一个同事编译的,Go的编译默认会带上代码的完整路径,而同事的GOPATH和我是不一样的,这个要怎么解决呢?
终极解决办法,编译参数加上 trimpath:go build -gcflags=-trimpath= G O P A T H − a s m f l a g s = − t r i m p a t h = GOPATH -asmflags=-trimpath= GOPATH−asmflags=−trimpath=GOPATH 这样子编译的代码路径,就会去掉你本人的文件夹路径,使用相对于GOPATH的路径了,别人在你编译的二进制上获取的coredump,也可以正常调试了。
如果编译前没有加上这个指令 ,则可以在 dlv 里执行 : config substitute-path otherPath mypath。
如果这个path弄错了,怎么清除弄错的配置呢?直接执行 config substitute-path otherPath 即可
注意 mypath一定要填绝大路径,使用 ~/ 这样子的路径会有问题。
config配置完之后,如果需要永久的,可以执行 config -save,这样子配置会默认保存在 ~/.config/dlv 目录里面。
不过另一个问题也难以解决,服务器上一般是不会带上Go代码及代码块的,所以在服务器上进行调试的时候,list指令就用不上了,很难排查问题。最终还是只能用coredump在开发机上排查调试?
其实也可以开启调试服务,线上开启,在开发机上用dlv连接线上开启的服务,就可以进行调试了。不过这个也挺麻烦的。
还遇到一个问题,是print变量的时候,如果是字符串变量,好像默认只打印前64个字符,这明显不够,可以通过设置:config max-string-len 99999 让打印的字符串长度加长。此时可以执行 config -save 把这个配置永久保存下来。