内存泄漏定位与排查:Heap Profiling 原理解析

网友投稿 602 2024-02-04

系统长时间运行之后,可用内存越来越少,甚至导致了某些服务失败,这就是典型的内存泄漏问题这类问题通常难以预测,也很难通过静态代码梳理的方式定位Heap Profiling 就是帮助我们解决此类问题的TiKV 作为分布式系统的一部分,已经初步拥有了 Heap Profiling 的能力。

内存泄漏定位与排查:Heap Profiling 原理解析

本文将介绍一些常见的 Heap Profiler 的实现原理及使用方法,帮助读者更容易地理解 TiKV 中相关实现,或将这类分析手段更好地运用到自己项目中什么是 Heap Profiling运行时的内存泄漏问题在很多场景下都相当难以排查,因为这类问题通常难以预测,也很难通过静态代码梳理的方式定位。

Heap Profiling 就是帮助我们解决此类问题的Heap Profiling 通常指对应用程序的堆分配进行收集或采样,来向我们报告程序的内存使用情况,以便分析内存占用原因或定位内存泄漏根源Heap Profiling 是如何工作的

作为对比,我们先简单了解下 CPU Profiling 是如何工作的当我们准备进行 CPU Profiling 时,通常需要选定某一时间窗口,在该窗口内,CPU Profiler 会向目标程序注册一个定时执行的 hook(有多种手段,譬如 SIGPROF 信号),在这个 hook 内我们每次会获取业务线程此刻的 stack trace。

我们将 hook 的执行频率控制在特定的数值,譬如 100hz,这样就做到每 10ms 采集一个业务代码的调用栈样本当时间窗口结束后,我们将采集到的所有样本进行聚合,最终得到每个函数被采集到的次数,相较于总样本数也就得到了每个函数的。

相对占比借助此模型我们可以发现占比较高的函数,进而定位 CPU 热点在数据结构上,Heap Profiling 与 CPU Profiling 十分相似,都是 stack trace + statistics 的模型。

如果你使用过 Go 提供的 pprof,会发现二者的展示格式是几乎相同的:Go CPU ProfileGo Heap Profile与 CPU Profiling 不同的是,Heap Profiling 的数据采集工作并非简单通过定时器开展,而是需要侵入到内存分配路径内,这样才能拿到内存分配的数量。

所以 Heap Profiler 通常的做法是直接将自己集成在内存分配器内,当应用程序进行内存分配时拿到当前的 stack trace,最终将所有样本聚合在一起,这样我们便能知道每个函数直接或间接地内存分配数量

了Heap Profile 的 stack trace + statistics 数据模型与 CPU Proflie 是一致的接下来我们将介绍多款 Heap Profiler 的使用和实现原理注:诸如 。

GNU gprof、Valgrind 等工具的使用场景与我们的目的场景不匹配,因此本文不会展开原因参考 gprof, Valgrind and gperftools - an evaluation of some tools for application level CPU profiling on Linux - Gernot.Klingler。

Heap Profiling in Go大部分读者应该对 Go 会更加熟悉一些,因此我们以 Go 为起点和基底来进行调研注:如果一个概念我们在靠前的小节讲过了,后边的小节则不再赘述,即使它们不是同一个项目。

另外出于完整性目的,每个项目都配有 usage 小节来阐述其用法,对此已经熟悉的同学可以直接跳过UsageGo runtime 内置了方便的 profiler,heap 是其中一种类型我们可以通过如下方式开启一个 debug 端口:。

import _ net/http/pprof go func(){ log.Print(http.ListenAndServe(0.0.0.0:9999, nil))}()Copy然后在程序运行期间使用命令行拿到当前的 Heap Profiling 快照:

?$ go tool pprof http://127.0.0.1:9999/debug/pprof/heapCopy或者也可以在应用程序代码的特定位置直接获取一次 Heap Profiling 快照:

importruntime/pprof pprof.WriteHeapProfile(writer)Copy这里我们用一个完整的 demo 来串一下 heap pprof 的用法:package main

