Go内存管理及性能观测工具

2023-11-19

内存管理

TCMalloc

Golang内存分配算法主要源自Google的TCMalloc算法,TCMalloc将内存分成三层最外层Thread Cache、中间层Central Cache、最里层Page Heap。Thread Cache和Central Cache里放着不同size的空闲内存块,相同size的空闲内存块会以链表的形式排布。申请内存分为两种,<=256KB的对象都被认为是小对象,>256KB的被认为是大对象,直接通过Page Heap来获取。大对象分配内存都是以Page为单位,即大对象内存以Page对齐。如果你看懂了下面的逻辑图,那么你已经理解了RCMalloc算法。
在这里插入图片描述

  • Page:操作系统的分页,1Page=8KB;
  • Span:一个Span是由多个Page构成的List;
  • Size Class:将每个小对象(1KB~256KB)分成88个可分配的尺寸等级,每个Size Class对应一个编号,从0开始递增;
  • Thread Cache:每个Thread Cache里对于每个Class Size都有一个单独的Free List,用来缓存N个未被使用的空闲对象。算法为每个线程都分配了一个Thread Cache,所以从中获取/释放内存是不需要加锁的,速度很快.。释放对象时,只需要将对象插入Thread Cache的Size Class对应的FreeList中,不需要加锁,速度也是非常快的;
  • Central Cache:Central Cache中对每个Size Class都维护着Free List。当Thread Cache中没有空闲对象时,会向Central Cache申请对象。Central Cache是所有线程共用的缓存,过程中需要自旋锁。为了平摊自旋锁的开销,Thread Cache会从CentralCache一次性取用或回收多个空闲对象。满足一定条件时,Thread Cache中的空闲对象会放回到Central Cache的Free List中,这个操作是需要加锁的;
  • Page Heap:Page Heap的基础单位是Span(Page List),根据Span的大小分为两种缓存形态,小Span(128个Page以内)通过链表来缓存,大Span会存储在一个有序Set。 当Central Cache中没有空闲对象时,会向Page Heap申请。Central Cache会将Span拆分成Size Class的大小使用;
  • Virtual Memory:虚拟内存。当Page Heap的空闲对象不足时,会向Virtual Memory申请一个或多个Page。向Virtual Memory申请应用程序所使用的对象时,每次至少尝试申请1MBkMinSystemAlloc),申请TCMalloc自身元数据所使用的内存时,每次至少申请8MB(kMetadataAllocChunkSize)。这样既可以减少内存碎片,又均摊了系统调用的开销。

优势

  • 多级缓存,提高内存获取速度;
  • 为每个线程分配独立内存空间,资源隔离、减少线程之间的锁竞争;
  • 将内存分为多个size等级,减少内存碎片,提高内存利用率。使内存的获取和释放简化;
  • 从操作系统获取固定大小(page整数倍)的内存,减少内存碎片、便于内存管理。

go内存分配

Go的内存管理思路和TCMalloc一致,内存池+多级对象管理,在Go里内存管理的对象结构主要是:mheap、mspan、arenas、mcentral、mcache。
在这里插入图片描述

  • page:在Go里Page的大小是固定的8KB;
  • mspan:内存管理的基本单元;
  • mcache:每个P都对应一个mcache,在申请小内存(<=32KB)时直接从mcache获取,不需要加锁;
  • mcentral:包含不同size的mspan(绿色-空闲、红色-已占用),当mcache空闲不足时会向mcentral申请内存;
  • arenas:堆区,动态分配的内存都在这个区域;
  • mheap:代表Go程序持有的所有堆空间。当空间不够时会向系统申请一块64M的内存块,封装成arena来管理。mheap中最多可以管理4194304个arena,每个arena 64MB。

go垃圾回收

垃圾回收(Garbage Collection,GC)就是把程序不用的内存空间视为“垃圾”。这里具体是指程序的堆、栈所占用的内存。GC 要做两件事:标记出需要清理的对象,回收标记的清除对象。标记就是从根节点(栈或者全局变量)扫描,每个根节点扫描到底,扫描所有的根节点。根据三色标记法将对象标记为黑色、灰色、白色;回收标为白色的对象,使其可以被再次利用。

