Golang 获取 goroutine id 完全指南

在Golang中,每个goroutine协程都有一个goroutine id (goid),该goid没有向应用层暴露。但是,在很多场景下,开发者又希望使用goid作为唯一标识,将一个goroutine中的函数层级调用串联起来。比如,希望在一个http handler中将这个请求的每行日志都加上对应的goid以便于对这个请求处理过程进行跟踪和分析。

关于是否应该将goid暴露给应用层已经争论多年。基本上,Golang的开发者都一致认为不应该暴露goid(faq: document why there is no way to get a goroutine ID),主要有以下几点理由:

  1. goroutine设计理念是轻量,鼓励开发者使用多goroutine进行开发,不希望开发者通过goid做goroutine local storage或thread local storage(TLS)的事情;
  2. Golang开发者Brad认为TLS在C/C++实践中也问题多多,比如一些使用TLS的库,thread状态非常容易被非期望线程修改,导致crash.
  3. goroutine并不等价于thread, 开发者可以通过syscall获取thread id,因此根本不需要暴露goid.

官方也一直推荐使用context作为上下文关联的最佳实践。如果你还是想获取goid,下面是我整理的目前已知的所有获取它的方式,希望你想清楚了再使用。

  1. 通过stack信息获取goroutine id.
  2. 通过修改源代码获取goroutine id.
  3. 通过CGo获取goroutine id.
  4. 通过汇编获取goroutine id.
  5. 通过汇编获取goroutine id.

在开始介绍各种方法前,先看一下定义在src/runtime/runtime2.go中保存goroutine状态的g结构:

其中goid int64字段即为当前goroutine的id。

1. 通过stack信息获取goroutine id

原理非常简单,将stack中的文本信息”goroutine 1234″匹配出来。但是这种方式有两个问题:

  1. stack信息的格式随版本更新可能变化,甚至不再提供goroutine id,可靠性差。
  2. 性能较差,调用10000次消耗>50ms。

如果你只是想在个人项目中使用goid,这个方法是可以胜任的。维护和修改成本相对较低,且不需要引入任何第三方依赖。同时建议你就此打住,不要继续往下看了。

2. 通过修改源代码获取goroutine id

既然方法1效率较低,且不可靠,那么我们可以尝试直接修改源代码src/runtime/runtime2.go中添加Goid函数,将goid暴露给应用层:

这个方式能解决法1的两个问题,但是会导致你的程序只能在修改了源代码的机器上才能编译,没有移植性,并且每次go版本升级以后,都需要重新修改源代码,维护成本较高。

3. 通过CGo获取goroutine id

那么有没有性能好,同时不影响移植性,且维护成本低的方法呢?那就是来自Dave Cheney的CGo方式:

文件id.c:

文件id.go:

完整代码参见junk/id.

这种方法的问题在于你需要开启CGo, CGo存在一些缺点,具体可以参见这个大牛的cgo is not Go. 我相信在你绝大部分的工程项目中,你是不希望开启CGo的。

4. 通过汇编获取goroutine id

如果前面三种方法我们都不能接受,有没有第四种方法呢?那就是通过汇编获取goroutine id的方法。原理是:通过getg方法(汇编实现)获取到当前goroutine的g结构地址,根据偏移量计算出成员goid int的地址,然后取出该值即可。

项目goroutine实现了这种方法。需要说明的是,这种方法看似简单,实际上因为每个go版本几乎都会有针对g结构的调整,因此goid int64的偏移并不是固定的,更加复杂的是,go在编译的时候,传递的编译参数也会影响goid int64的偏移值,因此,这个项目的作者花了非常多精力来维护每个go版本g结构偏移的计算,详见hack目录。

这个方法性能好,原理清晰,实际使用上稳定性也不错(我们在部分不太重要的线上业务使用了这种方法)。但是,维护这个库也许真的太累了,最近发现作者将这个库标记为“DEPRECATED”,看来获取goroutine id是条越走越远的不归路?

5. 通过汇编获取goroutine id

虽然方法4从原理和实际应用上表现都不错,但是毕竟作者弃坑了。回到我们要解决的问题上:我们并不是真的一定要获取到goroutine id,我们只是想获取到goroutine的唯一标识。那么,从这个角度看的话,我们只需要解决goroutine标识唯一性的问题即可。

