likes
comments
collection
share

【震惊】什么?野生GoRoutine发生panic也能Recover?

作者站长头像
站长
· 阅读数 44

序幕

这已经是老张这周背的第三个 P0 事故了。


在互联网深耕多年,老张一直靠着丰富的 CV (首先排除Computer Vision) 技巧、对轮子过目不忘的能力,倚仗一大堆搜刮来的 go mod 包,能够在 1 分钟内完成任意复杂度的需求。

这也为老张在公司里卷得了一席之地。没想到今天阴沟里翻船:

某个三方的 SDK 包里,居然起了一个野生的 Go Routine 。

func throwError() {
   go func() {
      panic("...")
   }()
}

咋回事

众所周知,goroutine 不像其它语言的线程,没有父子关系。


任何一个 goroutine 一旦 panic 了没有 recover ,迎来的就是整个进程的 Crash 。

服务进程就像个备胎,被守护进程反复拉起、挂掉、拉起、挂掉....公司收入以肉眼可见的速度下跌。

老张看着眼前的三方包陷入了沉思...

...

也就在10分钟之后,老张心里暗暗有了主意。

走进Panic

要 cover 野生 goroutine 的 panic ,我们就得先了解 panic 。


第一步

当你的业务代码发生 panic 的时候,~~那么意味着你的代码执行了 panic ~~。在代码层面,其实就是runtime.gopanic(),我们把将这类 panic 称为显式 panic 。

那么隐式 panic 呢?例如空指针,类型转换异常...哦抱歉,没有隐式 panic ,所有的这些异常都是通过 golang 的内置库显式 panic 的,例如类型转换异常:

【震惊】什么?野生GoRoutine发生panic也能Recover?

第二步

runtime.gopanic 中,会首先排除四种不可恢复的panic,直接抛出fatal。分别是:过年不回家的、

  • 系统栈上发生 panic
  • 内存分配时 panic
  • 禁止抢占发生 panic
  • g 锁在 m 上时发生 panic

第三步

然后就开始骚操作,尝试恢复,伪代码如下:

  1. g._defer 中的 fn(defer方法) ,取出来执行。
  2. 如果 fn 执行过程中出现了 recover,那么 g._panic.recovered 会被改成true,同时会记下 recover 方法的 callerspcallerpc
  3. 如果没有被恢复,那就通过 _defer.link 取出下一个要执行的 defer。继续 2 。
  4. 当所有 defer 都被执行完,或者 g._panic.recoveredtrue 的情况下,会搞一些inline优化,处理一些堆上的frame,然后:如果被恢复,就 gogo(刚才存的sp和pc); 如果没被恢复,那就 fatalpanic()

【震惊】什么?野生GoRoutine发生panic也能Recover?


卷动多年,老张从来没向困难低过头。赵鹏都能踢中卫,还有什么是不可能的。 --- 老张暗暗开始较劲

要想拦截任意 goroutinepanic ,没点黑科技是不行的。fatalpanic() 要开摆,我们就搞它!

搞定fatalpanic()

老张依稀想起了自己夕阳下的奔跑,那是他逝去的青春自己在淘宝开店卖 Github star 的时候,有一个顾客以2元钱一个 star 的价格买了200个 star ,这一单让老张赚到了人生的第一桶金。老张现在依然还记得这个库名字,gohook

来看看官网的介绍:

  • gohook will find go function address,then insert some NB jump code, let execute process redirect to a new good function。
  • gohook 会找出 go 函数的地址,然后插入一些很屌的跳转指令,将执行流程重定向到新函数。

问题开始变得简单起来了,我们只要让代码执行我们自制的 fatalpanic() 不就好了嘛。

搞定私有方法

runtime.fatalpanic() 开头的 f,就注定了这个方法无法被我们调用,更别说获取地址了。焦灼之际,老张打开了百度:

【震惊】什么?野生GoRoutine发生panic也能Recover?

通过 go:linkname 让编译器直接把 runtime.fatalpanic() 导出。

//go:linkname fatalpanic runtime.fatalpanic
func fatalpanic(_msgs *_panic)

搞定编译

什么?编译失败? 老张望着眼前的红字,气的发抖。又苦苦研习了一番:

  1. Golang就是矫情,用都用了,还非得你引入个 unsafe 包,提醒自己一下。这是高危行为,这样不可以哦。

  2. 因为go build默认加会加上-complete参数, fatalpanic 没有方法体会编译不通过。加个空的 x.s 文件,让编译器以为是cgo,可绕开这个限制。

【震惊】什么?野生GoRoutine发生panic也能Recover?

见证奇迹的时刻

【震惊】什么?野生GoRoutine发生panic也能Recover?

  • 可以看到第25行这个看似无药可救的 panic ,被轻松拦截。
  • 另一个 goroutine ,仍然正常打印时间~

附录

Q: 老张是不是作者你?

A: 每个人都是老张

Q: 为什么选择结束 goroutine ,而不是让发生错误的函数返回空?

A: 由于 runtime.gopanic() 中根据 callerspcallerpc 恢复现场,所以自定义的 defer 植入后虽然能恢复异常,但是由于 sp 暂时没有办法获取,即使强改 pc 回跳 panic 发生的地方,仍然会存在函数返回时找不到方法栈的问题 coredump 。所以,结束 GoRoutine 看起来是一种既安全,又能满足诉求的处理方式。

Q: 如何深入了解 panic 与 recover 原理?

panic.go:958 行开始看,配合下面两篇博客食用。光速掌握panic核心。