使用 go race 排查 protobuf Marshal Panic

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 使用 go race 排查 protobuf Marshal Panic

背景介绍

在斋月节大促压测期间,有一个业务压测发现,有一个RPC调用,在高并发的情况下出现panic,而panic的位置是在微服务框架序列化的位置(proto.Marshal)。但是由于框架在近期没有做什么变更,而且业务最近也没上线什么新的需求。所有的Panic仅在并发量较高的时候偶然出现,并且被框架的recover捕获,因此判定此问题已经在线上很久,只是由于出现的概率较低没有被发现。

示例代码:

2020/05/11 19:56:32 http: panic serving 127.0.0.1:59816: runtime error: index out of range
goroutine 20 [running]:
net/http.(*conn).serve.func1(0xc00015e0a0)
  /usr/local/Cellar/go@1.12/1.12.13/libexec/src/net/http/server.go:1769 +0x139
panic(0x13c1cc0, 0x175ad90)
  /usr/local/Cellar/go@1.12/1.12.13/libexec/src/runtime/panic.go:522 +0x1b5
github.com/cyningsun/go-test/20200508-go-race/pb.encodeVarintPerson(0xc000228ec0, 0x3c, 0x3c, 0x3c, 0x1f, 0x3b)
  /Users/yinhang.sun/Documents/workspace/src/github.com/cyningsun/go-test/20200508-go-race/pb/person.pb.go:146 +0x6a
    ...
  /Users/yinhang.sun/Documents/workspace/src/github.com/cyningsun/go-test/20200508-go-race/pb/person.pb.go:47 +0x5b
github.com/gogo/protobuf/proto.Marshal(0x14af2e0, 0xc0000bc040, 0x1, 0x140e400, 0xc0000bc040, 0xc00015c001, 0x0)
    ...
  /usr/local/Cellar/go@1.12/1.12.13/libexec/src/net/http/server.go:2884 +0x2f4

由于报错的结构体是业务最常用的一个,且报错的接口逻辑较深,通读代码也没有发现明显的点(其实是代码量太大了,很难关注的到)。报错又出现在框架层,panic 调用栈并没有提供任何有效信息。

问题排查

经过背景的一系列分析之后,所有的排查思路都被打断了。好在我们发现报错的错误类型是index out of range,通过阅读proto.Marshal的代码之后发现,此函数分为三步:

siz := info.Size(pb)
b := make([]byte, 0, siz)
return info.Marshal(b, pb, false)
  1. 先获取结构体 Size
  2. 然后按照 Size 创建字节数组
  3. 将数据序列化到字节数组中

因此,合理猜测是再获取 Size之后,Marshal之前,结构体的大小发生了变化。将 protobuf 的类型分类,可以排除固定长度的数值类型,剩下就是变长的 string 类型和 bytes 类型。虽然有了这个推论,由于发生panic的结构体十分复杂,还是很难定位到具体是哪个字段变化导致的。最后没有办法只有使用很笨的方法,修改 vendor 中的 protobuf 代码,添加日志,使用二分法,在info.Size()和info.Marshal()中打印字段的偏移量,并且在proto.Marshal中直接捕获发生panic的协程再打印数据,避免全部打印时panic请求与日志无法对应。然后对比排查,最终定位到了出现问题的字段,然后根据相关字段搜索相关代码,最终定位到了问题所在。

问题反思

修复完问题之后,促使我们反思,难道排查类似的问题一定要如此艰难么,有没有什么通用的方案来排查类似的问题呢。从问题源头出发,该问题本质来说还是数据的协程并发访问题,有了这个结论再看我们手头的已有工具,golang race 工具 出现在眼前。

理论上讲,可以在非生产环境打开 race 参数,辅助定位问题。说做就做,按照问题的原因,编写测试复现的test case

进程缓存

const Max = uint64(1)
type PersonCache struct {
  c *gocache.Cache
}
func NewPersonCache() *PersonCache {
  one := &PersonCache{c:gocache.New(time.Minute, time.Hour)}
  go one.load()
  return one
}
func (p *PersonCache) load() {
  for i:=uint64(0); i < Max; i++{
    r :=  i % 100
    key := strconv.FormatUint(r,10)
    newOne := &pb.Person{
      Id:                   proto.Uint64(r),
      Name:                 proto.String("init Name"),
      Age:                  proto.Uint32(rand.Uint32()),
      Address:   proto.String("init address"),
    }
    p.c.Set(key, newOne, time.Minute)
  }
}
func (p *PersonCache) Get(key string) (*pb.Person,bool) {
  ret, ok := p.c.Get(key)
  if !ok {
    return nil, false
  }
  return ret.(*pb.Person),true
}