显然,上面作者也想清楚了这个问题。他新开了一个库go-tls, 这个库实现了goroutine local storage,其中获取goroutine id的方式是:用方法4的汇编获取goroutine的地址,然后自己管理和分配goroutine id。由于它获取到的并不是真正的goroutine id,因此我将之称为goroutine id。其实现的核心代码如下:

  1. 获取g结构地址。
  2. 分配伪goroutine id.

这种方式基本没有什么不能接受的hack实现,从原理上来说也更加安全。但是获取到不是你最开始想要的goroutine id,不知你能否接受?

小结

获取goroutine id是一条不归路,目前也没有完美的获取它的方式。如果你一定要使用goroutine id,先想清楚你要解决的问题是什么,如果没有必要,建议你不要走上这条不归路。尽早在团队中推广使用context, 越早使用越早脱离对goroutine id的留恋和挣扎。

Credit

微服务架构下的立体监控系统设计和实现

背景

GOPS全球运维大会(北京站)听到了不少干货。特别受益的是来自腾讯SNG事业部聂鑫分享的
《从0到1到N,腾讯监控体系全透视》

在他的主题分享中,他将腾讯这些年的监控系统的发展历程概括为点监控-->面监控-->深度监控

看到他这页幻灯片的时候,有一种醍醐灌顶的感觉。因为在听他分享的时候,我们的系统才刚刚完成架构微服务化没多久,我们上线了调用链:分布式追踪系统来解决在微服务分布式系统中排查跟踪特定问题,但我们的监控系统还没有针对架构微服务化后进行相应的进化。比如,大部分监控系统停留在点监控的层面,少数进行关联多个服务的面监控也做得比较初级,需要人工分析和干预。

点监控比较好理解,就是对系统布置监控点,根据阈值触发告警。

面监控则是对告警信息进行时间和空间关联,有效消除毛刺告警,使告警更加准确。因为告警本身有时效性,时效性源于告警延时,连续性可能是干扰,因此只进行时间关联是不够的。链路相关性(空间相关性)和时间相关性一起决定准确性。

深度监控其实有点追深度学习的热点,从分享看,实际就是对面监控的链路相关性进一步完善,以及根据收集到的系统进行使用机器学习进行简单的分类。

参加会议回来以后,我们明确了自己监控系统的进化方向,根据自身系统的特点进行了一些取舍,确定了立体监控的方案。

立体监控方案目标

所谓「立体监控」即指在我们当前系统点监控为主的情况下,尽可能复用当前监控的探针,进行时间和空间(服务链路之间)维度上的扩展,实现对整个系统时空上的监控。

立体监控需要消除点监控带来的监控毛刺,如服务存在依赖情况下,级联告警通过立体监控分析融合后,应该只对最后一级进行告警。

立体监控可以快速的定位系统故障,定位粒度根据不同监控类型可以做到微服务级别、接口级别、数据库实例级别、缓存实例级别等。

立体监控方案设计和实现

对于微服务,我们通过data bus将需要进行监控的信息发送到kafka进行收集。这种方式在调用链分布式追踪系统也有使用。不得不说,data bus是架构微服务化后非常重要和实用的基础组件。

为了尽可能降低各个微服务集成监控组件的侵入性,我们通过修改基础库的方式进行集成。比如,微服务使用了MySQL数据库,那么我们就修改微服务使用的数据库驱动来对数据库进行监控,一旦发生错误或warning信息,将消息写入data bus;某个微服务需要调用腾讯的某个接口,我们就对修改我们的http客户端基础库,将错误和超时消息写入data bus实现对该接口的监控。

因此,我们的微服务监控集成几乎不需要研发人员的介入,只需要运维人员更新服务依赖库,然后重新发布上线即可。事实上,我们在微服务监控集成上就没有安排独立的发布上线时间,都是在研发上线feature或hotfix时搭车上线的,将近两百个微服务在2周内完成集成。需要注意的是,基础库的修改一定是由团队中相对资深的开发人员来做,并且测试一定要做到位,否则会引起大规模的问题。

对于k8s集群、数据库、redis等基础设施的监控沿用以前的点监控数据,只是将事件统一上报到了Event Collector事件收集服务。Event Collector除了收集事件数据,也对一些不符合通用规则的数据进行过滤。

Event Analyzer事件分析服务根据Event Collector收集到的事件,进行时间和空间上的分析监控结果,并发送告警通知。

