Go内存分配跟踪调优

今天小编为大家分享一篇关于Go内存分配跟踪调优的文章,文中涉及到一些压测及跟踪分析的工具,以及问题查找方法,希望能对大家有所帮助。

Make it work, make it right, make it fast.

– Kent Beck

最近,我决定对我的一个开源的 Go 项目 Flipt 进行一下深入分析,看能不能找到一些容易获得成绩的点,来优化一下,从而获得性能方面的提升。因为在项目中,路由和中间件都是使用的开源的,所以在这个过程中,我也可以对一些流行的开源项目进行分析,进而可以发现一些它们存在的问题。最终,我的项目可以减少近100倍的内存分配,因此也减少了 GC 回收次数并提高了整体性能。那么来看下我是怎么做到的呢。

1

生成

在开始分析之前,需要先生成大量的网络请求,有了流量才能看到观察到项目中目前存在的问题。这就有个问题,因为我没有在生产环境的项目上使用过 Flipt,那也就没有真正的流量。所以,我使用一款多功能的 HTTP 压测工具, Vegeta,来生成模拟流量。

这个很符合我的需求,它可以在某段时间持续的产生请求,我就可以测量诸如堆分配、堆使用情况、goroutine以及 GC 耗费的时间之类的情况。

经过一些试验,最终得到以下命令:

echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 1000 -duration 1m -body evaluate.json

这个命令以攻击模式来启动 vegeta,以1000次每秒的速率发送 HTTP POST 请求到 Flipt 的 REST API,持续一分钟。发送给 Flipt 用的 JSON 载荷可以不用关心,只要是 Flipt 服务器可以接收的包体即可。

我首先准备向 evaluate 这个接口开炮,因为它里面逻辑比较复杂,在后端有很多的复杂计算,所以应该更有可能暴露出问题。

2

测量

既然我们有解决了流量的问题,那么我们就需要测量,在项目运行时这些流量产生的实际结果。很幸运,Go 自身就提供了一套非常出色的标准工具,我们可以用 pprof 来衡量我们的 Go 应用的性能。关于 pprof 的详细信息,我们在此不深入讨论,后面可能会写相应的文章来单独介绍。

因为在 Flipt 中,我是使用了 go-chi/chi 作为 HTTP 路由,所以在项目中可以很简单的使用 Chi 的配置中间件,来启用 pprof。

我们再开启一个窗口来获取并查看堆的剖面信息:

pprof -http=localhost:9090 localhost:8080/debug/pprof/heap

这里我使用了 Google 的 pprof 工具,该工具可以直接在浏览器中可视化剖面数据。

首先,我检查了 inuse_objects 和 inuse_space 来查看堆现场,但是并没有真正注意到太多内容。但是,当我切换到 alloc_objects 和 alloc_space 的时候,才真正勾起我的兴趣。

看起来好像是调用了 flate.NewWriter,并且在一分钟的时间内分配了 19370 MB的内存。已经超过 19G!很显然这其中发生了什么事情。但是呢?如果放大图并仔细查看,可以看到 flate.NewWriter 是从gzip.(* Writer).Write 调用的,而它是从 middleware.(* compressResponseWriter).Write 调用的。很快我意识到这与 Flipt 本身的代码无关,而是因为我使用了 Chi 的压缩中间件库,该库中提供的 API 响应的压缩。

// 没错,就是这一行
r.Use(middleware.Compress(gzip.DefaultCompression))

我注释掉了上面的代码行,然后重新运行了一下性能测试,并且发现大量的内存分配已经消失了!

在寻求解决方案之前,我想对这些分配以及它是如何影响性能的(尤其是花在 GC 上的时间),有一个新的认识。我想到了 Go 有一个工具 trace 可以让我们查看某段时间内 Go 程序的执行情况,包括一些重要的统计信息,例如堆使用情况、正在运行的数量、网络和系统调用以及对我们最重要的 GC 耗时。

为了有效的捕获跟踪信息,我们需要降低 Vegeta 请求速率,因为经常得到服务器返回的 socket: too many open files 错误。可能是与我本地机器上的 ulimit 设置太小有关。

重新运行 Vegeta:

echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 100 -duration 2m -body evaluate.json

相比较,现在每秒的请求数量是之前的1/10,但是时间更长,因此我们能够捕获有效的跟踪信息。

另外一个窗口:

wget 'http://localhost:8080/debug/pprof/trace?seconds=60' -O profile/trace

60秒生成一个跟踪信息文件,保存到我的本地机器上。可以使用以下方法检查跟踪信息:

go tool trace profile/trace