import(lognet/http _ net/http/pproftime) func main(){ go func(){ log.Fatal(http.ListenAndServe(:9999

, nil))}() var data [][]byte for{ data = func1(data) time.Sleep(1 * time.Second)}} func func1(data

[][]byte)[][]byte { data = func2(data)return append(data, make([]byte, 1024*1024)) // alloc 1mb } func func2

(data [][]byte)[][]byte {return append(data, make([]byte, 1024*1024)) // alloc 1mbCopy代码持续地在 func1 和 func2 分别进行内存分配,每秒共分配 2mb 堆内存。

将程序运行一段时间后,执行如下命令拿到 profile 快照并开启一个 web 服务来进行浏览:$ go tool pprof -http=:9998 localhost:9999/debug/pprof/heap

CopyGo Heap Graph从图中我们能够很直观的看出哪些函数的内存分配占大头(方框更大),同时也能很直观的看到函数的调用关系(通过连线)譬如上图中很明显看出是 func1 和 func2 的分配占大头,且 func2 被 func1 调用。

注意,由于 Heap Profiling 也是采样的(默认每分配 512k 采样一次),所以这里展示的内存大小要小于实际分配的内存大小同 CPU Profiling 一样,这个数值仅仅是用于计算相对占比。

,进而定位内存分配热点注:事实上,Go runtime 对采样到的结果有估算原始大小的逻辑,但这个结论并不一定准确此外,func1 方框中的 48.88% of 90.24% 表示 Flat% of Cum%。

什么是 Flat% 和 Cum%?我们先换一种浏览方式,在左上角的 View 栏下拉点击 Top:Go Heap TopName 列表示相应的函数名Flat 列表示该函数自身分配了多少内存Flat% 列表示 Flat 相对总分配大小的占比

Cum 列表示该函数,及其调用的所有子函数一共分配了多少内存Cum% 列表示 Cum 相对总分配大小的占比Sum% 列表示自上而下 Flat% 的累加(可以直观的判断出从哪一行往上一共分配的多少内存) 上述两种方式可以帮助我们定位到具体的函数,Go 提供了更细粒度的代码行数级别的分配源统计,在左上角 View 栏下拉点击 Source:

Go Heap Source在 CPU Profiling 中我们常用火焰图找宽顶来快速直观地定位热点函数当然,由于数据模型的同质性,Heap Profiling 数据也可以通过火焰图来展现,在左上角 View 栏下拉点击 Flame Graph:。

Go Heap Flamegraph通过上述各种方式我们可以很简单地看出内存分配大头在 func1 和 func2然而现实场景中绝不会这么简单就让我们定位到问题根源,由于我们拿到的是某一刻的快照,对于内存泄漏问题来说这并不够用,我们需要的是一个增量数据,来判断哪些内存在持续地增长。

所以可以在间隔一定时间后再获取一次 Heap Profile,对两次结果做 diffImplementation details本节我们重点关注 Go Heap Profiling 的实现原理回顾 “Heap Profiling 是如何工作的” 一节,Heap Profiler 通常的做法是直接将自己集成在内存分配器内,当应用程序进行内存分配时拿到当前的 stack trace,而 Go 正是这么做的。

Go 的内存分配入口是 src/runtime/malloc.go 中的 mallocgc() 函数,其中一段关键代码如下:func mallocgc(size uintptr, typ *_type, needzero bool

) unsafe.Pointer { // ... if rate := MemProfileRate; rate >0{ // Note cache c only valid while m acquired

; see #47302if rate !=1&& size < c.nextSample { c.nextSample -= size }else{ profilealloc(mp, x, size

)}} // ... } func profilealloc(mp *m, x unsafe.Pointer, size uintptr){ c := getMCache()if c == nil

{ throw(profilealloc called without a P or outside bootstrapping)} c.nextSample = nextSample() mProf_Malloc

(x, size)}Copy这也就意味着,每通过 mallocgc() 分配 512k 的堆内存,就调用 profilealloc() 记录一次 stack trace为什么需要定义一个采样粒度?每次 mallocgc() 都记录下当前的 stack trace 不是更准确吗?。