三色标记法
在这里插入图片描述

  1. 所有对象初始状态都是白色;
  2. 从根节点开始扫描,并将引用对象标成灰色;
  3. 遍历灰色节点,将新遍历到的白色节点标记为灰色,并把上一步标记的灰色节点标记为黑色;
  4. 重复上面步骤,直到没有灰色节点;
  5. 回收所有白色节点。

为了避免在GC过程中对象之间的引用关系发生变化,导致GC出错(比如在GC过程中由于未扫描到新的引用对象导致错误清除),会停止所有正在运行的协程,即STW(Stop the world)。STW虽然保证了准确性但是对性能也有影响,那么GC和程序运行是否可以并发进行?
在这里插入图片描述

在图三被标记为黑色的对象新引用了一个白色对象,但是这个黑色对象不会再次被扫描,白色对象一人会被回收,这样会造成很严重的后果。为了解决漏标的问题,需要使用写屏障机制。

写屏障是在内存进行写操作之前执行的,一般需要满足以下两个原理:

  • 强三色不变式,强制性的不允许黑色对象引用白色对象;
  • 弱三色不变式,黑色对象可以引用的白色对象是,有其他灰色对象对它的直接引用,或者它的链路上游存在灰色对象。
    在这里插入图片描述
    插入写屏障,引入新的白色对象时,就将白色对象标记为灰色,满足强三色不变式。处于性能和实现复杂度的考虑,go对栈空间没有使用写屏障,导致新增的引用对象无法及时发现。为了保证程序正常运行,在执行清除回收前,go会执行STW重新扫描一遍栈空间。
    在这里插入图片描述

删除写屏障,在GC过程中如果出现在引用删除,所删除的对象依旧会全部保留下来,满足满足弱三色不变式。虽然不用在此STW但是标记删除粒度比较粗,需要被删除的对象只有在下一轮GC中才会被删除。
在这里插入图片描述
go的垃圾回收是基于三色标记法,通过合理的使用内存屏障,大大较少了垃圾回收的STW。GC开始就将栈上所有的对象标记为黑色,不需要二次扫描,不需要STW;GC期间任何栈上新建对象均标记为黑色;被删除的对象标记为灰色;新增对象标记为灰色。结合了删除、插入写屏障各自优势。

GC时机

主要有两种:

  1. 主动触发:runtime.GC()
  2. 被动触发:定时触发、GC百分比(在下一次垃圾收集必须启动之前可以分配多少新内存的比率,默认为100)

性能观测

名词解释

  • mark:标记阶段;
  • STW:Stop The World,在垃圾回收的某个阶段需要暂停整个应用程序;
  • P:processors,处理器;
  • markTermination:标记结束阶段;
  • mutator assist:辅助GC;
  • dedicated/fractional/idle:在标记阶段会分为三种不同的mark worker模式,分别是dedicated、fractional和idle,它们代表着不同的专注程度,其中dedicated模式最专注,是完整的GC回收行为,fractional只会干部分的GC行为,idle最轻松。(这篇文章你只需要了解它代表不同专注程度的mark worker就行);
  • heap_live:span是GO内存页的基本单元,每页大小为8kb,同时会根据对象大小分配span页数,heap_live就是所有span的总大小。

GODEBUG之gctrace

gctrace主要是观察GC各个阶段耗时及GC后的内存情况。gcvis提供了可视化功能,仅支持GO 1.6版本。下面是一段有内存泄漏的问题代码,执行GODEBUG=gctrace=1 go run demo.go,会得到详细的GC参数

// go 1.19
package main

import (
    "fmt"
    "net/http"
)

// 内存未被释放
var urlList []string

func main() {
    go func() {
        for {
            data := []byte("http://127.0.0.1")
            sData := string(data)
            urlList = append(urlList, sData)
        }
    }()
    http.ListenAndServe("0.0.0.0:6060", nil)
}

