Go语言实时GC - 三色标记算法

前言

Go语言能够支持实时的,高并发的消息系统,在高达百万级别的消息系统中能够将延迟降低到100ms以下,很大一部分需要归功于Go高效的垃圾回收系统。

对于实时系统而言,垃圾回收系统可能是一个极大的隐患,因为在垃圾回收的时候需要将整个应用程序暂停。所以在我们设计消息总线系统的时候,需要小心地选择我们的语言。Go一直在强调它的低延迟,但是它真的做到了吗?如果是的,它是怎么做到的呢?

在这篇文章当中,我们将会看到Go语言的GC是如何实现的(tricolor algorithm,三色算法),以及为什么这种方法能够达到如此之低的GC暂停,以及最重要的是,它是否真的有效(对这些GC暂停进行benchmar测试,以及同其它类型的语言进行比较)。

正文

1. 从Haskell到Go

我们用pub/sub消息总线系统为例说明问题,这些系统在发布消息的时候都是in-memory存储的。在早期,我们用Haskell实现了第一版的消息系统,但是后面发现GHC的gabage collector存在一些基础延迟的问题,我们放弃了这个系统转而用Go进行了实现。

这是有关 Haskell消息系统的一些实现细节,在GHC中最重要的一点是它GC暂停时间同当前的工作集的大小成比例关系(也就是说,GC时间和内存中存储对象的数目有关)。在我们的例子中,内存中存储对象的数目往往都非常巨大,这就导致gc时间常常高达数百毫秒。这就会导致在GC的时候整个系统是阻塞的。

而在Go语言中,不同于GHC的全局暂停(stop-the-world)收集器,Go的垃圾收集器是和主程序并行的。这就可以避免程序的长时间暂停。我们则更加关注于Go所承诺的低延迟以及其在每个新版本中所提及的 延迟提升 是否真的向他们所说的那样。

2. 并行垃圾回收是如何工作的?

Go的GC是如何实现并行的呢?其中的关键在于三色标记清除算法 (tricolor mark-and-sweep algorithm)。该算法能够让系统的gc暂停时间成为能够预测的问题。调度器能够在很短的时间内实现GC调度,并且对源程序的影响极小。下面我们看看三色标记清除算法是如何工作的:

假设我们有这样的一段链表操作的代码:

var A LinkedListNode;
var B LinkedListNode;
// ...
B.next = &LinkedListNode{next: nil};
// ...
A.next = &LinkedListNode{next: nil};
*(B.next).next = &LinkedListNode{next: nil};
B.next = *(B.next).next;
B.next = nil;
复制代码

2.1. 第一步

var A LinkedListNode;
var B LinkedListNode;// ...B.next = &LinkedListNode{next: nil};
复制代码

刚开始我们假设有三个节点A、B和C,作为根节点,红色的节点A和B始终都能够被访问到,然后进行一次赋值 B.next = &C。初始的时候垃圾收集器有三个集合,分别为黑色,灰色和白色。现在,因为垃圾收集器还没有运行起来,所以三个节点都在白色集合中。

2.2. 第二步

我们新建一个节点D,并将其赋值给A.next。即:

var D LinkedListNode;
A.next = &D;
复制代码

需要注意的是,作为一个新的内存对象,需要将其放置在灰色区域中。为什么要将其放在灰色区域中呢?这里有一个规则,如果一个指针域发生了变化,则被指向的对象需要变色。因为所有的新建内存对象都需要将其地址赋值给一个引用,所以他们将会立即变为灰色。(这就需要问了,为什么C不是灰色?)

2.3. 第三步

在开始GC的时候,根节点将会被移入灰色区域。此时A、B、D三个节点都在灰色区域中。由于所有的程序子过程(process,因为不能说是进程,应该算是线程,但是在go中又不完全是线程)要么事程序正常逻辑,要么是GC的过程,而且GC和程序逻辑是并行的,所以程序逻辑和GC过程应该是交替占用CPU资源的。

2.4. 第四步 扫描内存对象

在扫描内存对象的时候,GC收集器将会把该内存对象标记为黑色,然后将其子内存对象标记为灰色。在任一阶段,我们都能够计算当前GC收集器需要进行的移动步数:2*|white| + |grey|,在每一次扫描GC收集器都至少进行一次移动,直到达到当前灰色区域内存对象数目为0。

2.5. 第五步

程序此时的逻辑为,新赋值一个内存对象E给C.next,代码如下:

var E LinkedListNode;
C.next = &E;
复制代码