完全精确地拿到所有函数的内存分配看似更加吸引人,但这样带来的性能开销是巨大的malloc() 作为用户态库函数会被应用程序非常频繁地调用,优化内存分配性能是 allocator 的责任如果每次 malloc() 调用都伴随一次栈回溯,带来的开销几乎是不可接受的,尤其是在服务端长期持续进行 profiling 的场景。

选择 “采样” 并非结果上更优,而仅仅是一种妥协当然,我们也可以自行修改 MemProfileRate 变量,将其设置为 1 会导致每次 mallocgc() 必定进行 stack trace 记录,设置为 0 则会完全关闭 Heap Profiling,用户可以根据实际场景来权衡性能与精确度。

注意,当我们将 MemProfileRate 设置为一个通常的采样粒度时,这个值并不是完全精确的,而是每次都在以 MemProfileRate 为平均值的指数分布中随机取一个值// nextSample returns the next sampling point 。

for heap profiling. The goal is // to sample allocations on average every MemProfileRate bytes, but with a // completely random distribution over the allocation timeline

; this // corresponds to a Poisson process with parameter MemProfileRate. In Poisson // processes, the distance between two samples follows the exponential // distribution

(exp(MemProfileRate)), so the best return value is a random // number taken from an exponential distribution whose mean is MemProfileRate. func nextSample

() uintptrCopy由于很多情况下内存分配是有规律的,如果按照固定的粒度进行采样,最终得到的结果可能会存在很大的误差,可能刚好每次采样都赶上了某个特定类型的内存分配这就是这里选择随机化的原因不只是 Heap Profiling,基于 sampling 的各类 profiler 总会有一些误差存在(例:

SafePoint Bias),在审阅基于 sampling 的 profiling 结果时,需要时刻提醒自己不要忽视误差存在的可能性位于 src/runtime/mprof.go 的 mProf_Malloc() 函数负责具体的采样工作:。

// Called by malloc to record a profiled block. func mProf_Malloc(p unsafe.Pointer, size uintptr){ var stk

[maxStack]uintptr nstk := callers(4, stk[:]) lock(&proflock) b := stkbucket(memProfile, size, stk[

:nstk], true) c := mProf.cycle mp := b.mp() mpc :=&mp.future[(c+2)%uint32(len(mp.future))] mpc.allocs++ mpc.alloc_bytes

+= size unlock(&proflock) // Setprofilebucket locks a bunch of other mutexes, so we call it outside of proflock. // This reduces potential contention and chances of deadlocks. // Since the object must be alive during call to mProf_Malloc, // its fine to

do this non-atomically. systemstack(func(){ setprofilebucket(p, b)})} func callers(skip int, pcbuf

[]uintptr) int { sp := getcallersp() pc := getcallerpc() gp := getg() var n int systemstack(func

(){ n = gentraceback(pc, sp, 0, gp, skip, &pcbuf[0], len(pcbuf), nil, nil, 0)})return n }Copy通过调用 callers() 以及进一步的 gentraceback() 来获取当前调用栈保存在 stk 数组中(即 PC 地址的数组),这一技术被称为调用栈回溯,在很多场景均有应用(譬如程序 panic 时的栈展开)。

注:术语 PC 指 Program Counter,特定于 x86-64 平台时为 RIP 寄存器;FP 指 Frame Pointer,特定于 x86-64 时为 RBP 寄存器;SP 指 Stack Pointer,特定于 x86-64 时为 RSP 寄存器。

一种原始的调用栈回溯实现方式是在函数调用约定(Calling Convention)上保证发生函数调用时 RBP 寄存器(on x86-64)保存的一定是栈基址,而不再作为通用寄存器使用,由于 call 指令会首先将 RIP (返回地址)入栈,我们只要保证接下来入栈的第一条数据是当前的 RBP,那么所有函数的栈基址就以 RBP 为头,串成了一条地址链表。

我们只需为每个 RBP 地址向下偏移一个单元,便能拿到 RIP 的数组了Go FramePointer Backtrace(图片来自 go-profiler-notes)注:图中提到 Go 的所有参数均通过栈传递,这一结论现在已经过时了,Go 从 1.17 版本开始支持寄存器传参。