在浏览器中打开,可以更直观的查看。(关于 go tool trace 我们这儿也不详细介绍,有需要后续会写相应文章来介绍)

如上图所示,堆好像增长的很快并且伴随着 GC 频繁的出现迅速下降。还可以在 GC 通道中看到明显的蓝色条,表示在 GC 中花费的时间。

这就是我们要搜寻问题解决方案的有力证据。

3

修复

为了查找 flate.NewWriter 导致内存分配问题的原因,我需要查一下 Chi 的源码,首先看下在用版本。

➜ go list -m all | grep chi

github.com/go-chi/chi v3.3.4+incompatible

在源码中最终定位到了该方法:

func encoderDeflate(w http.ResponseWriter, level int) io.Writer {
    dw, err := flate.NewWriter(w, level)
    if err != nil {
        return nil
    }
    return dw
}

通过进一步的检查,可以发现 flate.NewWriter 在每次通过中间件输出时都会被调用。这与之前以 1000 rps 请求 API 时看到的大量内存分配相对应。

因为不想失去 API 输出压缩,所以试着先升级下版本,看能不能解决问题,但是新版本中仍然存在。所以就去扒了下 issues/PR,发现作者有提及到重新中间件压缩库。作者提到:对于具有Reset(io.Writer)方法的编码器,使用 sync.Pool 可减少内存开销。

发文前已经将 PR 合并到了 master,简单升级后就解决了这个问题。

4

结果

最后再进行一次运行负载测试和跟踪分析,可以验证确实修复了这个问题。

查看新的跟踪信息,可以看到堆以更稳定的速度增长,GC 的总量和 GC 的花费减少了:

总结

  1. 不要假设(即便是很流行的)开源库已经优化到极致了。
  2. 一个很小的问题可能会引起巨大的连锁反应,尤其是在高负载下。
  3. 合适的情况下使用 sync.Pool。
  4. 负载测试和性能分析是很好的工具。

以上就是本次分享的内容~

如果有什么改进建议,也可以在我们评论区留言,供大家参考学习。

  • https://github.com/tsenart/vegeta/
  • https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/
  • https://github.com/go-chi/chi/
  • https://making.pusher.com/go-tool-trace/
Image placeholder
noreply
未设置
  37人点赞

没有讨论,发表一下自己的看法吧

推荐文章
多进程之间的线程利用XSI IPC共享内存分配互斥量进行同步

···#include#include#include#include#include#include#include#include#include#definehandle_error_en(en

自动识别Android不合理的内存分配

写在前面Android开发中我们常常会遇到不合理的内存分配导致的问题,或是频繁GC,或是OOM。按照常规的套路我们需要打开AndroidStudio录制内存分配或者dump内存,然后人工分析,逐个排查

Go语言高级编程_2.7 CGO内存模型

2.7CGO内存模型 CGO是架接Go语言和C语言的桥梁,它使二者在二进制接口层面实现了互通,但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的

谁创建谁销毁,谁分配谁释放——JNI调用时的内存管理

在QQ音乐AndroidTV端的Cocos版本的开发过程中,我们希望尽量多的复用现有的业务逻辑,避免重复制造轮子。因此,我们使用了大量的JNI调用,来实现Java层和Native层(主要是C++)的代

PHP Opcache 注意事项以及调优

从PHP5.5开始,Opcache扩展是核心的一部分,增加了对PHP脚本的字节码缓存的支持。对于动态语言(例如PHP),字节码缓存可以显著的提高性能,因为它可以确保脚本仅被编译一次。 Opcache扩

开发中常见的Oracle三大故障与调优方法

墨墨导读:怀晓明先生(网名lastwinner),是具有多年数据库开发与项目管理经验的数据库专家。曾获得第一届ITPUB较佳建议奖,在多个大型IT企业多年的工作历练中,积累了丰富的系统架构设计经验。合

SpringBoot 深度调优,让你的项目飞起来!

项目调优作为一名工程师,项目调优这事,是必须得熟练掌握的事情。在SpringBoot项目中,调优主要通过配置文件和配置JVM的参数的方式进行。一、修改配置文件关于修改配置文件application.p

慢查询分析调优工具~mysqldumpslow

在日常的业务开发中,MySQL出现慢查询是很常见的,要么说明你家产品的增长性很好,要么就是你的SQL写的太烂了。所以对慢查询SQL进行分析和优化很重要,其中mysqldumpslow是MySQL服务自

超大规模商用 K8s 场景下,阿里巴巴如何动态解决容器资源的按需分配问题?