按照我们之前的规则,新建的内存对象需要放置在灰色区域,如图所示:

这样做,收集器需要做更多的事情,但是这样做当在新建很多内存对象的时候,可以将最终的清除操作延迟。值得一提的是,这样处理白色区域的体积将会减小,直到收集器真正清理堆空间时再重新填入移入新的内存对象。

2.6. 第六步 指针重新赋值

程序逻辑此时将 B.next.next赋值给了B.next,也就是将E赋值给了B.next。代码如下:

*(B.next).next = &LinkedListNode{next: nil};
// 指针重新赋值:
B.next = *(B.next).next;
复制代码

这样做之后,如图所示,C将不可达。

这就意味着,收集器需要将C从白色区域移除,然后在GC循环中将其占用的内存空间回收。

2.7. 第七步

将灰色区域中没有引用依赖的内存对象移动到黑色区域中,此时D在灰色区域中没有其它依赖,并依赖于它的内存对象A已经在黑色区域了,将其移动到黑色区域中。

2.8. 第八步

在程序逻辑中,将B.next赋值为了nil,此时E将变为不可达。但此时E在灰色区域,将不会被回收,那么这样会导致内存泄漏吗?其实不会,E将在下一个GC循环中被回收,三色算法能够保证这点:如果一个内存对象在一次GC循环开始的时候无法被访问,则将会被冻结,并在GC的最后将其回收。

2.9. 第九步

在进行第二次GC循环的时候,将E移入到黑色区域,但是C并不会移动,因为是C引用了E,而不是E引用C。

2.10. 第十步

收集器再扫描最后一个灰色区域中的内存对象B,并将其移动到黑色区域中。

2.11. 第十一步 回收白色区域

收集器再扫描最后一个灰色区域中的内存对象B,并将其移动到黑色区域中。

2.12. 第十二步 区域变色

这一步是最有趣的,在进行下次GC循环的时候,完全不需要将所有的内存对象移动回白色区域,只需要将黑色区域和白色区域的颜色换一下就好了,简单而且高效。

3. GC三色算法小结

上面就是三色标记清除算法的一些细节,在当前算法下仍旧有两个阶段需要 stop-the-world:一是进行root内存对象的栈扫描;二是标记阶段的终止暂停。令人激动的是,标记阶段的终止暂停 将被去除。在实践中我们发现,用这种算法实现的GC暂停时间能够在超大堆空间回收的情况下达到<1ms的表现。

4. 延迟 VS 吞吐

如果一个并行GC收集器在处理超大内存堆时能够达到极低的延迟,那么为什么还有人在用stop-the-world的GC收集器呢?难道Go的GC收集器还不够优秀吗?

这不是绝对的,因为低延迟是有开销的。最主要的开销就是,低延迟削减了吞吐量。并发需要额外的同步和赋值操作,而这些操作将会占用程序的处理逻辑的时间。而Haskell的GHC则针对吞吐量进行了优化,Go则专注于延迟,我们在考虑采用哪种语言的时候需要针对我们自己的需求进行选择,对于推送系统这种实时性要求比较高的系统,选择Go语言则是权衡之下得到的选择。

5. 实际表现

目前而言,Go好像已经能够满足低延迟系统的要求了,但是在实际中的表现又怎么样呢?利用相同的benchmark测试逻辑实现进行比较:该基准测试将不断地向一个限定缓冲区大小的buffer中推送消息,旧的消息将会不断地过期并成为垃圾需要进行回收,这要求内存堆需要一直保持较大的状态,这很重要,因为在回收的阶段整个内存堆都需要进行扫描以确定是否有内存引用。这也是为什么GC的运行时间和存活的内存对象和指针数目成正比例关系的原因。

这是Go语言版本的基准测试代码,这里的buffer用数组实现:

package mainimport ("fmt""time"
)const (windowSize = 200000msgCount   = 1000000
)type (message []bytebuffer  [windowSize]message
)var worst time.Durationfunc mkMessage(n int) message {m := make(message, 1024)for i := range m {m[i] = byte(n)}return m
}func pushMsg(b *buffer, highID int) {start := time.Now()m := mkMessage(highID)(*b)[highID%windowSize] = melapsed := time.Since(start)if elapsed > worst {worst = elapsed}
}func main() {var b bufferfor i := 0; i < msgCount; i++ {pushMsg(&b, i)}fmt.Println("Worst push time: ", worst)
}
复制代码

相同的逻辑,不同语言实现下进行的测试结果如下:

令人惊讶的是Java,表现得非常一般,而OCaml则非常之好,OCaml语言能够达到约3ms的GC暂停时间,这是因为OCaml采用的GC算法是 incremental GC algorithm (而在实时系统中不采用OCaml的原因是该语言对多核的支持不好)。

正如表中显示的,Go的GC暂停时间大约在7ms左右,表现也好,已经完全能够满足我们的要求。

总结

这次调查的重点在于GC要么关注于低延迟,要么关注于高吞吐。当然这些也都取决于我们的程序是如何使用堆空间的(我们是否有很多内存对象?每个对象的生命周期是长还是短?)

理解底层的GC算法对该系统是否适用于你的测试用例是非常重要的。当然GC系统的实际实现也至关重要。你的基准测试程序的内存占用应该同你将要实现的真正程序类似,这样才能够在实践中检验GC系统对于你的程序而言是否高效。正如前文所说的,Go的GC系统并不完美,但是对于我们的系统而言是可以接受的。

尽管存在一些问题,但是Go的GC表现已经优于大部分同样拥有GC系统的语言了,Go的开发团队针对GC延迟进行了优化,并且还在继续。Go的GC确实是有可圈可点之处,无论是理论上还是实践中。

参考

  • Golang’s Real-time GC in Theory and Practice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280361.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每小时50哈希——看看一个内部员工是如何摧毁整个公司网络的?

本文讲的是每小时50哈希——看看一个内部员工是如何摧毁整个公司网络的&#xff1f;&#xff0c;我们以前曾调查过黑客会通过连接在USB端口的正在充电的手机实施攻击&#xff0c;在这项研究中&#xff0c;我们重新审视了USB端口的安全性。我们发现&#xff0c;手机充电时&#…

推荐一款 在线+离线数据 同步框架 Dotmim.Sync

移动智能应用可以分为在线模式、纯离线模式与“在线离线”混合模式。在线模式下系统数据一般存储在服务器端的大中型数据库&#xff08;如 SQL Server、Oracle、MySQL 等&#xff09;&#xff0c;移动应用依赖于稳定可靠的网络连接&#xff1b;纯离线模式下系统数据一般存储在移…

如何在Windows 10中将您喜欢的设置固定到开始菜单

If you find you’re accessing the same settings over and over in Windows 10, you can add these settings to the Start menu as tiles for quick and easy access. We’ll show you how to do this. 如果发现要在Windows 10中反复访问相同的设置&#xff0c;则可以将这些…

20155202《网络对抗》Exp9 web安全基础实践

20155202《网络对抗》Exp9 web安全基础实践 实验前回答问题 &#xff08;1&#xff09;SQL注入攻击原理&#xff0c;如何防御 SQL注入产生的原因&#xff0c;和栈溢出、XSS等很多其他的攻击方法类似&#xff0c;就是未经检查或者未经充分检查的用户输入数据&#xff0c;意外变成…

web前端工程师热门岗位技能要求前瞻

春节假期以后&#xff0c;稍作调整&#xff0c;马上就要迎来求职高峰期。作为一名前端工程师或者有意向转行从事前端相关工作的人&#xff0c;你是否对2019年的前端市场有了新的解读&#xff0c;对于前端的企业岗位要求有了新的理解。今天我就跟大家分享一下2019年web前端热门岗…

MVC Html.AntiForgeryToken() 防止CSRF***

MVC中的Html.AntiForgeryToken()是用来防止跨站请求伪造(CSRF:Cross-site request forgery)***的一个措施,它跟XSS(XSS又叫CSS:Cross-Site-Script),***不同,XSS一般是利用站内信任的用户在网站内插入恶意的脚本代码进行***&#xff0c;而CSRF则是伪造成受信任用户对网站进行***…

如何反序列化派生类

前言上回&#xff0c;我们讲解了《如何序列化派生类》。那如何反序列化派生类呢&#xff1f;假设有一个 Person 抽象基类&#xff0c;其中包含 Student 和 Teacher 派生类&#xff1a;public class Person {public string Name { get; set; } }public class Student : Person {…

目标跟踪 facebook_如何关闭Facebook Messenger的位置跟踪(如果已启用)

目标跟踪 facebookIt seems like everyone is tracking our location now. Not surprisingly, Facebook Messenger can also transmit a significant amount of information on your location activity. If you use Messenger, here’s how to make sure it’s not reporting y…

哪位大兄弟有用 cMake 开发Android ndk的

