这个坑比较新鲜,刚填完,还冒着冷气。


- 1 -

在字节跳动,我们服务的所有 log 都通过统一的日志库采集到流式日志服务、落地 ES 集群,配上字节云超(sang)级(xin)强(bing)大(kuang)的监控能力,每一条 panic log 都可以触发一个打给值班同学的电话。

所以我们常常不选电话,只选飞书 ↓↓↓



但毕竟是 panic,大部分 case 都会迅速被就地正法,除了少数排查费劲、又不对线上产生太大影响的,比如这一个:

Error: invalid memory address or nil pointer dereferenceTraceback:goroutine 68532877 [running]:...src/encoding/json/encode.go:880 +0x59encoding/json.stringEncoder(0xcb9fead550, ...)...src/encoding/json/encode.go:298 +0xa5encoding/json.Marshal(0x1ecb9a0, ...).../path/to/util.SendData(0xca813cd300)

注:为了方便阅读,略有简化。

你看,它可以被 recover 兜住(不会把服务搞挂),而且出现频率很低(每天几次甚至没有),考虑到在每天数百亿请求中的占比,解决它的 ROI 实在太低,所以就耽搁了一段时间 且不用担心背 P0 的锅。


- 2 -

其实之前 S 同学和我都关注过这个 panic ,从上面的 Error log 可以看到,错误发生在调用 json.Marshal 的时候,调用方的代码大概长这样:

func SendData(...) {  data := map[string]interface{} {    "code":    ctx.ErrorCode,    "message": ctx.Message,    "step":    ctx.StepName,  }  msg, err := json.Marshal(data)  ...}

注:实际map有更多key/value,这里略作简化。

看这代码,第一反应是:这**也能 panic ?

找到对应的 json 库源码(encode.go第880行,对应下面第5行):

