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.pswp.cn/news/280361.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/280361.shtml
英文地址,請注明出處:http://en.pswp.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;具體的應用需求還得具體分析&…