mzh/blog

Go团队如何解bug[1]: 乱序执行与内存屏障

开篇前言

Go项目在发展过程中有过许许多多的bug,核心开发组在解决bug的时候有各种各样的方法,涉及的知识点都很值得学习。 我觉得这些都挺值得跟大家分享,也希望做成一个不鸽系列。

正题

Issue地址:issues/35541

开发:Cherry Zhang, Michael Knyszek

这个bug是由Go的自动构建机器linux-mipsle-mengzhuo在日常测试过程中首次发现的, 表现是某个对象里的指针指向了一个不存在的span上,导致runtime panic。

Cherry根据 经验 判断出这个span类型是goroutine用的,并且贴出了对应的对象映射(如下)

object=0xc000106300 s.base()=0xc000106000 s.limit=0xc000107f80 s.spanclass=42 s.elemsize=384 s.state=mSpanInUse
 *(object+0) = 0xc00052a000		g.stack.lo
 *(object+8) = 0xc000532000		g.stack.hi
 *(object+16) = 0xc00052a380		g.stackguard0 = g.stack.lo + _Stackguard = g.stack.lo + 896
 *(object+24) = 0xffffffffffffffff	g.stackguard1
 *(object+32) = 0x0			g._panic
 *(object+40) = 0xc0005313e0 <== 	// 故障点 g._defer
 *(object+48) = 0xc000080380		g.m
 *(object+56) = 0xc0005310b8		g.sched.sp
 *(object+64) = 0x87038			g.sched.pc
 *(object+72) = 0xc000106300		g.sched.g

虽然Cherry没有明说如何判断出是goroutine,但其中的偏移量24字节处的数据正好是64位的满位, 看起来确实像一般的goroutine的stackguard1 (malg时分配的)

这个bug奇怪的地方在于不是必现,是在不同的函数,时不时runtime panic, 而且出问题的span有时是在用的,有时又是废弃的。 更奇怪的是,RTRK(塞尔维亚的一家科研机构)提供的MIPS构建机从来都没有出现过一次类似原因的runtime panic。

一开始,开发组认为是MIPS的问题,时间长了,发现还有plan9-arm, darwin-arm64-corellium这两个架构也出现了类似问题,然而出现的频率远不如MIPS高。 因为没有有效解,几乎每周Bryan Mills(负责Go开发质量的工程师)都会在Github Issue中填上新增的panic日志地址,最后他也嫌烦了……

脑袋疼.jpg

问题定位

事情的转机是我有一次debug过程中,发现runtime有个测试函数TestDeferHeapAndStack 总是 会引发这个panic。 而在Go项目的自动化整体测试中,为了测试尽快完成,通常速度慢的机器都会选择short mode, 而恰好这个short mode不会导致panic。

本身这个测试也很有意思,因为编译器只会把函数作用域中无指针的defer分配在stack上,而循环体中的defer就会分配在heap中。

测试用例如下:

func deferHeapAndStack(n int) (r int) {
        if n == 0 {
                return 0
        }
        if n%2 == 0 {
                // heap-allocated defers
                for i := 0; i < 2; i++ {
                        defer func() {
                                r++
                        }()
                }
        } else {
                // stack-allocated defers
                defer func() {
                        r++
                }()
                defer func() {
                        r++
                }()
        }
        r = deferHeapAndStack(n - 1)
        escapeMe(new([1024]byte)) // force some GCs
        return
}

我尝试着自己修复这个bug,然而失败了,不过既然可以稳定重现,那可以帮助核心团队来bisect这个bug。

这个技能是从Austin(Go runtime leader)学来的(他如何从Go定位Linux bug正好可以写下一期哈)。

因为这个bug是从1.13之后才有的,所以我选择了HEAD 和 go1.13.9之间开始定位。

$ git bisect HEAD go1.13.9
$ git bisect run ./test.sh // 就是跑上面deferTest的脚本

最后发现是runtime: rearrange mheap_.alloc* into allocSpan这个commit导致了bug。

修复方法

Cherry在和Michael聊这个bug之后发现在allocSpan的过程中由于缺了一个内存屏障(memory barrier), 在弱内存顺序模型的机器上这个bug会随机出现。以下是她的修复commit:

When copying a stack, we
1. allocate a new stack,
2. adjust pointers pointing to the old stack to pointing to the
   new stack.

   If the GC is running on another thread concurrently, on a machine
   with weak memory model, the GC could observe the adjusted pointer
   (e.g. through gp._defer which could be a special heap-to-stack
   pointer), but not observe the publish of the new stack span. In
   this case, the GC will see the adjusted pointer pointing to an
   unallocated span, and throw. Fixing this by adding a publication
   barrier between the allocation of the span and adjusting pointers.

   One testcase for this is TestDeferHeapAndStack in long mode. It
   fails reliably on linux-mips64le-mengzhuo builder without the fix,
   and passes reliably after the fix.

简单翻译一下:

当拷贝一个栈数据(stack)时,我们先:
1. 申请一个新的栈空间
2. 将老的栈上的指针(pointer)从老栈指向新栈上。

如果此时在另一个线程并行地执行着GC(垃圾回收),且架构是弱内存顺序模型时,GC可以观察到指针的调整
(例如: gp._defer 就是一个特殊的 heap-to-stack 指针),但是观察不到新的栈分配。
这时GC就会观察到指针分配到了一个没有分配的空间中。
我们通过在分配和调整指针之间添加内存屏障(memory barrier)来修复这个bug。

代码就更简单了,只是在allocSpan的过程中添加了一个函数publicationBarrier()

那么什么是内存顺序模型?

背景知识

Lamport 在上世纪

并发GC

Go在1

多线程问题

MESI

小结

参考资料

[^1] https://blog.golang.org/ismmkeynote [^2] https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html [^3] https://zhuanlan.zhihu.com/p/48157076

代码就更简单了,只是在allocSpan的过程中添加了一个函数publicationBarrier()

什么是内存顺序模型可以参考这个系列的文章《Memory Barriers Are Like Source Control Operations》

X86没有问题,是因为强内存模型保证了每个写入操作的顺序能在各个CPU之间保证同步。 而弱内存模型就没有保证这点,所以强制sync