在事件分析上,时间维度非常简单,选取一个时间窗口内的事件信息即可(我们当前根据经验设定的是1min)。空间维度方面,则相对麻烦一下。我们没有采用腾讯使用的全链路分析算法。主要是因为该算法需要预生成链路拓扑图。而微服务架构中,各个微服务的增加、减少和变更是非常频繁的,预生成拓扑图有点反模式,也会产生一定的成本。我推测腾讯之所以觉得预生成拓扑图不是问题,跟它当前架构没有完全微服务化以及内部严格的管理流程有关。

因为不想预生成链路拓扑图,分享中给出的链路面积计算公式也就无法使用。另一方面,因为腾讯给出的全链路分析算法其实没有完备的理论证明,在我们数据量没有腾讯庞大的情况下,我们是否能用该算法取得同样效果是缺乏信心的。

但是,思想是可以借鉴的。通过讨论,我们一致认为所谓的链路分析其实就是关联性分析,而关联性分析那就毫不犹豫的使用Google Page Rank算法:

如上图所示,我们将服务(1, 2, 3, 4)之间的告警事件作为一个超级链接指向,然后计算PR值,那么,PR值最大的是我们认为出现问题可能性最大的服务。理论证明这里略过,这个锅我们扔给Google背即可。

需要说明的是,任何一个复杂的系统在出现故障时,往往不是一个组件或服务出现问题,很可能是多个服务同时出现问题。那么在计算PR时,可能就是面对多个独立的PR有向图。这个时候独立图之间的关系处理就可以根据历史数据进行机器学习,以进一步给出故障原因,然后根据预案快速处理故障,恢复服务。

Event Analyzer也提供了一个页面,可以查看历史的告警信息链路:

也可以对链路流量(边越粗流量越大)进行监控和分析:

总结

立体监控上线后,运维方面以前只能从点入手排查问题转变为直接根据Event Analyzer的聚合告警信息联系对应服务的开发者解决问题。同时,发现了很多以前被经验标记为系统抖动没有重视的潜在问题。至此,我们的微服务架构在调用链和立体监控的双重加持下,又完成了一次进化。

Golang多级内存池设计与实现

Golang多级内存池设计与实现

上个月,牙膏厂intel因为MeltdownSpectre两个bug需要给CPU固件和系统打了补丁。我们生产环境使用的是阿里云,打完补丁后,几台IO密集型的机器性能下降明显,从流量和cpu load估计,性能影响在50%左右,不是说好的最多下降30%麽?

在跑的业务是go写的,使用go pprof对程序profiling了一下,无意中发现,目前的系统gc和malloc偏高。其中ioutil.ReadAll占用了可观的CPU时间。

ioutil.ReadAll为什么慢?

这个函数的签名原型是func ReadAll(r io.Reader) ([]byte, error). 团队的小伙伴非常喜欢用这个函数,其中一个原因是这个函数可以将r中的数据一次性读完返回,不需要关心内存如何分配、如果分配的内存不够了,如何进行内存扩张等。作为一个util函数,这样设计是完全没问题的。但是,IO密集场景下,这个函数的开销就是你需要关心的了。这个函数实际调用realAll读取数据:

其中,capacity是常量值512. realAll函数在调用buf.ReadFrom进行数据读取:

看到这里,原因就非常清楚了:如果要读取的数据大小超过了初始buf大小(默认初始大小为512 bytes), 则会重新分配内存,并拷贝内容到新的buffer中。如果要读取的数据非常大,则会重复多次上述操作。那么优化的问题就转化为如何降低内存重分配和拷贝。

多级内存池的设计和实现

  1. 内存池被按照大小被分为多级。如上图所示,(0, 1024]使用level 0, (1024, 2048]使用level 1. 内存池分级有两个好处:
    1. 可以灵活的规划不同级别内存池的总大小和item数量,适应不同业务。
    2. 实现层面上,可以将一把内存池大锁拆分成多个小锁,减少锁争抢。
  2. 当已分配的内存池耗尽需要扩张时,一次性申请一大块内存,提高扩张效率。如level 0所示。
  3. 代码实现gmmpool,bench结果显示性能提高约19倍:

小结

对于频繁进行内存分配和释放的场景,使用内存池可以显著降低golang运行时的开销。同时也要注意,内存池的内存交给了用户管理,你需要小心检查是否存在内存泄露问题。如果你对性能要求没有这么苛刻,只是想复用一些小对象,那么我们推荐你使用标准库的sync.Pool.

另外,开头提到的阿里云性能问题,即使使用了内存池优化,结果还是非常悲剧。最后阿里云帮我们更换了没有打牙膏厂补丁的机器解决。是不是非常惊喜??