由于 x86-64 将 RBP 归为了通用寄存器,诸如 GCC 等编译器默认不再使用 RBP 保存栈基址,除非使用特定的选项将其打开然而 Go 编译器却保留了这个特性,因此在 Go 中通过 RBP 进行栈回溯是可行的。

但 Go 并没有采用这个简单的方案,原因是在某些特殊场景下该方案会带来一些问题,譬如如果某个函数被 inline 掉了,那么通过 RBP 回溯得到的调用栈就是缺失的另外这个方案需要在常规函数调用间插入额外的指令,且需要额外占用一个通用寄存器,存在一定的性能开销,即使我们不需要栈回溯。

每个 Go 的二进制文件都包含一个名为 gopclntab 的 section,这是 Go Program Counter Line Table 的缩写,它维护了 PC 到 SP 及其返回地址的映射这样我们就无需依赖 FP,便能。

直接通过查表的方式完成 PC 链表的串联同时 gopclntab 中维护了 PC 及其所处函数是否已被内联优化的信息,所以我们在栈回溯过程中便不会丢失内联函数帧此外 gopclntab 还维护了符号表,保存 PC 对应的代码信息(函数名,行数等),所以我们最终才能看到人类可读的 panic 结果或者 profiling 结果,而不是一大坨地址信息。

gopclntab与特定于 Go 的 gopclntab 不同,DWARF 是一种标准化的调试格式,Go 编译器同样为其生成的 binary 添加了 DWARF (v4) 信息,所以一些非 Go 生态的外部工具可以依赖它对 Go 程序进行调试。

值得一提的是,DWARF 所包含的信息是 gopclntab 的超集回到 Heap Profiling 来,当我们通过栈回溯技术(前边代码中的 gentraceback() 函数)拿到 PC 数组后,并不需要着急直接将其符号化,符号化的开销是相当可观的,我们完全可以先通过指针地址栈进行聚合。

所谓的聚合就是在 hashmap 中对相同的样本进行累加,相同的样本指的是两个数组内容完全一致的样本通过 stkbucket() 函数以 stk 为 key 获取相应的 bucket,然后将其中统计相关的字段进行累加。

另外,我们注意到 memRecord 有多组 memRecordCycle 统计数据:type memRecord struct { active memRecordCycle future [3]

memRecordCycle }Copy在累加时是通过 mProf.cycle 全局变量作为下标取模来访问某组特定的 memRecordCyclemProf.cycle 每经过一轮 GC 就会递增,这样就记录了三轮 GC 间的分配情况。

只有当一轮 GC 结束后,才会将上一轮 GC 到这一轮 GC 之间的内存分配、释放情况并入最终展示的统计数据中这个设计是为了避免在 GC 执行前拿到 Heap Profile,给我们看到大量无用的临时内存。

并且,在一轮 GC 周期的不同时刻我们也可能会看到不稳定的堆内存状态最终调用 setprofilebucket() 将 bucket 记录到此次分配地址相关的 mspan 上,用于后续 GC 时调用 mProf_Free() 来记录相应的释放情况。

就这样,Go runtime 中始终维护着这份 bucket 集合,当我们需要进行 Heap Profiling 时(譬如调用 pprof.WriteHeapProfile() 时),就会访问这份 bucket 集合,转换为 pprof 输出所需要的格式。

这也是 Heap Profiling 与 CPU Profiling 的一个区别:CPU Profiling 只在进行 profiling 的时间窗口内对应用程序存在一定采样开销,而 Heap Profiling 的采样是无时无刻不在发生的,

执行一次 profiling 仅仅是 dump 一下迄今为止的数据快照接下来我们将进入 C/C++/Rust 的世界,幸运的是,由于大部分 Heap Profiler 的实现原理是类似的,前文所述的很多知识在后文对应的上。

最典型的,Go Heap Profiling 其实就是从 Google tcmalloc 移植而来的,它们具备相似的实现方式Heap Profiling with gperftoolsgperftools