命令执行结果:
在这里插入图片描述
下面介绍输出参数的具体含义,以图片的最后一行为例

gc 10 @0.264s 1%: 0.39+5.0+0.034 ms clock, 1.5+3.2/3.9/1.4+0.13 ms cpu, 4->5->2 MB, 5 MB goal, 4 P

  • gc 10:第10次gc
  • @0.264s:当前程序启动后的第0.264秒
  • 1%:程序启动到现在花费在gc上的时间是1%
  • 0.39+5.0+0.034 ms clock:
    • 0.39:单个P在mark阶段的STW时间
    • 5.0:所有P并发标记使用的时间
    • 0.034:单个P在markTermination阶段所用时间
  • 1.5+3.2/3.9/1.4+0.13 ms cpu:
    • 1.5:进程在mark阶段的STW时间
    • 3.2/3.9/1.4:3.2表示mutator assist占用的时间,3.9表示dedicated mark workers + fractional mark workers占用的时间,1.4表示idle占用的时间
    • 0.13:整个进程在markTermination阶段 STW 时间
  • 4->5->2 MB:
    • 4:mark阶段前heap_live 大小。
    • 5:markTermination阶段前heap_live大小。
    • 2:被标记对象的大小
  • 5 MB goal:下次触发GC阈值是5MB
  • 4 P:这次GC一共涉及4个P
  • GC forced: 如果两分钟内没有执行GC,会强制执行一次GC,会换行打印 GC forced

下面贴出官方的解释:

Currently, it is:
    gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
where the fields are as follows:
    gc #        the GC number, incremented at each GC
    @#s         time in seconds since program start
    #%          percentage of time spent in GC since program start
    #+...+#     wall-clock/CPU times for the phases of the GC
    #->#-># MB  heap size at GC start, at GC end, and live heap
    # MB goal   goal heap size
    # P         number of processors used
The phases are stop-the-world (STW) sweep termination, concurrent
mark and scan, and STW mark termination. The CPU times
for mark/scan are broken down in to assist time (GC performed in
line with allocation), background GC time, and idle GC time.
If the line ends with "(forced)", this GC was forced by a
runtime.GC() call and all phases are STW.

pprof

pprof是可视化和分析性能分析数据的工具。下面一段代码

package main

import (
    "fmt"
    "net/http"
    "time"
  _ "net/http/pprof"
)

func main() {
    var forkNum int
    for forkNum < 100 {
        forkWorker(forkNum)
        forkNum++
    }
    http.ListenAndServe("0.0.0.0:6060", nil)
}
func forkWorker(i int) {
    go func(i int) {
        for {
            fmt.Println("worker id ", i, "at ", time.Now().Format("2006-01-02"))
        }
    }(i)
}

上面的代码会一直创建空跑协程。接下来通过pprof工具来分析。

通过web页面

访问http://127.0.0.1:6060/debug/pprof/ 会看到如下页面
在这里插入图片描述

点进子页面能查看到更多的信息。

通过终端交互

执行命令 $ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60
在这里插入图片描述

命令执行后需要等待seconds秒(可调整),此时在pprof的命令交互模式,可以详细查看、导出结果。具体命令执行help查看。
在这里插入图片描述

  • flat:函数上运行耗时
  • flat%:函数CPU运行耗时总比例
  • sum%:函数累积使用 CPU总比例
  • cum:函数加上它之上的调用运行总耗时
  • cum%:函数CPU 运行耗时总比例

$ go tool pprof http://localhost:6060/debug/pprof/heap分析程序常驻内存使用情况

在这里插入图片描述
$ go tool pprof http://localhost:6060/debug/pprof/goroutine分析协程数

在这里插入图片描述

命令$ go tool pprof http://localhost:6060/debug/pprof/***最后的内容可以用web方法页面中的内容替换,会看到不同方向下的内存分配情况。

pprof可视化

安装工具

