『每周译Go』Golang 在大规模流处理场景下的最小化内存使用
Posted GoCN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了『每周译Go』Golang 在大规模流处理场景下的最小化内存使用相关的知识,希望对你有一定的参考价值。
作为公司平台团队的一员,我接触了很多文件处理的场景,比如管理一个通用文件上传中心服务,处理邮件附件,处理和导出大文件。在过去,这项工作要容易得多,因为我们可以完全支配整个服务器。我们可以写入一个文件让它持久化在服务器磁盘上,尽管这个作业所需的资源是非常多的。而现在,你的代码库是在更小的处理单元上发布的,比如 pods 。它的资源是虚拟分配的,并且在许多情况下是有限的,所以你需要知道如何有效地使用它们。实现优雅的处理和解决 OOM 退出问题也许对于那些已经熟悉自由地使用内存的人来说是一个大麻烦。
在我看来,Reader
和 Writer
是 Golang 最重要的部分。它给 goroutine 和并发处理提供了重要支持,是 Go 编程模型精简且具有良好性能的关键。因此,为了更进一步掌握 Go 编程语言,你应该能够优雅地操作 go buffers 和 goroutines。在本文中,我将讨论在文件上传到云存储引擎之前,处理从卫星客户端的文件流到中央文件上传器时遇到的问题。
操作,你应该得到过下面这些内容:是一种非常方便的操作,因为在将数据写入另一个文本进程之前,我们不需要读取数据。然而要小心的是这可能会导致你落入一个不想踏入的陷阱。官方文件中写道:从 src 复制副本到 dst,直到在 src 上到达 EOF 或发生错误。它返回复制的字节数和复制时遇到的第一个错误(如果有的话)。— Go 官方文档
在文件离线处理时,你可以打开一个带缓冲的 writer
然后完全复制 reader
中内容,并且不用担心任何其他影响。然而,Copy
操作将持续地将数据复制到 Writer
,直到 Reader
读完数据。但这是一个无法控制的过程,如果你处理 writer
中数据的速度不能与复制操作一样快,那么它将很快耗尽你的缓冲区资源。此外,选择丢弃或者撤销缓冲区分配也是一件很难考虑的事情。
就出现来解决这类问题。提供一对 writer
和 reader
,并且读写操作都是同步的。利用内部缓冲机制,直到之前写入的数据被完全消耗掉才能写到一个新的 writer
数据快。这样你就可以完全控制如何读取和写入数据。现在,数据吞吐量取决于处理器读取文本的方式,以及 writer
更新数据的速度。我用它来做我的微服务文件转发器,实际工作效果非常好。能够以最小的内存使用量来复制和传输数据。有着 Pipe
提供的写阻塞功能 ,Pipe
和 Copy
就形成了一个完美的组合。
在本地加载,也可以通过 multipart reader
从其他请求中加载。预取和补偿文件流
在我们的中央文件上传服务中,我们使用云引擎进行存储,它的 API 接受一个提供文件原始数据的 reader
。除此之外,我们还需要识别上传的内容类型,以确定是否将其删除,还是将其分类到可用的 bucket 中。但是,读取操作是不可逆的,我们必须找到一种方法,为类型检测器读取最小长度的嗅探字节,同时也需要为后一个过程保留原始数据流。
一个可行的解决方案是使用 io.TeeReader
,它会将从 reader 读取的数据写入另一个 writer
中。TeeReader
最常见的用例是将一个流克隆成一个新的流,在保持流不被破坏的情况下为 reader
提供服务。
来操作它,达到无本地缓存效果。但另一个问题是,TeeReader
要求在完成读取过程之前必须完成写入过程,而 Pipe
则相反。所以最后我们设计了一个定制化的预取 reader
,专门用来处理这种情况。
用于后面的操作。结论
这就是我在工作中遇到的问题。操作这些 readers
和 writers
是非常麻烦的,但还是非常值得一试,因为这其中包含了很多乐趣。我希望你能学习到一种处理与它们相关的各种问题的方法,并能有一个更好的 Go 使用体验。
从 src 复制副本到 dst,直到在 src 上到达 EOF 或发生错误。它返回复制的字节数和复制时遇到的第一个错误(如果有的话)。— Go 官方文档
在文件离线处理时,你可以打开一个带缓冲的 writer
然后完全复制 reader
中内容,并且不用担心任何其他影响。然而,Copy
操作将持续地将数据复制到 Writer
,直到 Reader
读完数据。但这是一个无法控制的过程,如果你处理 writer
中数据的速度不能与复制操作一样快,那么它将很快耗尽你的缓冲区资源。此外,选择丢弃或者撤销缓冲区分配也是一件很难考虑的事情。
就出现来解决这类问题。提供一对 writer
和 reader
,并且读写操作都是同步的。利用内部缓冲机制,直到之前写入的数据被完全消耗掉才能写到一个新的 writer
数据快。这样你就可以完全控制如何读取和写入数据。现在,数据吞吐量取决于处理器读取文本的方式,以及 writer
更新数据的速度。我用它来做我的微服务文件转发器,实际工作效果非常好。能够以最小的内存使用量来复制和传输数据。有着 Pipe
提供的写阻塞功能 ,Pipe
和 Copy
就形成了一个完美的组合。
在本地加载,也可以通过 multipart reader
从其他请求中加载。预取和补偿文件流
在我们的中央文件上传服务中,我们使用云引擎进行存储,它的 API 接受一个提供文件原始数据的 reader
。除此之外,我们还需要识别上传的内容类型,以确定是否将其删除,还是将其分类到可用的 bucket 中。但是,读取操作是不可逆的,我们必须找到一种方法,为类型检测器读取最小长度的嗅探字节,同时也需要为后一个过程保留原始数据流。
一个可行的解决方案是使用 io.TeeReader
,它会将从 reader 读取的数据写入另一个 writer
中。TeeReader
最常见的用例是将一个流克隆成一个新的流,在保持流不被破坏的情况下为 reader
提供服务。
来操作它,达到无本地缓存效果。但另一个问题是,TeeReader
要求在完成读取过程之前必须完成写入过程,而 Pipe
则相反。所以最后我们设计了一个定制化的预取 reader
,专门用来处理这种情况。
用于后面的操作。结论
这就是我在工作中遇到的问题。操作这些 readers
和 writers
是非常麻烦的,但还是非常值得一试,因为这其中包含了很多乐趣。我希望你能学习到一种处理与它们相关的各种问题的方法,并能有一个更好的 Go 使用体验。
原文信息
原文地址:https://engineering.be.com.vn/large-stream-processing-in-golang-with-minimal-memory-usage-c1f90c9bf4ce
原文作者:Tài Chí
本文永久链接:https://github.com/gocn/translator/blob/master/2022/w04_Large_stream_processing_in_Golang_with_minimal_memory_usage.md
译者:haoheipi
校对:watermelo
想要了解关于 Go 的更多资讯,还可以通过扫描的方式,进群一起探讨哦~
『每周译Go』golang 垃圾回收器如何标记内存?
本文基于 Go 1.13。这里讨论的关于内存管理的概念在我的文章Go:内存管理和分配 中有解释
Go 垃圾回收器负责回收不再使用的内存。实现的算法是一个并行的三色标记扫描采集器。在本文中,我们将详细了解标记阶段,以及不同颜色的用法。
您可以在 kenfox 的可视化垃圾回收算法 中找到关于不同类型垃圾回收器的更多信息。
标记阶段
此阶段执行内存扫描,以了解代码仍在使用哪些块,以及应该回收哪些块。
但是,由于垃圾回收器可以与我们的 Go 程序同时运行,因此它需要一种在扫描时检测内存中潜在变化的方法。为了解决这个潜在的问题,实现了一个写屏障算法,允许 Go 跟踪任何指针的变化。启用写屏障的唯一条件是短时间停止程序,也称为 “STW”:
在进程开始时,Go 还会为每个处理器启动一个标记辅助进程,以帮助标记内存。
然后,一旦根节点被排队等待处理,标记阶段就可以开始遍历内存并为其着色。
现在让我们以一个简单的程序为例,该程序允许我们遵循标记阶段所做的步骤
Type struct1 struct {
a, b int64
c, d float64
e *struct2
}
type struct2 struct {
f, g int64
h, i float64
}
func main() {
s1 := allocStruct1()
s2 := allocStruct2()
func () {
_ = allocStruct2()
}()
runtime.GC()
fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2)
}
//go:noinline
func allocStruct1() *struct1 {
return &struct1{
e: allocStruct2(),
}
}
//go:noinline
func allocStruct2() *struct2 {
return &struct2{}
}
由于 struct subStruct 不包含任何指针,因此它存储在一个专用于对象的范围中,而不引用其他对象:
这使得垃圾回收器的工作更容易,因为它在标记内存时不必扫描这个范围。
一旦分配完成,我们的程序就会强制垃圾回收器运行一个周期。以下是工作流程:
垃圾回收器从堆栈开始标记,然后跟着指针递归遍历内存。直到对象都被标记时停止扫描。然而,这个过程不是在同一个 goroutine 中完成的 完成的;每个指针都在工作池中排队。然后 ,后台的标记线程发现之前的出列队列是来自该工作池,扫描对象,然后将在其中找到的指针加入队列:
着色!
后台线程现在需要一种方法来跟踪哪些内存有没有被扫描。垃圾回收器使用三色算法,其工作原理如下:
所有对象一开始都被认为是白色的
根对象(堆栈、堆、全局变量)将以灰色显示
完成此主要步骤后,垃圾回收器将:
选择一个灰色的对象,把它涂成黑色
遵循此对象的所有指针并将所有引用的对象涂成灰色
然后,它将重复这两个步骤,直到没有更多的对象要着色。从这一点来看,对象不是黑色就是白色。白色集合表示未被任何其他对象引用且准备好回收的对象。
下面是使用上一个示例对其进行的表示:
作为第一种状态,所有对象都被视为白色。然后,对象被遍历,可到达的对象将变为灰色。如果对象位于标记为 “无扫描” 的范围内,则可以将其绘制为黑色,因为不需要对其进行扫描:
灰色对象现在入队等待扫描并变黑:
在没有更多的对象要处理之前,入队的对象也会发生同样的情况:
在进程结束时,黑色对象是内存中正在使用的对象,而白色对象是要回收的对象。如我们所见,由于 struct2 的实例是在匿名函数中创建的,并且无法从堆栈访问,因此它保持为白色,可以清除。
由于每个跨度中有一个名为 gcmarkBits 的位图属性,颜色在内部实现,该属性跟踪扫描,并将相应的位设置为 1:
正如我们所见,黑色和灰色的工作原理是一样的。这一过程的不同之处在于,当黑色对象结束扫描链时,灰色对象排队等待扫描。
垃圾回收器最终会 stops the world,将每个写屏障上所做的更改刷新到工作池,并执行剩余的标记。
您可以在我的文章Go:垃圾回收器如何监视您的应用程序 中找到有关并发进程和垃圾回收器中标记阶段的更多详细信息
运行时分析器
Go 提供的工具允许我们可视化所有这些步骤,并在程序中查看垃圾回收器的影响。在启用跟踪的情况下运行我们的代码提供了前面步骤的一个大图。以下是 traces:
标记线程的生命周期也可以在 goroutine 级别的 tracer 中可视化。下面是 goroutine#33 的示例,它在开始标记内存之前先在后台等待。
以上是关于『每周译Go』Golang 在大规模流处理场景下的最小化内存使用的主要内容,如果未能解决你的问题,请参考以下文章