(Google Performance Tools)是一套工具包,包括 Heap Profiler、Heap Checker、CPU Profiler 等工具之所以在 Go 之后紧接着介绍它,是因为它与 Go 有很深的渊源。

前文提到的 Go runtime 所移植的 Google tcmalloc 从内部分化出了两个社区版本:一个是 tcmalloc,即纯粹的 malloc 实现,不包含其它附加功能;另一个就是 gperftools

,是带 Heap Profiling 能力的 malloc 实现,以及配套的其它工具集其中 pprof 也是大家最为熟知的工具之一pprof 早期是一个 perl 脚本,后来演化成了 Go 编写的强大工具 。

pprof,现在已经被集成到了 Go 主干,平时我们使用的 go tool pprof 命令内部就是直接使用的 pprof 包注:gperftools 的主要作者是 Sanjay Ghemawat,与 Jeff Dean 结对编程的牛人。

UsageGoogle 内部一直在使用 gperftools 的 Heap Profiler 分析 C++ 程序的堆内存分配,它可以做到:Figuring out what is in the program heap at any given time

Locating memory leaksFinding places that do a lot of allocation作为 Go pprof 的祖先,看起来和 Go 提供的 Heap Profiling 能力是相同的。

Go 是直接在 runtime 中的内存分配函数硬编入了采集代码,与此类似,gperftools 则是在它提供的 libtcmalloc 的 malloc 实现中植入了采集代码用户需要在项目编译链接阶段执行 -ltcmalloc 链接该库,以替换 libc 默认的 malloc 实现。

当然,我们也可以依赖 Linux 的动态链接机制来在运行阶段进行替换:$ envLD_PRELOAD=/usr/lib/libtcmalloc.soCopy当使用 LD_PRELOAD 指定了 libtcmalloc.so 后,我们程序中所默认链接的 malloc() 就被覆盖了,Linux 的动态链接器保证了优先执行 LD_PRELOAD 所指定的版本。

在运行链接了 libtcmalloc 的可执行文件之前,如果我们将环境变量 HEAPPROFILE 设置为一个文件名,那么当程序执行时,Heap Profile 数据就会被写入该文件在默认情况下,每当我们的程序分配了 1g 内存时,或每当程序的内存使用高水位线增加了 100mb 时,都会进行一次 Heap Profile 的 dump。

这些参数可以通过环境变量来修改使用 gperftools 自带的 pprof 脚本可以分析 dump 出来的 profile 文件,用法与 Go 基本相同 $ pprof --gv gfs_master /tmp/profile.0100.heap。

Copygperftools gv$ pprof --text gfs_master /tmp/profile.0100.heap 255.624.7% 24.7% 255.624.7% GFS_MasterChunk::AddServer

184.617.8% 42.5% 298.828.8% GFS_MasterChunkTable::Create 176.217.0% 59.5% 729.970.5% GFS_MasterChunkTable::UpdateState

169.816.4% 75.9% 169.816.4% PendingClone::PendingClone 76.37.4% 83.3% 76.37.4% __default_alloc_template::_S_chunk_alloc

49.54.8% 88.0% 49.54.8% hashtable::resize ...Copy同样的,从左到右依次是 Flat(mb),Flat%,Sum%,Cum(mb),Cum%,Name。

Implementation details类似的,tcmalloc 在 malloc() 和 operator new 中增加了一些采样逻辑,当根据条件触发采样 hook 时,会执行以下函数:// Record an allocation

in the profile. static void RecordAlloc(const void* ptr, size_t bytes, int skip_count){ // Take the stack trace outside the critical section. void* stack

[HeapProfileTable::kMaxStackDepth]; int depth = HeapProfileTable::GetCallerStackTrace(skip_count +

1, stack); SpinLockHolder l(&heap_lock);if(is_on){ heap_profile->RecordAlloc(ptr, bytes, depth, stack

); MaybeDumpProfileLocked();}} void HeapProfileTable::RecordAlloc( const void* ptr, size_t bytes, int stack_depth, const void* const call_stack

[]){ Bucket* b = GetBucket(stack_depth, call_stack); b->allocs++; b->alloc_size += bytes; total_.allocs++

; total_.alloc_size += bytes; AllocValue v; v.set_bucket(b); // also did set_live(false); set_ignore

(false) v.bytes = bytes; address_map_->Insert(ptr, v);}Copy执行流程如下:调用 GetCallerStackTrace() 获取调用栈以调用栈作为 hashmap 的 key 调用 GetBucket() 获取相应的 Bucket。