$ brew install gperftools
$ brew install graphviz

安装graphviz需要很多依赖包,根据报错手动安装对应包。我在安装过程中遇到了gdk-pixbuf安装失败,执行下面命令成功后再次安装graphviz就可以了
$ brew install cairo pango gdk-pixbuf libffi

简单demo

package main

import (
    "testing"
)

func TestAdd(t *testing.T) {
    s := Add()
    if s == "" {
        t.Errorf("Test.Add error!")
    }
}

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add()
    }
}

var urlList []string

func Add() string {
    data := []byte("http://127.0.0.1")
    sData := string(data)
    urlList = append(urlList, sData)
    return sData
}

分别执行下面命令:

$ go test -bench=. -cpuprofile=cpu.prof

$ go tool pprof -http=:8080 cpu.prof

执行后会弹出web页面:

在这里插入图片描述

红色框起来的是二级页面,点进去可以查看更详细的信息。有了这个可视化工具,我们可以清晰的看出函数的调用关系、以及每一步的耗时情况。可以快速的帮我们找到程序的问题。

pprof火焰图

在上面web页面中,点击VIEW->Flame Graph就可以看到火焰图了。
在这里插入图片描述

在上面提到的一步$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10,会产生文件~/pprof/pprof.samples.cpu.003.pb.gz
执行命令$ go tool pprof -http=:8081 ~/pprof/pprof.samples.cpu.003.pb.gz 也会跳转到pprof的可视化界面。

