调试 Go 中简单的内存泄漏
内存泄漏 是一种即使当某块内存不再使用之后仍然没有被释放而产生的 bug。通常来说,它们是非常明显的,高度可见的,这使得它们成为学习程序调试的最佳选择。Go 是一门特别适合识别定位内存泄漏的语言,因为它有一套强大的工具链,这套工具链配备了非常强大的工具(pprof),它可以非常轻松地查明内存的使用情况。
我希望这篇文章能够演示如何直观地识别内存,并将其使用范围缩小至特定的进程内,将进程的泄漏与我们的工作关联起来,最后使用 pprof 工具找到内存泄漏的根源。设计这篇博客文章的初衷是为了简单地识别产生内存泄漏的根本原因。我们对 pprof 工具只做简单的功能介绍,不会对其做详细功能的描述。
这里 提供了本文用来生成数据的服务。
什么是内存泄漏?
如果内存的使用率无限增长且永远达不到稳定的状态,那么极有可能存在内存泄漏。这里的关键在于,内存野蛮增长且无法达到稳定状态,并最终会导致程序明显地崩溃或者会产生影响系统性能的问题。
产生内存泄漏的原因有很多种。有可能是数据结构无限制增长导致的逻辑泄漏,亦或是来自复杂的糟糕对象的引用产生的泄漏,又或者是因为其他原因。不管是什么原因导致的内存泄漏,就大多数的内存泄漏的现象而言,它们都会呈现出一种『锯齿状』的形态。
调试过程
这篇博客文章旨在探索如何定位和查明 Go 中内存泄漏的根本原因。我们主要关注内存泄漏的特征,以及如何定位它们,并且学习如何使用 Go(的工具链)来确定产生它们(内存泄漏)的根本原因。因此,我们实际的调试过程可能会比较浅显。
我们分析的目的是逐步缩小问题的范围,排除各种可能性,直到我们拥有足够的信息来确定某个假设能够成立。 在我们有足够的数据和合理的根因范围之后,我们应该提出一个假设,并试图用数据佐证这个假设是否成立。
我们调试的每一步都将试图找出问题的根本原因或者证明这个假设是是否成立。在此过程中,我们将形成一系列的假设,首先它们必须是通用的,然后逐步具体化。大体上来说,这是基于科学的方法。Brandon Gregg 在覆盖系统研究的不同方法(他主要关注性能)这方面做的非常出色。
再次重申下,我们将会按照下面的方式逐步尝试:
- 先提出一个问题
- 形成一个假设
- 分析这个假设
- 重复这个过程直到发现根因
定位问题
我们是如何知道当前的系统存在某个问题(即内存泄漏)?有明显的错误是一个问题的直接表象。对于内存泄漏来说,一般的错误类型就是: OOM(Out Of Memory) 错误或者是一个明显的程序崩溃。
OOM 错误
错误是问题出现的明确指标。虽然用户或应用程序在某段逻辑关闭可能会产生一些误报的错误,但 OOM 错误不同,它实际上表示的是操作系统使用了过多的内存。下面清单列举出的错误,是因为触发了 Cgroup 的限制导致容器被杀死而产生的。
dmesg
[14808.063890] main invoked oom-killer: gfp_mask=0x24000c0, order=0, oom_score_adj=0 [7/972]
[14808.063893] main CPUset=34186d9bd07706222bd427bb647ceed81e8e108eb653ff73c7137099fca1cab6 mems_allowed=0
[14808.063899] CPU: 2 PID: 11345 Comm: main Not tainted 4.4.0-130-generic #156-Ubuntu
[14808.063901] Hardware name: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
[14808.063902] 0000000000000286 ac45344c9134371f ffff8800b8727c88 ffffffff81401c43
[14808.063906] ffff8800b8727d68 ffff8800b87a5400 ffff8800b8727cf8 ffffffff81211a1e
[14808.063908] ffffffff81cdd014 ffff88006a355c00 ffffffff81e6c1e0 0000000000000206
[14808.063911] Call Trace:
[14808.063917] [<ffffffff81401c43>] dump_stack+0x63/0x90
[14808.063928] [<ffffffff81211a1e>] dump_header+0x5a/0x1c5
[14808.063932] [<ffffffff81197dd2>] oom_kill_process+0x202/0x3c0
[14808.063936] [<ffffffff81205514>] ? mem_cgroup_iter+0x204/0x3a0
[14808.063938] [<ffffffff81207583>] mem_cgroup_out_of_memory+0x2b3/0x300
[14808.063941] [<ffffffff8120836d>] mem_cgroup_oom_synchronize+0x33d/0x350
[14808.063944] [<ffffffff812033c0>] ? kzalloc_node.constprop.49+0x20/0x20
[14808.063947] [<ffffffff81198484>] pagefault_out_of_memory+0x44/0xc0
[14808.063967] [<ffffffff8106d622>] mm_fault_error+0x82/0x160
[14808.063969] [<ffffffff8106dae9>] __do_page_fault+0x3e9/0x410
[14808.063972] [<ffffffff8106db32>] do_page_fault+0x22/0x30
[14808.063978] [<ffffffff81855c58>] page_fault+0x28/0x30
[14808.063986] Task in /docker/34186d9bd07706222bd427bb647ceed81e8e108eb653ff73c7137099fca1cab6 killed as a result of limit of /docker/34186d9bd07706222bd427bb647ceed81e8e108eb653ff73c7137099fca1cab6
[14808.063994] memory: usage 204800kB, limit 204800kB, failcnt 4563
[14808.063995] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0
[14808.063997] kmem: usage 7524kB, limit 9007199254740988kB, failcnt 0
[14808.063986] Task in /docker/34186d9bd07706222bd427bb647ceed81e8e108eb653ff73c7137099fca1cab6 killed as a result of limit of /docker/34186d9bd07706222bd427bb647ceed81e8e108eb653ff73c7137099fca1cab6
[14808.063994] memory: usage 204800kB, limit 204800kB, failcnt