并发访问

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const letterLength = len(letterBytes)
var (
  c = cache.NewPersonCache()
)
func randString(n int) string {
  b := make([]byte, n)
  for i := range b {
    b[i] = letterBytes[rand.Intn(len(letterBytes))]
  }
  return string(b)
}
func Write(w http.ResponseWriter, req *http.Request) {
  r := rand.Uint64() % cache.Max
  key := strconv.FormatUint(r, 10)
  p, ok := c.Get(key)
  if !ok {
    return
  }
  p.Name = proto.String(randString(rand.Int()%letterLength))
  time.Sleep(time.Nanosecond)
  p.Address = proto.String(randString(rand.Int()%letterLength))
}
func Read(w http.ResponseWriter, req *http.Request) {
  r :=  rand.Uint64() % cache.Max
  key := strconv.FormatUint(r,10)
  p,ok := c.Get(key)
  if !ok {
    return
  }
  b,_ := proto.Marshal(p)
  w.Write(b)
}
func main() {
  http.HandleFunc("/read", Read)
  http.HandleFunc("/write", Write)
  fmt.Println("server is listening on 8080")
  http.ListenAndServe(":8080", nil)
}

压测脚本

run:
  go run main.go &> normal.log
racerun:
  go run -race main.go &> race.log 
benchmark:
  wrk -t1 -c2 -d30s http://127.0.0.1:8080/read &
  wrk -t1 -c2 -d30s http://127.0.0.1:8080/write &

日志结果

==================
WARNING: DATA RACE
Read at 0x00c000188140 by goroutine 11:
  github.com/cyningsun/go-test/20200508-go-race/pb.(*Person).Size()
...
Previous write at 0x00c000188140 by goroutine 55:
  main.Write()
      /Users/yinhang.sun/Documents/workspace/src/github.com/cyningsun/go-test/20200508-go-race/main.go:39 +0x271
...

通过日志,可以很轻松的发现问题的原因。

总结优化

以上过程促使我们优化非生产环境的运行脚本,通过编译参数控制 -race 参数的开关,当再遇到类似的问题时,可以快速复现、定位、修复。

源码链接:

https://github.com/cyningsun/go-test/tree/master/20200508-go-race

本文作者 : cyningsun

本文地址https://www.cyningsun.com/05-11-2020/proto-marshal-panic.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Golang

  1. 译|There Are No Reference Types in Go
  2. Go 语言没有引用类型,指针也与众不同
  3. 译|What “accept interfaces, return structs” means in Go
  4. 如何用好 Go interface
  5. 一个优雅的 LRU 缓存实现
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
Go
Go - 如何编写 ProtoBuf 插件 (一) ?
Go - 如何编写 ProtoBuf 插件 (一) ?
39 2
|
3月前
|
Go
实验深度理解Go中try...catch...的panic、defer、recover用法
文章通过实验代码演示了Go语言中如何使用panic、defer和recover函数来模拟try...catch...的异常处理机制,并详细解释了每个函数的作用和在异常处理中的使用场景。
35 0
|
3月前
|
Go
Go - 如何编写 ProtoBuf 插件 (三) ?
Go - 如何编写 ProtoBuf 插件 (三) ?
23 1
|
3月前
|
Go
Go - 如何编写 ProtoBuf 插件(二)?
Go - 如何编写 ProtoBuf 插件(二)?
76 0
|
6月前
|
Go 开发者
Golang深入浅出之-Go语言 defer、panic、recover:异常处理机制
Go语言中的`defer`、`panic`和`recover`提供了一套独特的异常处理方式。`defer`用于延迟函数调用,在返回前执行,常用于资源释放。它遵循后进先出原则。`panic`触发运行时错误,中断函数执行,直到遇到`recover`或程序结束。`recover`在`defer`中捕获`panic`,恢复程序执行。注意避免滥用`defer`影响性能,不应对可处理错误随意使用`panic`,且`recover`不能跨goroutine捕获panic。理解并恰当使用这些机制能提高代码健壮性和稳定性。
131 2
|
6月前
|
Go
Go语言中的异常处理:理解panic与recover
【2月更文挑战第7天】Go语言虽然以简洁和直接错误处理机制而著称,但它也提供了`panic`和`recover`这两个内置函数来处理程序中的异常情况。本文将深入探讨Go语言中的异常处理机制,包括`panic`和`recover`的使用场景、原理以及最佳实践,帮助读者更好地理解如何在Go中处理异常情况。
|
6月前
|
网络协议 BI Go
Go-异常处理(defer recover panic)
Go-异常处理(defer recover panic)
75 0
|
编译器 Go
Go学习笔记-defer、panic、recover分析
Go学习笔记-defer、panic、recover分析
84 1
Go学习笔记-defer、panic、recover分析
|
JSON Linux 测试技术
go语言处理数据、基本通信以及环境配置 -- json,protobuf,grpc
go语言处理数据、基本通信以及环境配置 -- json,protobuf,grpc
|
11月前
|
存储 Cloud Native Go
Go 语言中 panic 和 recover 搭配使用
Go 语言中 panic 和 recover 搭配使用