导读:资源利用率一直是很多平台管理和研发人员关心的话题。本文作者通过阿里巴巴容器平台团队在这一领域的工作实践,整理出了一套资源利用提升的方案,希望能够带给大家带来一些讨论和思考。引言不知道大家有没有过

Go语言高级编程_1.5 面向并发的内存模型

1.5面向并发的内存模型 在早期,CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行,在相同的时刻有且仅有

记一次 vue 的异步更新队列导致内存泄漏

起因 由于项目是需要连续传输图片形成一个伪视频(没办法,客户钱给的不够)来观看。后端采用传输base64的图片到前端展示。 环境 php:7.2 workerman:3.X vue:2.X 过程 wo

基于内存和文件存储的 queue worker, 不用 Redis 适合单进程使用没有外部依赖

因为最近要做一个简单的并发任务系统,在github上面找了一圈并没有简单可依赖的库,所以自己写了一个。欢迎大家Review贡献代码。项目地址https://github.com/iflamed/mfw

共享内存在不同系统的应用与优劣详解

共享内存是一种使计算机程序能够同时共享内存资源以实现更高性能和更少冗余数据副本的技术。共享系统内存可以在单处理器系统、并行多处理器或集群微处理器上运行。对于分布式系统会有一些差异,但共享内存也可以其上

Java内存映射,上G大文件轻松处理

内存映射文件(Memory-mappedFile),指的是将一段虚拟内存逐字节映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作),

面试题:请解释一下什么是虚拟内存?

内存对于用户来说就是一个字节数组,我们可以根据地址来访问到某个字节或者某些字节:很久之前的内存很久很久之前,一台机器上只放置一个程序,操作系统仅仅作为一个函数库存在。对于内存来说,除去操作系统的代码和

Kafka 如何优化内存缓冲机制造成的频繁 GC 问题?

目录1、Kafka的客户端缓冲机制2、内存缓冲造成的频繁GC问题3、Kafka设计者实现的缓冲池机制4、总结一下“ 这篇文章,给大家聊一个硬核的技术知识,我们通过Kafka内核源码中的一些设计思想,来

打破边界 不是所有“内存与存储”都叫傲腾

人类正在向一个万物感知、万物互联、万物智能的世界进化。一方面海量的数据对数据基础设施带来了新的挑战;另一方面伴随着数据中心业务和应用的多样化以及智能化,企业对数据存储的需求越来越高。智能世界的特点是能

深入理解JVM - 内存溢出实战

Java堆溢出Java堆用于存储对象实例,只要不断地创建对象,当对象数量到达最大堆的容量限制后就会产生内存溢出异常。最常见的内存溢出就是存在大的容器,而没法回收,比如:Map,List等。出现下面信息

Java内存模型与Hppens-Before规则

为什么要有Java内存模型?并发编程的3个源头问题分别是: 可见性,由缓存导致的可见性问题 有序性,由编译优化导致的有序性问题 原子性,由线程切换导致的原子性问题 Java内存模型就是为了解决可见性和

JVM内存布局

   JVM中将内存分为若干部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器             程序计数器:该区域是内存中较小的一块区域---是当前线程在执行的字节码的行号指示器。程序计数器是

JavaScript常见的内存泄漏

课程推荐:Java开发工程师--学习猿地精品课程 前言1介绍2内存泄露的主要原因3常见的内存泄露3.1全局变量3.2计时器3.3多处引用3.4闭包4Chrome内存分析工具资料前言在阅读这篇博客之前,

Go 解决国内下载 go get golang.org/x 包失败

GOPROXY环境变量 我们知道从Go1.11版本开始,官方支持了gomodule包依赖管理工具。其实还新增了GOPROXY环境变量。如果设置了该变量,下载源代码时将会通过这个环境变量设置的代理地址,

Google 21 岁生日,一文回顾 Google 发展史

9月27日,谷歌在Google搜索引擎首页庆祝自己21岁生日。 在涂鸦存档上,谷歌发布了一封庆祝信: 21年前,两位斯坦福大学博士生谢尔盖·布林和拉里·佩奇发表了一篇有关启动“大型搜索引擎”原

GoWeb教程_05.5. 使用 Beego orm 库进行 ORM 开发

beegoorm是我开发的一个Go进行ORM操作的库,它采用了Gostyle方式对数据库进行操作,实现了struct到数据表记录的映射。beegoorm是一个十分轻量级的GoORM框架,开发这个库的本

GoWeb教程_06.2. Go 如何使用 session

通过上一小节的介绍,我们知道session是在服务器端实现的一种用户和服务器之间认证的解决方案,目前Go标准包没有为session提供任何支持,这小节我们将会自己动手来实现go版本的session管理