本文目录一览:
- 1、深入理解golang
- 2、如何监控 golang 程序的垃圾回收
- 3、如何用go语言抓取网络摄像头数据
- 4、Golang实验性功能SetMaxHeap 固定值GC
- 5、Golang什么时候会触发GC
- 6、golang channel 超时如何处理
深入理解golang
最近三年,在工作中使用go开发了不少服务。深感go的便捷,以及它的runtime的复杂。我觉得需要定期的进行总结,因此决定写这篇文章,也许更准确的,应该叫笔记。
最近终于解决了一个和cgo有关的问题。这个问题从发现到解决前后经历了接近4个月,当然,和人手不足也有关系。而对于我个人而言,这个问题其实历时2年!这得从头说起。
在上一家公司的一个项目里,有一个服务做音视频数据的提取,这个服务运行在嵌入式设备TX2上。音视频提取这一关键功能主要利用nvidia基于gstreamer开发的插件,这个插件可以发挥nvidia gpu的硬件解码功能。当时这个服务使用go和c混编的方式,问题的症状是服务运行一段时间后,不输出音视频数据。遗憾的是,由于疫情,项目停止,因此没有机会继续研究这个问题。
时间来到去年底。当前这个项目进行压力测试,发现关键的语音处理服务运行一段时间后,会出现不拉流的情况,因此也没有后续的结果输出。症状和上一个项目非常像。虽然使用的第三方SDK不一样,但同样用了go和c混编的方式。一开始,焦点就放在go的运行时上,觉得可能是go和c相互调用的方式不对。经过合理猜测,并用测试进行验证后,发现问题还是在第三方拉流的SDK上,它们的回调函数必须要快,否则有可能会阻塞它们的回调线程。当然,在go调用c的时候,如果耗时比较长,会对go的运行时造成一些副作用;在c回调go的时候,go的运行时也有可能阻塞c的回调线程。但go的运行时已经比较成熟,因此我觉得它对这个问题的贡献不大。以上采用了假设-验证的方法,主要的原因还是第三方的拉流SDK不开源。在定位问题的过程中,使用了gdb的gcore来生成堆栈;也搭建了灰度环境来进行压力测试,以及完善监控,这些都是解决方法的一部分。
正是这一问题,促使我更多的了解go的运行时。而我看得越多,越觉得go的运行时是一个庞大的怪物。因此,抱着能了解一点是一点的心态,不断的完善这篇笔记。
如何监控 golang 程序的垃圾回收
如果是本地开发环境, 可以利用 GODEBUG=gctrace=1 /path/to/binary 的方式输出 GC 信息,然后用 gcvis 作可视化。
如何用go语言抓取网络摄像头数据
理论上是不行的,要想实时就必须连续不断传输的视频信号,而你的软件是播放视频文件的,文件的话必须有头尾,如果做成文件格式再播放,那就不叫实时监控了。
Golang实验性功能SetMaxHeap 固定值GC
简单来说, SetMaxHeap 提供了一种可以设置固定触发阈值的 GC (Garbage Collection垃圾回收)方式
官方源码链接
大量临时对象分配导致的 GC 触发频率过高, GC 后实际存活的对象较少,
或者机器内存较充足,希望使用剩余内存,降低 GC 频率的场景
GC 会 STW ( Stop The World ),对于时延敏感场景,在一个周期内连续触发两轮 GC ,那么 STW 和 GC 占用的 CPU 资源都会造成很大的影响, SetMaxHeap 并不一定是完美的,在某些场景下做了些权衡,官方也在进行相关的实验,当前方案仍没有合入主版本。
先看下如果没有 SetMaxHeap ,对于如上所述的场景的解决方案
这里简单说下 GC 的几个值的含义,可通过 GODEBUG=gctrace=1 获得如下数据
这里只关注 128-132-67 MB 135 MB goal ,
分别为 GC开始时内存使用量 – GC标记完成时内存使用量 – GC标记完成时的存活内存量 本轮GC标记完成时的 预期 内存使用量(上一轮 GC 完成时确定)
引用 GC peace设计文档 中的一张图来说明
对应关系如下:
简单说下 GC pacing (信用机制)
GC pacing 有两个目标,
那么当一轮 GC 完成时,如何只根据本轮 GC 存活量去实现这两个小目标呢?
这里实际是根据当前的一些数据或状态去 预估 “未来”,所有会存在些误差
首先确定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 为本轮 GC 存活量, gcpercent 默认为 100 ,可以通过环境变量 GOGC=100 或者 debug.SetGCPercent(100) 来设置
那么默认情况下 goal = 2 * heap_marked
gc_trigger 是与 goal 相关的一个值( gc_trigger 大约为 goal 的 90% 左右),每轮 GC 标记完成时,会根据 |Ha-Hg| 和实际使用的 cpu 资源 动态调整 gc_trigger 与 goal 的差值
goal 与 gc_trigger 的差值即为,为 GC 期间分配的对象所预留的空间
GC pacing 还会预估下一轮 GC 发生时,需要扫描对象对象的总量,进而换算为下一轮 GC 所需的工作量,进而计算出 mark assist 的值
本轮 GC 触发( gc_trigger ),到本轮的 goal 期间,需要尽力完成 GC mark 标记操作,所以当 GC 期间,某个 goroutine 分配大量内存时,就会被拉去做 mark assist 工作,先进行 GC mark 标记赚取足够的信用值后,才能分配对应大小的对象
根据本轮 GC 存活的内存量( heap_marked )和下一轮 GC 触发的阈值( gc_trigger )计算 sweep assist 的值,本轮 GC 完成,到下一轮 GC 触发( gc_trigger )时,需要尽力完成 sweep 清扫操作
预估下一轮 GC 所需的工作量的方式如下:
继续分析文章开头的问题,如何充分利用剩余内存,降低 GC 频率和 GC 对 CPU 的资源消耗
如上图可以看出, GC 后,存活的对象为 2GB 左右,如果将 gcpercent 设置为 400 ,那么就可以将下一轮 GC 触发阈值提升到 10GB 左右
前面一轮看起来很好,提升了 GC 触发的阈值到 10GB ,但是如果某一轮 GC 后的存活对象到达 2.5GB 的时候,那么下一轮 GC 触发的阈值,将会超过内存阈值,造成 OOM ( Out of Memory ),进而导致程序崩溃。
可以通过 GOGC=off 或者 debug.SetGCPercent(-1) 来关闭 GC
可以通过进程外监控内存使用状态,使用信号触发的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式进行堆内存使用的监测
可以通过调用 runtime.GC() 或者 debug.FreeOSMemory() 来手动进行 GC 。
这里还需要说几个事情来解释这个方案所存在的问题
通过 GOGC=off 或者 debug.SetGCPercent(-1) 是如何关闭 GC 的?
gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)
通过 GC trace 可以看出,上面所说的 goal 变成了一个很诡异的值 17592186044415
实际上关闭 GC 后, Go 会将 goal 设置为一个极大值 ^uint64(0) ,那么对应的 GC 触发阈值也被调成了一个极大值,这种处理方式看起来也没什么问题,将阈值调大,预期永远不会再触发 GC
那么如果在关闭 GC 的情况下,手动调用 runtime.GC() 会导致什么呢?
由于 goal 和 gc_trigger 被设置成了极大值, mark assist 和 sweep assist 也会按照这个错误的值去计算,导致工作量预估错误,这一点可以从 trace 中进行证明
可以看到很诡异的 trace 图,这里不做深究,该方案与 GC pacing 信用机制不兼容
记住,不要在关闭 GC 的情况下手动触发 GC ,至少在当前 Go1.14 版本中仍存在这个问题
SetMaxHeap 的实现原理,简单来说是强行控制了 goal 的值
注: SetMaxHeap ,本质上是一个软限制,并不能解决 极端场景 下的 OOM ,可以配合内存监控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆内存大小, Go 中除了堆内存还分配了如下内存,所以实际使用过程中,与实际硬件内存阈值之间需要留有一部分余量。
对于文章开始所述问题,使用 SetMaxHeap 后,预期的 GC 过程大概是这个样子
简单用法1
该方法简单粗暴,直接将 goal 设置为了固定值
注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal
简单用法2
当不关闭 GC 时, SetMaxHeap 的逻辑是, goal 仍按照 gcpercent 进行计算,当 goal 小于 SetMaxHeap 阈值时不进行处理;当 goal 大于 SetMaxHeap 阈值时,将 goal 限制为 SetMaxHeap 阈值
注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal
切换到 go1.14 分支,作者选择了 git checkout go1.14.5
选择官方提供的 cherry-pick 方式(可能需要梯子,文件改动不多,我后面会列出具体改动)
git fetch “” refs/changes/67/227767/3 git cherry-pick FETCH_HEAD
需要重新编译Go源码
注意点:
下面源码中的官方注释说的比较清楚,在一些关键位置加入了中文注释
入参bytes为要设置的阈值
notify 简单理解为 GC 的策略 发生变化时会向 channel 发送通知,后续源码可以看出“策略”具体指哪些内容
返回值为本次设置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
注:作者尽量用通俗易懂的语言去解释 Go 的一些机制和 SetMaxHeap 功能,可能有些描述与实现细节不完全一致,如有错误还请指出
Golang什么时候会触发GC
Golang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个回收动作呢?
源码主要位于文件 src/runtime/mgc.go go version 1.16
触发条件从大方面说,可分为 手动触发 和 系统触发 两种方式。手动触发一般很少用,主要由开发者通过调用 runtime.GC() 函数来实现,而对于系统自动触发是 运行时 根据一些条件判断来进行的,这也正是本文要介绍的内容。
不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,根据它来找系统触发的条件。
可以看到开始执行GC的是 gcStart() 函数,它有一个 gcTrigger 参数,是一个触发条件结构体,它的结构体也很简单。
其实在Golang 内部所有的GC都是通过 gcStart() 函数,然后指定一个 gcTrigger 的参数来开始的,而手动触发指定的条件值为 gcTriggerCycle 。 gcStart 是一个很复杂的函数,有兴趣的可以看一下源码实现。
对于 kind 的值有三种,分别为 gcTriggerHeap 、 gcTriggerTime 和 gcTriggerCycle 。
运行时会通过 gcTrigger.test() 函数来决定是否需要触发GC,只要满足上面基中一个即可。
到此我们基本明白了这三种触发GC的条件,那么对于系统自动触发这种,Golang 从一个程序的开始到运行,它又是如何一步一步监控到这个条件的呢?
其实 runtime 在程序启动时,会在一个初始化函数 init() 里启用一个 forcegchelper() 函数,这个函数位于 proc.go 文件。
为了减少系统资源占用,在 forcegchelper 函数里会通过 goparkunlock() 函数主动让自己陷入休眠,以后由 sysmon() 监控线程根据条件来恢复这个gc goroutine。
可以看到 sysmon() 会在一个 for 语句里一直判断这个 gcTriggerTime 这个条件是否满足,如果满足的话,会将 forcegc.g 这个 goroutine 添加到全局队列里进行调度(这里 forcegc 是一个全局变量)。
调度器在调度循环 runtime.schedule 中还可以通过垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 获取并执行用于后台标记的任务。
golang channel 超时如何处理
个人理解的channel超时处理思路分享,若有错误或者不足,请联系我:qq 869329877
主程序通过go timeout()挂起一个协程,在timeout方法里面利用select来监控逻辑处理的变化,如果请求时间过长或者连接到其他服务比如grpc、mysql等服务中断导致的请求时间过长,则直接超时,超时要返回定义的管道数据结果,否则程序会报错。
原创文章,作者:YSUO,如若转载,请注明出处:https://www.506064.com/n/144385.html