累加 Bucket 中的统计数据由于没有了 GC 的存在,采样流程相比 Go 简单了许多从变量命名上来看,Go runtime 中的 profiling 代码的确是从这里移植过去的sampler.h 中详细描述了 gperftools 的采样规则,总的来说也和 Go 一致,即:512k average sample step。

在 free() 或 operator delete 中同样需要增加一些逻辑来记录内存释放情况,这比拥有 GC 的 Go 同样要简单不少:// Record a deallocation in the profile. static void RecordFree

(const void* ptr){ SpinLockHolder l(&heap_lock);if(is_on){ heap_profile->RecordFree(ptr); MaybeDumpProfileLocked

();}} void HeapProfileTable::RecordFree(const void* ptr){ AllocValue v;if(address_map_->FindAndRemove

(ptr, &v)){ Bucket* b = v.bucket(); b->frees++; b->free_size += v.bytes; total_.frees++

; total_.free_size += v.bytes;}}Copy找到相应的 Bucket,累加 free 相关的字段即可现代 C/C++/Rust 程序获取调用栈的过程通常是依赖 libunwind 库进行的,libunwind 进行栈回溯的原理上与 Go 类似,都没有选择 Frame Pointer 回溯模式,都是依赖程序中的某个特定 section 所记录的 unwind table。

不同的是,Go 所依赖的是自己生态内创建的名为 gopclntab 的特定 section,而 C/C++/Rust 程序依赖的是 .debug_frame section 或 .eh_frame section。

其中 .debug_frame 为 DWARF 标准定义,Go 编译器也会写入这个信息,但自己不用,只留给第三方工具使用GCC 只有开启 -g 参数时才会向 .debug_frame 写入调试信息而 .eh_frame 则更为现代一些,在

Linux Standard Base 中定义原理是让编译器在汇编代码的相应位置插入一些伪指令(CFI Directives,Call Frame Information),来协助汇编器生成最终包含 unwind table 的 .eh_frame section。

以如下代码为例:// demo.c int add(int a, int b){return a + b;}Copy我们使用 cc -S demo.c 来生成汇编代码(gcc/clang 均可),注意这里并没有使用 -g 参数。

.section __TEXT,__text,regular,pure_instructions .build_version macos, 11, 0 sdk_version 11, 3 .globl _add

## -- Begin function add .p2align 4, 0x90 _add: ## @add .cfi_startproc

## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp movl %edi, -4

