关于golang:终于解决了这个线上偶现的panic问题

43次阅读

共计 3805 个字符,预计需要花费 10 分钟才能阅读完成。

来自公众号:Gopher 指北

不晓得其他人是不是这样,反正老许最怕听到的词就是“偶现”,至于起因我不多说,懂的都懂。

上面间接看 panic 信息。

runtime error: invalid memory address or nil pointer dereference

panic(0xbd1c80, 0x1271710)
        /root/.go/src/runtime/panic.go:969 +0x175
github.com/json-iterator/go.(*Stream).WriteStringWithHTMLEscaped(0xc00b0c6000, 0x0, 0x24)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/stream_str.go:227 +0x7b
github.com/json-iterator/go.(*htmlEscapedStringEncoder).Encode(0x12b9250, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/config.go:263 +0x45
github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002e9c8d0, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:110 +0x78
github.com/json-iterator/go.(*structEncoder).Encode(0xc002e9c9c0, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:158 +0x3f4
github.com/json-iterator/go.(*structFieldEncoder).Encode(0xc002eac990, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:110 +0x78
github.com/json-iterator/go.(*structEncoder).Encode(0xc002eacba0, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_struct_encoder.go:158 +0x3f4
github.com/json-iterator/go.(*OptionalEncoder).Encode(0xc002e9f570, 0xc006b18b38, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect_optional.go:70 +0xf4
github.com/json-iterator/go.(*onePtrEncoder).Encode(0xc002e9f580, 0xc0096c4c00, 0xc00b0c6000)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect.go:219 +0x68
github.com/json-iterator/go.(*Stream).WriteVal(0xc00b0c6000, 0xb78d60, 0xc0096c4c00)
        /go/pkg/mod/github.com/json-iterator/go@v1.1.11/reflect.go:98 +0x150
github.com/json-iterator/go.(*frozenConfig).Marshal(0xc00012c640, 0xb78d60, 0xc0096c4c00, 0x0, 0x0, 0x0, 0x0, 0x0)

首先我深信一条,开源的力量值得信赖。因而老许第一波操作就是,剖析业务代码是否有逻辑破绽。很显著,共事也是值得信赖的,因而果决猜想是某些未曾构想到的数据触发了边界条件。接下来就是保留现场的惯例操作。

如题目所说,这是偶现的 panic 问题,因而依照下面的分类采纳合乎以后技术栈的办法保留现场即可。接下来就是坐等播种的节令,而这一等就是好多天。两头数次收到告警,却没有合乎预期的现场。

这个时候我不仅不慌,甚至还有点小冲动。某某曾曰:“要敢于质疑,敢于挑战权威”,一念至此便一发不可收拾,我老许又要为开源事业做出奉献了嘛!说干就敢干,怀着小心理开始浏览 json-iterator 的源码。

刚开始研读我便明确了一个情理,“当上帝关了这扇门,肯定会为你关上另一扇门”这句话是骗人的。老许只感觉上帝不仅关上了所有的门甚至还关上了所有的窗。上面咱们看看他到底是怎么关门的。

func (cfg *frozenConfig) Marshal(v interface{}) ([]byte, error) {stream := cfg.BorrowStream(nil)
    defer cfg.ReturnStream(stream)
    stream.WriteVal(v)
    if stream.Error != nil {return nil, stream.Error}
    result := stream.Buffer()
    copied := make([]byte, len(result))
    copy(copied, result)
    return copied, nil
}


// WriteVal copy the go interface into underlying JSON, same as json.Marshal
func (stream *Stream) WriteVal(val interface{}) {
    if nil == val {stream.WriteNil()
        return
    }
    // 省略其余代码
}

依据 panic 栈晓得是因为空指针造成了 panic,而 (*frozenConfig).Marshal 函数外部曾经做了非空判断。到此,老许真的曾经别无他法只得战略性放弃解决此次 panic。毕竟,这个影响也没那么大,而且程序员哪有修的完的 bug 呢。通过这样一番刺激,心里的确容易接受多了。

事实上,在较长一段时间内我都无意识地疏忽这个问题,毕竟没有找到问题的根因。这个问题在线上始终继续到一个说不上来什么日子的日子,总而言之就是兴致来了,我再次看了两眼,而这两眼很要害!

func doReq() {req := paramsPool.Get().(*model.Params)
    // defer 1
    defer func() {reqBytes, _ := json.Marshal(req)
        // 省略其余打印日志的代码
    }()
    // defer 2
    defer paramsPool.Put(req)
    // req 初始化以及发动申请和其余操作
}

注:

  1. 上述代码变量命名曾经被老许通用化解决。
  2. 我的项目中理论代码远比上述简单,但上述代码仍旧是造成本次问题的最小原型。

下面代码中 paramsPoolsync.Pool类型的变量,而 sync.Pool 想必大家都很相熟。sync.Pool是为了复用曾经应用过的对象(协程平安),缩小内存调配和升高 GC 压力。

type test struct {a string}

var sp = sync.Pool{New: func() interface{} {return new(test)
    },
}

func main() {t := sp.Get().(*test)
    fmt.Println(unsafe.Pointer(t))
    sp.Put(t)
    t1 := sp.Get().(*test)
    t2 := sp.Get().(*test)
    fmt.Println(unsafe.Pointer(t1), unsafe.Pointer(t2))
}

根据上述代码和输入后果知,t1变量和 t 变量地址统一,因而他们是复用对象。此时再回顾下面的 doReq 函数就很容易发现问题的根因。

defer 2defer 1 程序反了!!!

defer 2defer 1 程序反了!!!

defer 2defer 1 程序反了!!!

sync.Pool提供的 GetPut办法是协程平安的,然而高并发调用 doReq 函数时 json.Marshal(req) 和申请初始化会存在并发问题,极有可能引起 panic 的并发调用工夫线如下图所示。

既然曾经找到起因,解决起来就容易多了,只需调整 defer 2defer 1的调用程序即可。老许将批改后的代码公布到线上后也的确再没有呈现 panic。造成这次事变的根本原因是一个微不足道的细节,所以咱们平时在开发中还是要审慎加审慎,防止因为这种小白谬误造成不可挽回的损失。另外一个经验之谈就是,开发和查问题时尽量不要钻牛角尖,适当的进展可能会有意想不到的奇效。

最初,衷心希望本文可能对各位读者有肯定的帮忙。

正文完
 0