当前位置: 首页 > Linux

踩坑:本周Go服务内存暴涨

时间:2023-04-07 00:11:17 Linux

求一改,记录一下去年踩过的一个大坑。大约从去年八月份开始,当时我们还在用64GB的“小内存”机器。由于升级一次版本需要很长时间(1~2小时),所以我们每天只发一次车,值班同学负责发布所有合并的commit。那天我正在开车值班,突然接到了字节跳动系统的一连串致命电话。我打开Lark看了看:【规则】:机器资源告警【告警上下文】:主机:10.x.x.x内存占用:0.944【告警方式】:Phone&Lark打开Ganglia一看,更吓人:这个样子像一个典型的内存泄漏案例,那么按照正常的套路检查:一方面,通知车上的同学review他们的commit,看看代码中有没有疑似内存泄漏的地方,或者添加的逻辑大量内存?另一方面,我们的go服务默认开启了pprof,所以我们找了一台机器恢复原版来比较内存占用:$gotoolpprofhttp://$IP:$P??ORT/debug/pprof/heap(pprof)top10Showingtop10nodesoutof125flat%sum%cumcum%2925.01MB17.93%17.93%3262.03MB19.99%**[此处代码]**2384.37MB14.61%32.54%4817.78MB29.52%**[codehere]**2142.40MB13.13%45.67%2142.40MB13.13%**[codehere]**...就这样,操作凶猛如虎,大起大落全靠特朗普。最后的结果是,一方面没看出问题,另一方面也没看出问题。在我手足无措准备回滚的时候,内存自己稳定了下来:虽然占用率还是很高,但是没有继续上升,也没有出现OOM的情况。在排查过程中,我们还发现了一个现象:并不是所有的机器都增加了内存。(确实有点“精神”。。。)这些机器的硬件是一致的,但是使用uname-a可以看到内存异常的机器版本是4.14,比本机的3.16高很多machinewithnormalmemory:$uname-a#Linux4.14.81.xxx...$uname-a`Linux3.16.104.xxx...`可见两个内核有些差异版本是原因之一,但还不足以说明前面提到的问题:毕竟这些机器也是在汽车启动前使用的。另外,Y同学提到自己将编译服务指定的go版本从1.10升级到了1.12。当时go1.12已经发布半年了。Y同学在开发环境编译运行正常,在线灰度机也运行了一段时间。看来没什么问题,于是决定升级。既然已经排查了其他的可能性,我们回过头来看看。我们用go1.10重新编译了master,发布到几台内存异常的机器上。所以问题解决了。为什么go1.12会导致内存异常上升?查看Go1.12ReleaseNotes,您可以找到一些线索:RuntimeGo1.12显着提高了大部分堆保持活动状态时清除的性能。这减少了垃圾收集后的分配延迟。内容)在Linux上,运行时现在使用MADV_FREE来释放未使用的内存。这样效率更高,但可能会导致报告的RSS更高。内核会在需要时回收未使用的数据。golang.org/doc/go1.12翻译:在大部分堆内存处于活动状态的情况下,go1.12可以显着提高清理性能并减少[内存分配跟随某个gc]的延迟。在Linux上,Go运行时现在使用MADV_FREE来释放未使用的内存。这样效率更高,但可能会导致更高的RSS;内核在需要时回收此内存。这两段每一个字我都认识,但都写在这里了。我还是尝试解释一下,借用C语言中的malloc和free(Go的内存分配逻辑类似):linux下的内存分配,malloc需要调用brk或者mmap系统调用(syscall)寻找内核扩展其可用地址空间当它管理的内存不够时。这些地址空间对应于前面提到的堆内存(heap)。注意是“扩展地址空间”:因为有些地址空间可能不会立即使用,也可能永远不会使用,为了提高效率,内核不会立即将这些内存分配给进程,而只是在页中标记(可用,但未分配)的进程表。注:OS使用页表来管理进程的地址空间,页表记录了页的状态,对应的物理页地址等信息;一个页面通常是4KB。当进程读/写未分配的页面时,会触发页面错误中断(pagefault),然后内核分配该页面,在页表中标记为已分配,然后恢复执行process(从process的角度看似乎什么都没有发生)。注意:类似的策略在其他很多地方都有使用,包括交换到磁盘(“虚拟内存”)的页面,以及fork后的cow机制。内存回收当我们不使用内存时,调用free(ptr)释放内存。相应地,当free认为有必要时,它会调用sbrk或munmap来缩小地址空间:这是针对腾出一整段地址空间的情况。但更多时候,free可能只释放部分内容(比如ABCDE连续5页只释放C和D),没必要(也不可能)缩小地址空间。这时候最简单的策略就是:什么都不做。但是这种占坑不拉屎的行为会导致内核无法为其他进程分配空闲页面。所以free可以通过madvise告诉内存“我不用这一段”。madvise通过系统调用madvise(addr,length,advise)告诉内核如何处理从addr开始的长度字节。在LinuxKernel4.5之前,只支持MADV_DONTNEED(上面提到的go1.11及更早版本的默认advise),内核会在进程的页表中将这些页标记为“未分配”,这样进程的RSS就会变小.然后操作系统可以将相应的物理页面分配给其他进程。注:RSS是ResidentSetSize(常驻内存集)的缩写,是进程在物理内存中实际占用的内存大小(即页表中实际分配的未换出到内存页的总大小)交换)。我们会在ps命令中看到它,在top命令中是REZ(mantop还有更多惊喜)。madvise标记的地址空间仍然可以被进程访问(无段错误),但是当读/写其中一个页时(例如malloc分配新内存,或者Go创建新对象),内核会重新分配一个全为0的新页面。如果进程大量读写这个地址空间(即releasenotes中的“alargefractionoftheheapremainslive”,大部分堆空间是活跃的),内核需要频繁分配页面并清除页面内容,这会导致分配延迟变高。go1.12的改进从内核4.5开始。Linux支持MADV_FREE(建议在go1.12中默认使用)。内核只会在页表中将这些进程页标记为可回收,并在需要时回收这些页。如果进程在内核回收之前读写了这个空间,它可以继续使用原来的页面。与DONTNEED模式相比,它减少了重新分配内存和清除数据所需的时间。这对应ReleaseNotes中写的“reduces”allocationlatencyimmediatelyfollowingagarbagecollection”,因为内存是在gc之后立即分配的,对应的page很有可能还没有被OS回收。但是代价是"mayresultinhigherreportedRSS".由于页面没有被OS回收,所以还是包含在进程的RSS中,所以看起来进程的内存占用会比较大。到此结束解释,建议你再看一遍:在大部分堆内存处于活动状态的情况下,go1.12可以显着提高清理性能,减少[内存分配跟随某个gc]的延迟。在Linux上,GoRuntime现在使用MADV_FREE来释放未使用的内存。这样效率更高,但可能导致RSS更高;内核会在需要时回收此内存。如果还有不明白的地方,可以留言讨论。有兴趣了解更多详情的同学推荐阅读《What Every Programmer Should Know About Memory》(TL;DR),或其精简版《What a C programmer should know about memory》(参考文末链接)。走到这里,上面提到的内存膨胀问题算是告一段落了,但是Y同学还是有点担心:会不会是某个bug只会出现在Go1.12,导致内存泄露?这个问题有点刁钻,但好像也有道理。毕竟,第一段只是说说而已,像个假把戏。但如何做到这一点?前面提到,go1.12使用了MADV_FREE,内核会在需要的时候回收这些页面。如果我们能想办法让内核觉得有必要回收这些可回收??的页面,那我们就真的可以锤了。熟悉虚拟化(如xen、kvm)的同学可能会觉得这个问题很眼熟:如果宿主机(准确的说是hypervisor)可以回收客户端不再使用的内存,那么就可以超卖更多的VPS,赚取更多钱大大提高了内存的利用率。他们是如何做到的呢?Xen的解决方案是:在客户机中植入一个程序,它的主要工作是申请新的内存。它能申请到的内存就是client不能使用的内存(当然也不能申请太多,否则会导致client使用swap,或者其他进程OOM)。然后主机就可以安全地将这些内存对应的物理页面用于其他目的。这个过程就像是把气球吹大了一样,把客户端能占据的空间都占满了。所以这个程序的名字是:balloondriver。那么真正的锤子计划就要出来了:如果我们也得到一个膨胀的气球(申请内存),内核会觉得需要寻找其他进程来回收FREE标记的内存。只需这样做:#include#include#includeintmain(){char*p=NULL;constintMB=1024*1024;while(1){p=malloc(100*MB);memset(p,0,100*MB);睡觉(1);}return0;}(注意memset,否则不会真正分配内存)效果如下:可以看到,虽然代码进程的VIRT(地址空间大小)还是52G,但是实际占用的内存已经降到35G,气球生效。简单总结一下之前的内容:Go1.12升级可以减少内存分配的延迟,但是会导致进程RSS变高,因为Go1.12使用了MADV_FREE,会使得内核通过在页表中标记为delay来延迟内存回收memory内存的分配和回收可以提高内存管理的效率。气球可用于使OS/Hypervisor占用内存并将其用于其他目的。顺便说一下,面试的时候偶尔会问到这篇文章涉及的一些知识点。有的考生觉得我刁难他,转而问我:“你问的东西都能用在工作上吗?”在字节跳动真的好用,不信你来试试?~发货链接~网联广告(穿山甲)-后台开发(上海)https://job.toutiao.com/s/sBAvKe网联广告(穿山甲)-后台开发(北京)https://job.toutiao.com/s/sBMyxk其他地区,其他功能线https://job.toutiao.com/s/sB9Jqk字节跳动面试详情可以参考我之前写的:《程序员面试指北:面试官视角》参考链接:[1]Go1.12的一次改进关于内存发布https://ms2008.github.io/2019...[2]C程序员应该了解的内存知识https://marek.vavrusa.com/mem...[3]tcmalloc2.1分析https://wertherzhang.com/tcma...