等等,你问为啥ARM没有这个问题?这是玄学(划掉 因为ARM虽然是弱内存模型,但是保证了“Data dependency barrires

所以以后碰到诡异的多线程问题,就先sync(手动狗头

参考资料

  1. https://blog.golang.org/ismmkeynote
  2. https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
  3. https://zhuanlan.zhihu.com/p/48157076

龙芯 & Golang!

龙芯,不少人都比较陌生,见过的就更少了。

龙芯活着,还在云时代的2019年拯救了一下MIPS这棵34年的枯树。

一点背景故事

事情还要从去18年底,Go的MIPS架构的构建机(builder)集体下线说起。 这里顺便说一下builder的功能,其实就是Go在验证各个平台兼容性用的机器, 主要是各大公司和志愿者捐赠的,绝大部分是国内开发者很少见的PowerPC、ARM、S390X这类ISA, 操作系统更多,具体可以看看build.golang.org

19年4月时, Go核心团队的Bradfitz发现ImgTec负责的mips、 mipsle、mips64le、mips64(大小端/32/64位)四种机型的builder已经下线半年多了, 根据Go的平台支持条件要求,任何一个架构+操作系统组合都需要有验证机型,否则就要踢出Go的支持列表。 所以Bradfitz发邮件给ImgTec维护者, 收到的只有查无此人的自动回复, 他觉得是这哥们离职的原因, 但实际上是2017年底的时候MIPS背后的ImgTec把MIPS卖了……这些builder竟然还多撑了一年。

大概同时,我从国内Go开发大牛Ben那里获得了一台龙芯3A1500, 这台机器是龙芯团队希望能有人维护Go MIPS,毕竟Go已经是云时代的C了, 不少服务是运行在Go的runtime上的, 另一方面docker已经成了事实标准,龙芯云也是基于docker的。 所以把机器寄给了Ben,但Ben忙于工作,我又喜欢多管闲事性能优化……于是我愉快地收下了这台3A1500。

Loongson 3A1500

不过这台机器可能因为暴力的快递摔坏了,一直点不亮,我只好退给了Ben, 从龙梦公司通过古老的转账汇款方式买了一台3A3000。

就在我搜索MIPS可优化点的时候,发现了MIPS要被踢出去的帖子, 所以我回帖说可以让我的这台龙芯替代ImgTec做builder。 经过自己

  1. 编译4.19内核
  2. 申请密钥
  3. 改Go build项目代码
  4. 艰难地设置网络之后

龙芯的builder: linux-mipsle-mengzhuo终于上线了。(名字不是我挑的) 具体的申请builder的wiki可以看这里

龙芯Go现状

毕竟3A3000是16年的CPU,加上是1.5Ghz/8KB L3 Cache/28nm 制程自然也不能和Intel、AMD比。

其他问题嘛……

Unalign access penalty,没有Hyper Threading,SIMD支持也几乎没有。 就算这么多缺陷,龙芯也是目前市面上零售方式能买到的唯一的MIPS架构的CPU了, MIPS新东家Wave computing是搞AI的,不知道买MIPS来干嘛,架构不发展,只是 开源了r6架构,但是看官网制程还是28nm的…… 所以可以说龙芯是MIPS,这个1985年就出现的架构最后脸面了(欢迎打脸)。

大家可以看看龙芯的cpuinfo哈

Linux ls-3a3k 4.19.53+ #1 SMP PREEMPT Wed Jul 10 15:12:52 UTC 2019 mips64 mips64 mips64 GNU/Linux
system type             : generic-loongson-machine
machine                 : loongson,generic
processor               : 0
cpu model               : Loongson-3 V0.13  FPU V0.1
model name              : Loongson-3A R3 (Loongson-3A3000) @ 1450MHz
CPU MHz                 : 1450.00
BogoMIPS                : 2887.52
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 1088
extra interrupt vector  : no
hardware watchpoint     : yes, count: 0, address/irw mask: []
isa                     : mips1 mips2 mips3 mips4 mips5 mips32r1 mips32r2 mips64r1 mips64r2
ASEs implemented        : dsp dsp2 vz
shadow register sets    : 1
kscratch registers      : 6
package                 : 0
core                    : 0
VCED exceptions         : not available
VCEI exceptions         : not available

说到Go在龙芯上的实际性能,通过观察,大概比PPC64、ARM这些builder快点, Go所有源代码编译+测试一次大概要耗时25分钟左右。

不过我发现不少性能关键路径上的代码甚至都没按一台正常的64位机器写, 而是明显的“能用”的状态,可能和Minux和Cherry移植的时候先让MIPS架构能跑起来有关。 更要命的是,Go不知道为啥,最小版本要求竟然是MIPS III(1993年发布), 想在Go上用常见的优化指令,比如count leading zero(CLZ), conditional move (CMOV.CON), BSWAP ( ROR DSHB ) prefech统统都不行……

不过我还是提交了一些优化的CL,平时还要忙无聊的工作,精力有限,目前只有:

未来的展望

如果我能多提交一些bytealg,syscall,SSA相关优化之后应该就能更快点, 就算没有向量优化,硬件指令集,至少总体性能也应该能提升30%左右。 国内我知道在优化的人也就Xenon一个了,如果你也有兴趣搞龙芯Go优化的欢迎联系我。

有可能的话,我也想尽可能地推动核心团队提升Go MIPS的版本,MIPS III 实在是太老了。

同时我也希望各位开发们能借着“国产化”的春风,在工作中多用国产CPU,帮助提升性能, 丰富一下生态,多影响一下上游。至少不是做个冷嘲热讽的键盘侠。顺便祈祷MIPS的新东家Wave computing 不要再搞什么幺蛾子把MIPS真的送进博物馆里了。

最后附上这台builder的样子,毕竟应该是国内第一台在Go项目里的服务器。

LS 3A3K

Go Contributor Summit 2019参会记

今年有幸去美国圣地亚哥参加了Go Contributor Summit 2019,主要是Go的代码贡献者和核心开发团队在一起讨论关于Go相关的话题。

紧跟着的GopherCon也是第一次换城市到了圣地亚哥,还在中途岛号航母(USS Midway)上搞了欢迎party,真是毕生难忘了。

破冰环节

刚进门,就发现Russ Cox站在门口,跟他打了打招呼,顺便吐槽了一番他的范型设计在国内引起的巨大讨论,不过大佬很快就被其他人抓去讨论别的话题了。 这时热情的Joe Tsai(Protobuf维护者)跟我打招呼,然后给我介绍他认识的Google同事, 但毕竟是大型网友见面会,除了Github上见过照片的,基本都名字对不上脸,大家很腼腆,当然也有像Möhrmann(Google SRE)热心地跑来跑去介绍自己的人。

这么尴尬怎么玩?没事,Google组织方已经想好了,接下来就是破冰环节。

图1

图1: 会议日程表,左spf13 右rsc

其实很简单,就是一桌人按生日的月排序,小的介绍大的。介绍内容除了名字公司以外还要介绍个自己的fun fact,这下真难倒我了, 幸好我的介绍人Julie Qiu(Go文档维护)给我想到了一个,坐了14个小时的飞机第一次来美国(虽然我没get到fun是啥…… 轮到我介绍了,正好介绍的是Joe,他的fun fact是一家3姐弟都在Google上班,又一次没get到。 最让我震惊的是Cherry Zhang(ARM/MIPS 维护者)是个妹子!主要是看代码和说话风格,感觉都是个男的,轮到她介绍的时候了才发现是个妹子。

因为是贡献者大会,所以主办方希望大家自选话题并分组讨论各自感兴趣的话题。 大家先头脑风暴,讲出了自己关心的话题,然后经过投票之后,上午的话题分成以下4组:

  1. 编译器
  2. 范型和错误处理
  3. 社区相关问题,发展和规划
  4. 工具相关问题,proxy mod这类

讨论环节

每个组讨论都要选一个负责人来组织讨论,防止一言堂,然后有个两个志愿者来记录一下大家说的话题。 我参加了编译器组,见识到了Keith Randell(编译器维护),Austin Clements(编译器组负责人)和其他大牛们讨论的问题:

后来的事实证明,我适合去社区相关组,毕竟我在组里几乎没发言,而我又是唯一一个从中国来的开发者,而Go的核心团队相当关心Go在中国的发展。

图2

图2:社区组讨论如何提Bug和proposal,大佬云集

杂事体会

国外开会真心体验到了什么叫人性化,每40分钟就停下来休息,然后每2次休息就有茶歇。这样开会一点都不累。 由于是Contributor Summit,所以是管午饭的,吃的嘛……真不敢恭维,都是三明治+沙拉。 在午饭时,Joe Tsai跟我聊了聊中国用户时怎么使用文档的,我跟他说大家基本都能看懂英语的或者代码, 所以一般不用操心中国用户看文档,当然也聊了protobuf维护的坑啊,中国美国社会、政治问题之类的。 期间还有些其他开发者看到我衣服上的公司名字,跑过来问某鹅怎么用Go啊,中国开发者怎么盗用weather.com的API的这类的事。

还有就是我真心不建议英语不好的朋友来参加,我雅思听力6.5、平时基本能不看字幕看美剧也扛不住各种口音的攻击,特别是8小时的大型听力考试+技术能力考试+时差, 脑子基本转不动了,而且很多俚语和文化确实需要浸入式地学习才能了解,出现了几次全场大笑,我还很懵逼的状态。

总体还是收获特别大,不仅是跟一堆核心开发面对面,而且还认识了不少好朋友哈~

沈阳美签记

水一篇。

念念不忘,必有回响。

这真是对我一直想去美国看看的真实写照。 本来Gopher Contributor Summit并没有邀请我,但是Ben不去参加,把名额转给我了。 我想着我的贡献度远比他低,只是10个左右的CL而已,没想到询问了主办方后2周后,突然就收到了同意参加的邮件。 6月22号填完查户口般的DS160后发现,我应该去的广州领事馆面签预约时间竟然到了一个月以后,可是这个会就在7月23号开了啊。这样肯定是不行的, 四处找啊找,上海、北京也是1个多月,最后在预约栏里发现了“沈阳”这个选项。点进去一看,只要7天!! 最快就是7月1号办!!赶紧买了机票。

以前都是看文章,说什么东三省衰落,落后啥的,没想到飞到沈阳以后发现,压根不是这么回事! 要啥有啥!地铁还比南宁还多! 真是应验了主席的“没有调查就没有发言权”。 坐了出租车到订的“公寓式酒店”,发现真是公寓改的酒店……因为这酒店就在一栋40多层高的高档居民楼里。 不过到的时候凌晨1点了,所以也没看清城市啥样,虽然确实不能和深圳的灯火通明比,但也不算差了。

因为看网上攻略美签不能带手机啥的,只能拿个透明资料袋,又懒得取现金来打车,所以订的酒店就隔了20分钟走路时间。第二天醒了之后时间还早,所以走着去看了看,9点就已经好多人呆在领事馆外头等叫号了。 不过领事馆安保太严了,不让拍照,我前面有个人拍了一张,就被保安追过去,站在身后要求他删掉。

走回酒店的路上发现有个饺子馆,想着午饭就吃个正宗东北酸菜饺子好了。 早饭就匆匆吃了个面包,然后等到时间去做面签。

到了以后,坐在领事馆对面的椅子上,边上的一个妈妈跟我攀谈了起来,问我为啥去,我说开个会,我礼貌性地反问她,她很自豪地说是送女儿上普渡大学。听着很野鸡,但回头一查……常青藤呵呵 就在尴尬的时候,幸好边上一对夫妇听见她也是送小孩上学,就跟她聊了起来,自曝一年要花60万学杂费,好可怕。

轮到我了,跟着前面的人做,拿资料,录指纹。 真的面签了,一个白人大叔,中文问我为啥去美国,我答开会。 然后跟网上攻略一样,他直接英语问工作在哪里,多久了,bla bla,特意问了我是不是做AI相关的开发,还没问我要全家福和资产证明时,就把我的护照收走了,丢了张通过的红纸出来……果然是大厂牌子响么。

离晚上的飞机时间还早,我就吃了个酸菜饺子做午饭,嗯……跟我们家楼下的东北人夫妇开的店里卖的是一个味道,就是皮得多,吃起来有嚼劲。

东北酸菜饺子

然后在浑河边上转了转,名不副实,这河清着呢,沈阳还有个比深圳好的一个地方就是,特别凉快~ 最后水几张沈阳的照片

沈阳电视塔

沈阳电视塔

好像是图书馆

好像是图书馆

电车

电车

NABHash介绍及原理

项目地址

github.com/mengzhuo/nabhash

什么是NABHash?

NABHash 是一种 超级快的 基于AES的非密码学安全哈希算法(Hash Algorithm)

有以下特点:

性能对比图

![NABHash性能对比图](/2019/05/Hash speed vs size.png)

可以看到,在65536字节时,NABHash的速度是xxhash、murmur3这些常见快速哈希算法的10倍左右。

为啥可以这么快?

因为“简约”。

一般哈希算法是使用加法、异或、移位完成的(Shift Add XOR) 而哈希算法为了满足雪崩性和分布性,会多计算几轮,导致就算使用SIMD技术,也无可避免性能下降。

这就是NABHash优化的地方,NABHash对于一个16字节数据块,在Intel平台上仅仅使用了1个指令:

AESENC

即AES加密。而选择AES的主要原因是,现在主流的CPU都支持了AES硬件加速(ARM、PPC),而SHA系列虽然有硬件支持,但指令复杂,往往一个数据块还需要多个指令才能完成计算。

主要流程可见下图: NABHash流程图

相当于用数据不停地加密初始向量值(0x5A827999 0x6ED9EBA1),最终得到一个哈希值。

可能有些小朋友就会问了,为啥初始向量是这个值,是随机生成的么?不,这是sha1的初始key值。

最后,为啥叫NABHash

因为这是Non-Crypto-Safe AES Based Hash,简写NABHash,基于AES的非密码学安全哈希算法。