本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是学蒸鱼的建宇。在上一篇介绍Go1.16特性的文章中,我们提到从v1.16开始,Linux下Go默认的内存管理策略将从MADV_FREE改为MADV_DONTNEED策略。这时候,可能至少有两类朋友,即:知其然,被这个问题“折磨”过,瞬间眼前一亮。也不知道是什么东西,各种疑惑生起,到底在说什么。灵魂拷问,你是否有以下疑问,或者你清楚:文中提到的MADV_FREE是什么?文中提到的MADV_DONTNEED是什么?为什么特指Go语言的Linux环境呢?为什么说从MADV_FREE变成MADV_DONTNEED呢?在这篇文章中,我们将进一步展开和解释,让我们了解一下这种修改内存机制是什么。Madvise的爱与恨在Linux系统中,在GoRuntime中,系统调用madvise(addr,length,advise)方法可以告诉内核如何处理从addr开始的length字节。关键之一是“如何应对”。Linux下的Go语言目前支持两种策略,分别是(via@felix021):MADV_FREE:内核会在进程的页表中将这些页标记为“未分配”,这样进程的RSS就会变成更小。操作系统随后可以将相应的物理页面分配给其他进程。MADV_DONTNEED:内核只会在页表中将这些进程页标记为可回收,并在需要时回收这些页。影响Go语言官方在2019年刚刚对Go1.12做了如下调整。在Go1.12之前。Go.12-Go1.15。在Go1.12之前,GoRuntime在Linux上默认使用MADV_DONTNEED策略。//没有奇怪的判断madvise(v,n,_MADV_DONTNEED)从整体效果来看,进程RSS可以下降的比较快,但是在性能效率上差不多。Go1.12-Go1.15当前Linux内核版本>=4.5时,GoRuntime默认在Linux上使用更高效的MADV_FREE策略。varadviseuint32ifdebug.madvdontneed!=0{advise=_MADV_DONTNEED}else{advise=atomic.Load(&adviseUnused)}iferrno:=madvise(v,n,int32(advise));advise==_MADV_FREE&&errno!=0{//MADV_FREE被添加到Linux4中。5.FallbacktoMADV_DONTNEEDifitis//notsupported.atomic.Store(&adviseUnused,_MADV_DONTNEED)madvise(v,n,_MADV_DONTNEED)}从整体效果来看,进程的RSS不会立即下降,直到系统有内存压力才会释放,RSS将下降。副作用往往不是那么好。显然,从Go1.12开始,madvise对MADV_FREE策略的调整是非常“片面”的。社区微信群的小伙伴结合社区遇到的案例可以了解到,这次调整带来了很多问题:条件不满足,内存不立即释放。混淆统计信息和监控工具:在Grafana等监控中,发现容器进程内存高,释放慢,惊慌失措。与内存使用相关的个别管理系统集成度低:比如KubernetesHPA,或者自定义的扩缩容策略,这些很难评估。挤压同一主机上的其他应用资源:并不是所有的Go程序都必须在一台主机上独立运行,这自然会导致同一主机上的其他应用被挤压,这是很难评估的。从社区的反馈来看,问题很多,弊大于利。官方想过更好的性能,但是在实际场景中造成了很多新的问题,甚至提到了与Android进程管理的不兼容。有种“捡到芝麻丢了西瓜”的感觉。Go1.16:转机既然社区反映了这么多问题,有没有人提出来?是的,有这么多。这么多提议改回MADV_DONTNEED的问题,只讨论了1-2天。快速结束并合并CL以关闭问题。Go1.16修改内容如下:funcparsedebugvars(){//defaultsdebug.cgocheck=1debug.invalidptr=1ifGOOS=="linux"{debug.madvdontneed=1}...}直接指定回debug.madvdontneed=1,简单粗暴。总结在本文中,我们对Linux下Go语言的madvise方法的策略调整进行了历史介绍和描述,并介绍了调整带来的副作用和对策。这个变化很好地印证了会调动全身的说法。大家以后在应用这块的时候要多加注意。你如何看待Go1.16中的这个特性变化?欢迎在评论区留言。参考runtime:linux默认为MADV_DONTNEED踩坑:goservicememoryskyrocketsGo1.12是对内存释放的改进