怎么看火焰图

  • 纵轴代表调用栈,调用顺序从上到下
  • 横轴代表函数。一个函数在横轴越宽,说明函数执行时间越长。一个函数横向越长,越有可能是性能瓶颈,但是横轴的长度不等于时长;
  • 如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的;
  • 这里的颜色没有特殊含义,是随机暖色系;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go内存管理及性能观测工具 的相关文章

  • 权重实现随机抽奖

    一般抽奖是怎么实现的 在实习期间学会了一种通用的写法 在这里记录一下 最近在学Golang语法基础 这里就用Golang来写 package main import fmt time math rand func main r rand N
  • go 进阶 gin实战相关: 五. gin_scaffold 企业脚手架

    目录 一 gin scaffold 企业级脚手架 二 gin scaffold 脚手架安装及使用演示 文件分层解释 开始使用 1 配置开启go mod 功能 2 下载 安装 gin scaffold 3 整合 golang common 4
  • Centos7 安装Redis详细教程

    本文主要介绍如果在Centos7下安装Redis 1 安装依赖 redis是由C语言开发 因此安装之前必须要确保服务器已经安装了gcc 可以通过如下命令查看机器是否安装 gcc v 如果没有安装则通过以下命令安装 yum install y
  • Redis热点数据处理

    1 概念 热点数据就是访问量特别大的数据 2 热点数据引起的问题 流量集中 达到物理网卡上限 请求过多 缓存分片服务被打垮 redis作为一个单线程的结构 所有的请求到来后都会去排队 当请求量远大于自身处理能力时 后面的请求会陷入等待 超时
  • 4、动态代理的缓存机制

    1 背景 上一节大致介绍了Proxy动态代理的原理 从几个疑问上面分析 这一节介绍一下动态代理的缓存机制 网上的资源比较少 可以怀着下面几个问题阅读源码 为什么要缓存 缓存的内容是什么 哪里调用的缓存 缓存的实现机制 缓存的过期机制 2 属
  • Redis 分布式缓存

    分布式缓存 单点 Redis 的问题及解决 数据丢失 实现Redis数据持久化 并发能力 搭建主从集群 实现读写分离 存储能力 搭建分片集群 利用插槽机制实现动态扩容 故障恢复能力 利用哨兵机制 实现健康检测和自动恢复 RDB RDB全称R
  • 基于Go语言实现简易Web应用

    目录 前言 Go语言特点 写在使用Go语言实现Web应用前面 创建Web服务器 声明一个结构体操作 加入中间件的使用 使用静态文件服务器 最后 前言 在编程语言中 近几年问世的几个新语言都是非常不错的 比如Go Python Rust等等
  • 为什么最近听说 Go 岗位很少很难?

    大家好 我是煎鱼 其实这个话题已经躺在我的 TODO 里很久了 近来很多社区的小伙伴都私下来交流 也有在朋友圈看到朋友吐槽 Go 上海的大会没什么人 还不如 Rust 大会 比较尴尬 今天主要是看看为什么 Go 岗位看起来近来很难的样子 也
  • 深入理解 Go 语言中的接口(interface)

    一 GoLang 接口的定义 1 GoLang 中的接口 在 Go 语言中接口 interface 是一种类型 一种抽象的类型 接口 interface 定义了一个对象的行为规范 只定义规范不实现 由具体的对象来实现规范的细节 实现接口的条
  • 【go语言开发】Minio基本使用,包括环境搭建,接口封装和代码测试

    本文主要介绍go语言使用Minio对象存储 首先介绍搭建minio 创建bucket等 然后讲解封装minio客户端接口 包括但不限于 上传文件 下载 获取对象url 最后测试开发的接口 文章目录 前言 Minio docker安装mini
  • go-zero 开发入门-加法客服端示例

    定义 RPC 接口文件 接口文件 add proto 的内容如下 syntax proto3 package add 当 protoc gen go 版本大于 1 4 0 时需加上 go package 否则编译报错 unable to d
  • go-zero开发入门-API网关鉴权开发示例

    本文是 go zero开发入门 API网关开发示例 一文的延伸 继续之前请先阅读此文 在项目根目录下创建子目录 middleware 在此目录下创建文件 auth go 内容如下 鉴权中间件 package middleware impor
  • GoLong的学习之路,进阶,Viper(yaml等配置文件的管理)

    本来有今天是继续接着上一章写微服务的 但是这几天有朋友说 再写Web框架的时候 遇到一个问题 就是很多的中间件 redis 微信 mysql mq 的配置信息写的太杂了 很不好管理 希望我能写一篇有管理配置文件的 所以这篇就放到今天写吧 微
  • Go 语言中切片的使用和理解

    切片与数组类似 但更强大和灵活 与数组一样 切片也用于在单个变量中存储相同类型的多个值 然而 与数组不同的是 切片的长度可以根据需要增长和缩小 在 Go 中 有几种创建切片的方法 使用 datatype values 格式 从数组创建切片
  • 【Redis】Redis 配置文件

    1 概述 相同文件 Redis redis 配置 配置文件 redis conf 自定义目录 myredis redis conf 4 1 Units单位 配置大小单位 开头定义了一些基本的度量单位 只支持bytes 不支持bit 大小写不
  • 【Redis】Redis 红锁

    1 概述 上一篇文章 redis Redis 分布式锁 redis session Redlock 红锁 Zookeeper锁 本章节主要讲解redis中的红锁 假设我们有个客户端要获取锁 然后向master去获取锁 然后master会把锁
  • go开发--操作mysql数据库

    在 Go 中访问 MySQL 数据库并进行读写操作通常需要使用第三方的 MySQL 驱动 Go 中常用的 MySQL 驱动有 github com go sql driver mysql 和 github com go xorm xorm
  • Golang拼接字符串性能对比

    g o l a n g golang g o l an g
  • go-carbon v2.3.4 发布,轻量级、语义化、对开发者友好的 Golang 时间处理库

    carbon 是一个轻量级 语义化 对开发者友好的 golang 时间处理库 支持链式调用 目前已被 awesome go 收录 如果您觉得不错 请给个 star 吧 github com golang module carbon gite
  • 【go语言】读取toml文件

    一 简介 TOML 全称为Tom s Obvious Minimal Language 是一种易读的配置文件格式 旨在成为一个极简的数据序列化语言 TOML的设计原则之一是保持简洁性 易读性 同时提供足够的灵活性以满足各种应用场景 TOML

随机推荐