func (e *encodeState) string(s string, escapeHTML bool) {  e.WriteByte('"')  start := 0  for i := 0; i < len(s); {    if b := s[i]; b < utf8.RuneSelf {      ...

—— 也只是从string里逐个读取字符,看着并没什么猫饼。

由于 panic 发生在官方 json 库里,不适合修改并部署到全量机器;引入第三方 json 库又涉及很多依赖问题,所以当时没再跟进。

直到最近 panic 频率逐渐升高, H 和 L 同学实在看不下去了。


- 3 -

L 同学的思路是,既然这个 panic 能被 recover 兜住,那为什么不看看 panic 时这个 map 里装了什么呢?

于是代码就变成了这样:

defer func() {  if p := recover(); p != nil {    log.Warnf("Error: %v, data: %v", p, data)  }}()data := map[string]...

然后 panic 顺利转移到了 log.Warnf 这一行 


- 4 -

不管怎么说成功地转移了问题,只要把 log.Warnf 这一行注释掉……

作为一个追求极致的 ByteDancer,L 同学抵制住了诱惑并尝试了新的思路,既然从 panic log 看到是跪在了一个 string 上,那至少先看看是哪一个string:

data := make(map[string]interface{})defer func() {  if p := recover(); p != nil {    for k, v := range data {      log.Warnf("CatchMe: k=%v", k)      log.Warnf("CatchMe: v=%v", v)    }  }}()...

改起来倒是很简单,赶在这个 需要上班的 周日下午发了车,晚上就捉到了一个case。

通过线上 log,我们发现错误出现在 "step" 这个 key 上(log里有输出key、但没输出value),value 本应是 ctx.StepName 这个 string。

可是 string 这种看起来人畜无害的 immutable 的 type 为什么会导致 panic 呢?


- 5 -

通过走读代码得知,在遇到异常的时候,我们会往 ctx.StepName 写入这个异常点的名称,就像这样:

const STEP_XX = "XX"func XX(...) {  if err := process(); err != nil {    ctx.StepName = STEP_XX  }}

一边读一边写,有那么点并发的味道了。

考虑到我们为了降低媒体感知的超时率,将整个广告的召回流程包装成一个带时间限制的任务:

finished := make(chan struct{})timer := time.NewTimer(duration)go recall(finished)select {  case     sendResponse()  case     sendTimeoutResponse()}

因此在一个请求流程中,确实可能会出现并发读写 ctx.StepName 这个 string object 的情况。

但如何实锤是这儿挖的坑呢?


- 6 -

在线上服务中直接验证这一点不太容易,但是 H 同学做了一个简单的 POC,大概像这样:

const (  FIRST  = "WHAT THE"  SECOND = "F*CK")func main() {  var s string  go func() {    i := 1    for {      i = 1 - i      if i == 0 {        s = FIRST      } else {        s = SECOND      }      time.Sleep(10)    }  }()  for {    fmt.Println(s)    time.Sleep(10)  }}

代码一跑起来就有点味道了:

$ go run poc.goWHAT THEF*CK...WHATWHATWHATF*CKGOGC...

虽然没看到 panic,但是确实看到了点奇怪的东西(严正声明:不是故意要吐槽GO的GC)。

再用 go 的 race detector 瞅瞅:

$ go run -race poc.go >/dev/null    ==================WARNING: DATA RACEWrite at 0x00c00011c1e0 by goroutine 7:  main.main.func1()    poc.go:19 +0x66(赋值那行)Previous read at 0x00c00011c1e0 by main goroutine:  main.main()    poc.go:28 +0x9d(println那行)

这下可算是实锤了。


- 7 -

那么为什么 string 的并发读写会出现这种现象呢?

这就得从 string 底层的数据结构说起了。在 go 的 reflect 包里有一个 type StringHeader ,对应的就是 string 在 go runtime的表示:

type StringHeader struct {    Data uintptr    Len  int}

可以看到, string 由一个指针(指向字符串实际内容)和一个长度组成。

比如说我们可以这么玩弄 StringHeader:

s := "hello"p := *(*reflect.StringHeader)(unsafe.Pointer(&s))fmt.Println(p.Len)

对于这样一个 struct ,golang 无法保证原子性地完成赋值,因此可能会出现goroutine 1 刚修改完指针(Data)、还没来得及修改长度(Len),goroutine 2 就读取了这个string 的情况。

因此我们看到了 "WHAT" 这个输出 —— 这就是将 s 从 "F*CK" 改成 "WHAT THE" 时,Data 改了、Len 还没来得及改的情况(仍然等于4)。

至于 "F*CKGOGC" 则正好相反,而且显然是出现了越界,只不过越界访问的地址仍然在进程可访问的地址空间里。


- 8 -

既然问题定位到了,解决起来就很简单了。

最直接的方法是使用 sync.Mutex:

func (ctx *Context) SetStep(step string) {  ctx.Mutex.Lock()  defer ctx.Mutex.Unlock()  ctx.StepName = Step}

Mutex 性能不够好(lock does not scale with the number of the processors),对于这种读写冲突概率很小的场景,性能更好的方案是将 ctx.StepName 类型改成 atomic.Value,然后

ctx.StepName.Store(step)

注:也可以改成 *string 然后使用 atomic.StorePointer

实际上,Golang 不保证任何单独的操作是原子性的,除非使用 atomic 包里提供的原语或加锁


- 9 -

大结局:周一下午 H 同学提交了修复代码并完成发布,这个 panic 就再没出现了。

总结一下:

  • string 没有看起来那么人畜无害

  • 并发的坑可以找 -race 帮帮忙

  • 记得使用 mutex 或 atomic

最后留下一个小问题供思考:

这说了半天并没有完全复现 panic,不过文中已经给了足够多的工具,你能想到怎么办吗?


推荐阅读

  • 字节跳动踩坑记#2:Go 服务锁死

学习交流 Go 语言,扫码回复「进群」即可

站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验

Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注

go nil json.marshal 完是null_字节跳动踩坑记#3:Go服务灵异panic相关推荐

  1. 字节跳动瞄准千亿互联网医疗蓝海,张一鸣想靠AI算法当“药神”?

    文章转载自 有牛财经 字节跳动越长越大,临近年关,这家互联网巨头又瞄上了时下火热的"千亿赛道"互联网医疗. 据多家媒体报道称,字节跳动AI Lab(人工智能实验室)位于北京.上海, ...

  2. 字节跳动(今日头条),战斗力为何如此凶猛?| 畅言

    作者 | 岳京杭 责编 | 郭   芮 Python这么火,为什么还不学? https://edu.csdn.net/topic/python115?utm_source=csdn_bw 年前,一位久 ...

  3. 字节跳动面试准备 | 关于字节

    一. 字节使命 激发创造,丰富生活 Inspire Creativity, Enrich Life 二. 字节文化 始终创业 多元兼容 坦诚清晰 求真务实 敢为极致 共同成长 三. 字节旗下产品 3. ...

  4. 多闪付、岁岁通...字节跳动的支付业务终上正轨,但“逐梦金融圈”谈何容易...

    [ 图片来源:人人都是产品经理  所有者:人人都是产品经理 ] 近年来,今日头条兵分多路进军金融业务,但最先踏上正轨的似乎还是支付. 2018年初,曾有消息称,字节跳动(即今日头条母公司)正低调收购武 ...

  5. 字节跳动如何系统性治理 iOS 稳定性问题

    本文是丰亚东讲师在2021 ArchSummit 全球架构师峰会中「如何系统性治理 iOS 稳定性问题」的分享全文. 首先做一下自我介绍:我是丰亚东,2016 年 4 月加入字节跳动,先后负责今日头条 ...

  6. [Golang] json.Marshal问题总结

    1.Quiz 有如下一个例子: package mainimport ("encoding/json""fmt""time" )type R ...

  7. go json.Marshal

    json.Marshal 是 Go 语言中用于将 Go 值转换为 JSON 格式的字符串的函数.它接受一个 Go 值作为参数,并返回转换后的 JSON 格式的字符串和一个错误值.例如: package ...

  8. golang json Marshal遇到的字符转义

    使用json.Marshal 如果字符串中含有 < > &字符会出现自动转义问题 不太符合预期输出 原始字符 转义后 &         \u0026 < \u003 ...

  9. 字节跳动《算法中文手册》火了,完整版 PDF 开放下载

    今天给大家推荐两份来自字节跳动大佬的算法进阶指南,据说有不少小伙伴靠这份指南成功掌握了算法的核心技能,拿到了 BAT offer.希望对大家有帮助. 第一份资料是 70K Star 的<labu ...

最新文章

  1. 面试官:哪些原因会导致JAVA进程退出?
  2. 数据库书籍大师推荐的Oracle数据库相关的书籍,收集汇总。
  3. 在VS中如保快速查看DLL或exe的已导出的函数
  4. java快速寻找一个数组的最大值或最小值, min, max,三种方法
  5. Python 学习拾遗
  6. Redis是单线程为什么还那么快?
  7. 前端18个月难度翻番?来这里把握大前端技术本质进展丨稀土开发者大会
  8. js怎么获取ueditor值_UEditor获取内容
  9. 使用模板部署的Linux虚拟机网卡不可用的处理方法
  10. 如何默认选择一个单选按钮? [重复]
  11. Mybatis案例升级版——小案例大道理
  12. [第四组]TOUCHBeta版本测试报告及发布说明
  13. RAC Debug开关修改工具
  14. Atitit.创业之uke团队规划策划 v9
  15. 【J2EE】在项目中理解J2EE规范
  16. 我的区块链著作《区块链的数学原理》,今天正式出版和发行
  17. Python 可轻松生成图文并茂的PDF报告!
  18. 如何在AD中批量创建域用户
  19. Hadoop/Hive-学习笔记【中级篇】
  20. 关于runtime error '429'解决方案

热门文章

  1. 深入理解 Java 线程池!
  2. “生命游戏之父”因新冠肺炎逝世,回顾数学顽童的一生
  3. 面对疫情等群体性危机,程序员如何在家高效办公?
  4. 蚂蚁金服自研数据库打败Oracle拿下世界第一;三星手机全面退出中国;微软发布Windows 10X双屏系统 | 极客头条...
  5. 年轻就是程序员的资本?我不敢苟同!
  6. 著名程序员 Eric S. Raymond :用 SaaS 是一种危险的愚蠢行为
  7. Top 10 盘点:2019 Java 开发者必学的测试框架、工具和库!
  8. 在使用 Go 两年之后,我又转回 PHP 了
  9. C++ 转 Python 这三年,我都经历了什么?
  10. 趣店斗鱼深陷裁员风波,程序员寒冬何去何从?| 畅言