解决golang 的内存碎片问题

解决,golang,内存,碎片,问题 · 浏览次数 : 388

小编点评

**Go内存碎片问题解决方案** **1. 通过排序缓存数据结构** 在将chunk写入文件之前,按照chunk的时间戳进行排序,使其按照时间顺序进行申请字节。 **2. 使用大小类控制内存分配** 设置每个mspan的size class以确保内存申请模式一致,降低碎片率。 **3. 优化快照文件写入** 将容量申请设置为128字节,让内存申请模式保持一致,并使用时间局部性优化写入过程。 **4. 使用`go_memstats`指标监控内存碎片** 通过`go_memstats_heap_inuse_bytes`和`go_memstats_heap_alloc_bytes`指标,定期监控正在使用的堆内存数量,及时发现碎片并进行处理。 **5. 通过`go_memstats`指标分析碎片原因** 通过`go_memstats_heap_alloc_bytes`和`go_memstats_heap_inuse_bytes`指标分析碎片原因,例如碎片是由哪些对象引起的,以及哪些size class最经常出现碎片。 **6. 调整`mspan`大小** 根据实际需求调整`mspan`的大小,以减少碎片率。 **7. 使用高效的内存分配库** 选择支持高效内存分配的库,例如`github.com/go-memory/heap`。 **8. 监控应用程序性能** 密切关注应用程序的性能,及时发现内存碎片问题并采取必要的措施。

正文

解决golang 的内存碎片问题

本文译自Why I encountered Go memory fragmentation? How did I resolve it?,作者通过分析golang的堆管理方式,解决了内存碎片的问题。

背景

我们的团队正在搭建运行一个兼容Prometheus的内存时序数据库,该数据库有一个数据结构,称为"chunk"。每个chunk对应一个唯一键值标签对的4个小时的数据点,如:

{host="host1", env="production"}

可以将一个数据点认为是一个时间戳加数值的组合,一个chunk包含了4个小时的数据点。数据库同一时间只会保存每个(唯一标签对的)指标的8个chunk,且每4小时会对老的chunk进行清除。由于它是一个内存数据库,因此使用快照恢复逻辑来防止数据丢失。

遇到的问题

通过观察内存使用发现,在数据库启动32~36小时之后,内存使用一直在增加:

image

第1种调试方式 -- Go pprof

一开始怀疑是内存泄露问题,因此通过每小时采集heap profile来对比内存使用差异,但此时并没有发现任何异常。一开始怀疑可能是chunks没有完全释放,如果长期持有未使用的对象,可能会导致该问题,但通过pprof并没有找到相关线索。

为什么使用的内存在增加,但总的堆使用却保持不变?

第2种调试方式 -- Go memstats指标

通过如下go memstats指标发现可能出现了内存碎片:

go_memstats_heap_inuse_bytes{…} - go_memstats_heap_alloc_bytes{…}
image

指标结果显示,堆申请的字节数要少于使用的字节数。这意味着有很多申请的空间没有被有效地利用。通常在chunks过期前的4小时内,该值会增加,但之后会逐步降低。然而在出问题的节点上,该值并没有降低。

我怀疑它可以为非重启节点使用过期的空间来处理新摄取的数据,但是由于内存碎片而不能为重启过的节点使用过期的空间(即使用恢复逻辑读取快照)。

之后我将怀疑点转向了快照的恢复逻辑。快照实际上由chunks的字节构成,并放在文件中。在处理过程中会并行写chunk,因此chunk的顺序是随机的,这样可以提高写性能,而读操作则是从文件头按顺序读取的。因此可以想象,每4个小时,当某些零散chunk过期时,就会导致大量内存碎片。

image

下面是尝试的解决方式,即在将chunk写入文件之前会按照chunk的时间戳进行排序,这样就可以按照时间顺序来申请字节(恢复期间会从头部读取字节并分配内存),下面是修复后的申请方式:

image

经验证发现,问题并没有解决,且写操作性能严重降级。

第3种调试方式--理解Go 堆管理方式

至此需要理解Go是如何进行堆管理的。参考golang-memory-allocation

image

简单地说,Go运行时管理着大量mspans,每个mspans包含特定数目的连续8KB内存页,不同msapns有着不同的size class(大小),size class决定了mspan中的对象的大小,用于适应不同大小的对象,降低内存浪费。

假设要申请100字节的对象,则需要选择112字节的size class(参见列表)。

通常每个chunk都有一个用于内部数据的字节数组,其创建方式为:

make([]byte, 0, 128)

Go中slice的大小并不是固定不变的,当slice的容量小于1024时会以2的倍数增加,当容量大于1024时,新slice的容量会变为原来的1.25倍。(本文对这部分描述有误,此处纠正),在本场景中,大部分size-classes是固定的:

image

而目前恢复使用的chunk的为:

make([]byte, 0, actual chunk byte size)

这意味着摄取时采用的chunk size classes与恢复是采用的chunk size classes完全不同!恢复时使用未对齐mspan的实际chunk大小来保存数据,导致过期内存重复利用率不高,也导致mspan中出现了大量内存碎片:

image-20230306095308333

最后作者,通过如下方式解决了该问题:

  1. 将容量申请设置为128字节,让内存申请模式保持一致(即让系统自动对其mspan),这样就可以尽可能地复用内存
  2. 按照时间顺序来写入快照文件,防止因为数据乱序导致出现chunk层面的内存碎片

通过如上两种方式解决了该问题:

image

这里解释一下文中涉及的mstat的2个指标,更多参见Exploring Prometheus Go client metrics

  • go_memstats_heap_alloc_bytes:为对象申请的堆内存,单位字节。该指标计算了所有GC没有释放的所有堆对象(可达的对象和不可达的对象)
  • go_memstats_heap_inuse_bytes: in-use span中的字节数。go_memstats_heap_inuse_bytes-go_memstats_heap_alloc_bytes表示那些已申请但没有使用的堆内存。

总结

  • Go将堆分为mspans
  • 一个mspan由特定数目的连续8KB页组成
  • 每个mspan对应特定的size class,用来决定申请创建的对象大小
  • 为么避免在Go 运行时中出现内存碎片,需要同时考虑size classes和时间局部性

与解决golang 的内存碎片问题相似的内容:

解决golang 的内存碎片问题

解决golang 的内存碎片问题 本文译自Why I encountered Go memory fragmentation? How did I resolve it?,作者通过分析golang的堆管理方式,解决了内存碎片的问题。 背景 我们的团队正在搭建运行一个兼容Prometheus的内存时序

golang如何使用指针灵活操作内存?unsafe包原理解析

本文将深入探讨Golang中unsafe包的功能和原理。同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的。所以,本文还会为大家介绍unsafe 包的典型应用以及高频面试题。

一个有趣的nginx HTTP 400响应问题分析

背景 之前在一次不规范HTTP请求引发的nginx响应400问题分析与解决 中写过客户端query参数未urlencode导致的400问题,当时的结论是: 对于query参数带空格的请求,由于其不符合HTTP规范,golang的net/http库无法识别会直接报错400,而nginx和使用uwsgi

Golang漏洞管理

原文在[这里](https://go.dev/security/vuln/) ## 概述 Go帮助开发人员检测、评估和解决可能被攻击者利用的错误或弱点。在幕后,Go团队运行一个管道来整理关于漏洞的报告,这些报告存储在Go漏洞数据库中。各种库和工具可以读取和分析这些报告,以了解特定用户项目可能受到的影

从源码中解析fabric区块数据结构(一)

从源码中解析fabric区块数据结构(一) 前言 最近打算基于fabric-sdk-go实现hyperledger fabric浏览器,其中最重要的一步就是解析fabric的上链区块。虽说fabric是Golang实现的,但直到2021年2月1号才发布了第一个稳定版fabric-sdk-go,而且官

[转帖]Influxdb 2.x 快速入门

Influxdb 2.x 快速入门 https://www.jianshu.com/p/268fca65f10e Influxdb是由Golang 构建的时序数据库,由于由Go语言构建使得其跨平台部署相对方便。用户只需下载其可执行文件到相应系统执行即可。 核心概念 示例数据(解释某些概念用) _ti

golang import 导入的四种方式

1 标准导入: import "package_name" 2 导入别名: import ( alias "package_name" ) 3 匿名导入: import ( _ "package_name" ) 4 点导入: import ( . "package_name" ) 下面做详细解释:

Golang 切片作为函数参数传递的陷阱与解答

作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。 GitHub : https://github.com/af913337456/ 出版的书籍: 《1.0-区块链DApp开发实战》 《2.0-区块链DApp开发:基于公链》 例子 切片作为函数参数传递的是值 用来误导切片作为函数参数传递的

20个Golang片段让我不再健忘

本文使用代码片段的形式来解释在 go 语言开发中经常遇到的小功能点,由于本人主要使用 java 开发,因此会与其作比较,希望对大家有所帮助。

动手造轮子自己实现人工智能神经网络(ANN),解决鸢尾花分类问题Golang1.18实现

人工智能神经网络( Artificial Neural Network,又称为ANN)是一种由人工神经元组成的网络结构,神经网络结构是所有机器学习的基本结构,换句话说,无论是深度学习还是强化学习都是基于神经网络结构进行构建。关于人工神经元,请参见:人工智能机器学习底层原理剖析,人造神经元,您一定能看