一直用 Android studio 开发ndk&#xff0c;但是gradle支持的不是很好&#xff0c;只有experimental 版本支持 配置各种蛋疼。主要每次新建一个module都要修改配置半天。之前也看到过google 开发文档有提到 cmake 但是一直没用。哪位大兄弟用过&#xff0c;说下经验 哪位大兄弟…

restfull知识点

网络应用程序&#xff0c;分为前端和后端两个部分。当前的发展趋势&#xff0c;就是前端设备层出不穷&#xff08;手机、平板、桌面电脑、其他专用设备......&#xff09;。因此&#xff0c;必须有一种统一的机制&#xff0c;方便不同的前端设备与后端进行通信。这导致API构架的…

云计算基础知识:CPU虚拟化

虚拟化技术的分类主要有服务器虚拟化、存储虚拟化、网络虚拟化、应用虚拟化。服务器虚拟化技术按照虚拟对象来分&#xff0c;可分为&#xff1a;CPU虚拟化、内存虚拟化、I/O虚拟化;按照虚拟化程度可分为&#xff1a;全虚拟化、半虚拟化、硬件辅助虚拟化。将不同的虚拟化对象和程…

WPF-18 INotifyPropertyChanged 接口

我们先来看看微软官方给出的定语&#xff1a;通知客户端属性值已经更改。其实对于一个陌生小白来说&#xff0c;很难通过这句话来理解其中的原理&#xff0c;这个接口在WPF和Winform编程中经常会用到&#xff0c;下面是该接口的定义&#xff1a;namespace System.ComponentMode…

头脑风暴 软件_头脑风暴和思维导图的最佳网站和软件

头脑风暴 软件A mind map is a diagram that allows you to visually outline information, helping you organize, solve problems, and make decisions. Start with a single idea in the center of the diagram and add associated ideas, words, and concepts connected ra…

NULL的陷阱:Merge

NULL表示unknown&#xff0c;不确定值&#xff0c;所以任何值&#xff08;包括null值&#xff09;和NULL值比较都是不可知的&#xff0c;在on子句&#xff0c;where子句&#xff0c;Merge或case的when子句中&#xff0c;任何值和null比较的结果都是false&#xff0c;这就是NULL…

Python实现将不规范的英文名字首字母大写

Python实现将不规范的英文名字首字母大写 这篇文章给大家主要介绍的是利用map()函数&#xff0c;把用户输入的不规范的英文名字&#xff0c;变为首字母大写&#xff0c;其他小写的规范名字。文中给出了三种解决方法&#xff0c;大家可以根据需要选择使用&#xff0c;感兴趣的朋…

使用 System.Text.Json 时,如何处理 Dictionary 中 Key 为自定义类型的问题

在使用 System.Text.Json 进行 JSON 序列化和反序列化操作时&#xff0c;我们会遇到一个问题&#xff1a;如何处理字典中的 Key 为自定义类型的问题。背景说明 例如&#xff0c;我们有如下代码&#xff1a;// 定义一个自定义类型 public class CustomType {public int Id { get…

极限编程 (Extreme Programming) - 发布计划 (Release Planning)

编写用户故事后&#xff0c;您可以使用发布计划会议来创建发布计划。发布计划指定 将为每个系统版本实现哪些用户故事以及这些版本的日期。这给出了一组用户故事供客户在迭代计划会议期间进行选择&#xff0c;以便在下一次迭代期间实施。然后将这些选定的故事翻译成单独的编程任…

使用Ubuntu的公用文件夹轻松地在计算机之间共享文件

You’ve probably noticed that Ubuntu comes with a Public folder in your home directory. This folder isn’t shared by default, but you can easily set up several different types of file-sharing to easily share files on your local network. 您可能已经注意到&am…

NSA泄露的恶意软件DoublePulsar感染了数万台Windows电脑

本文讲的是NSA泄露的恶意软件DoublePulsar感染了数万台Windows电脑&#xff0c;安全研究人员认为&#xff0c;世界各地的脚本小子和在线犯罪分子正在利用Shadow Brokers 黑客组织上周泄露的NSA黑客工具&#xff0c;致使全球数十万台Windows计算机正面临网络攻击威胁。 上周&…

Nginx、LVS及HAProxy负载均衡软件的优缺点详解

转自&#xff1a;https://www.csdn.net/article/2014-07-24/2820837 摘要&#xff1a;Nginx/LVS/HAProxy是目前使用最广泛的三种负载均衡软件&#xff0c;一般对负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术&#xff0c;具体的应用需求还得具体分析&…