(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %eax addl -8(%rbp), %eax popq %rbp retq .cfi_endproc

## -- End function .subsections_via_symbolsCopy从生成的汇编代码中可以看到许多以 .cfi_ 为前缀的伪指令,它们便是 CFI DirectivesHeap Profiling with jemalloc。

接下来我们关注 jemalloc,这是因为 TiKV 默认使用 jemalloc 作为内存分配器,能否在 jemalloc 上顺利地进行 Heap Profiling 是值得我们关注的要点Usagejemalloc 自带了 Heap Profiling 能力,但默认是不开启的,需要在编译时指定 --enable-prof 参数。

./autogen.sh ./configure --prefix=/usr/local/jemalloc-5.1.0 --enable-prof makemakeinstallCopy与 tcmalloc 相同,我们可以选择通过 -ljemalloc 将 jemalloc 链接到程序,或通过 LD_PRELOAD 用 jemalloc 覆盖 libc 的 malloc() 实现。

我们以 Rust 程序为例展示如何通过 jemalloc 进行 Heap Profilingfn main(){let mut data = vec![]; loop { func1。

(&mut data); std::thread::sleep(std::time::Duration::from_secs(1));}} fn func1(data: &mut Vec

){ data.push(Box::new([0u8;1024*1024])); // alloc 1mb func2(data);} fn func2

(data: &mut Vec){ data.push(Box::new([0u8;1024*1024])); // alloc 1mb }Copy与 Go 一节中提供的 demo 类似,我们同样在 Rust 中每秒分配 2mb 堆内存,func1 和 func2 各分配 1mb,由 func1 调用 func2。

直接使用 rustc 不带任何参数编译该文件,然后执行如下命令启动程序:$ exportMALLOC_CONF=prof:true,lg_prof_interval:25 $ exportLD_PRELOAD

=/usr/lib/libjemalloc.so $ ./demoCopyMALLOC_CONF 用于指定 jemalloc 的相关参数,其中 prof:true 表示开启 profiler,log_prof_interval:25 表示每分配 2^25 字节(32mb)堆内存便 dump 一份 profile 文件。

注:更多 MALLOC_CONF 选项可以参考文档等待一段时间后,即可看到有一些 profile 文件产生jemalloc 提供了一个和 tcmalloc 的 pprof 类似的工具,叫 jeprof,事实上它就是由 pprof perl 脚本 fork 而来的,我们可以使用 jeprof 来审阅 profile 文件。

$ jeprof ./demo jeprof.7262.0.i0.heapCopy同样可以生成与 Go/gperftools 相同的 graph:$ jeprof --gv ./demo jeprof.7262.0.i0.heap

Copyjeprof svgImplementation details与 tcmalloc 类似,jemalloc 在 malloc() 中增加了采样逻辑:JEMALLOC_ALWAYS_INLINE int imalloc_body

(static_opts_t *sopts, dynamic_opts_t *dopts, tsd_t *tsd){ // ... // If profiling is on, get our profiling context.

if(config_prof && opt_prof){ bool prof_active = prof_active_get_unlocked(); bool sample_event = te_prof_sample_event_lookahead

(tsd, usize); prof_tctx_t *tctx = prof_alloc_prep(tsd, prof_active, sample_event); emap_alloc_ctx_t alloc_ctx

;if(likely((uintptr_t)tctx ==(uintptr_t)1U)){ alloc_ctx.slab =(usize <= SC_SMALL_MAXCLASS); allocation

= imalloc_no_sample( sopts, dopts, tsd, usize, usize, ind);}elseif((uintptr_t)tctx >(uintptr_t

)1U) { allocation = imalloc_sample( sopts, dopts, tsd, usize, ind); alloc_ctx.slab = false

; } else { allocation = NULL; } if (unlikely(allocation == NULL)){ prof_alloc_rollback(tsd, tctx

); goto label_oom;} prof_malloc(tsd, allocation, size, usize, &alloc_ctx, tctx);}else{ assert(

!opt_prof); allocation = imalloc_no_sample(sopts, dopts, tsd, size, usize, ind);if(unlikely(allocation

== NULL)){ goto label_oom;}} // ... }Copy在 prof_malloc() 中调用 prof_malloc_sample_object() 对 hashmap 中相应的调用栈记录进行累加:

void prof_malloc_sample_object(tsd_t *tsd, const void *ptr, size_t size, size_t usize, prof_tctx_t *tctx

){ // ... malloc_mutex_lock(tsd_tsdn(tsd), tctx->tdata->lock); size_t shifted_unbiased_cnt = prof_shifted_unbiased_cnt

[szind]; size_t unbiased_bytes = prof_unbiased_sz[szind]; tctx->cnts.curobjs++; tctx->cnts.curobjs_shifted_unbiased

+= shifted_unbiased_cnt; tctx->cnts.curbytes += usize; tctx->cnts.curbytes_unbiased += unbiased_bytes

; // ... }Copyjemalloc 在 free() 中注入的逻辑也与 tcmalloc 类似,同时 jema

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:使用 TiDB 构建高效实时应用的策略
下一篇:刘奇观点:传统数据库亟需创新跃迁,TiDB 领航新时代
相关文章