自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

通過 eBPF 深入探究 Go GC

開發(fā) 前端
對程序員來說,內(nèi)存管理是很重要的。編程語言按內(nèi)存管理方式一般可以分為手動內(nèi)存管理和自動內(nèi)存管理。手動內(nèi)存管理典型代表有 C、C++;自動內(nèi)存管理代表有 Java、C# 等。

大家好,我是程序員幽鬼。

對程序員來說,內(nèi)存管理是很重要的。編程語言按內(nèi)存管理方式一般可以分為手動內(nèi)存管理和自動內(nèi)存管理。手動內(nèi)存管理典型代表有 C、C++;自動內(nèi)存管理代表有 Java、C# 等。通常,自動內(nèi)存管理即自帶垃圾收集器,即 GC(當(dāng)然,Rust 另辟蹊徑,它既沒有 GC,也不需要手動內(nèi)存管理,感興趣的可以了解下)。Go 語言也采用了 GC 的方式管理內(nèi)存,雖然 Gopher 不需要手動管理內(nèi)存了,但了解 Go 如何分配和釋放內(nèi)存可以讓我們編寫更好、更高效的應(yīng)用程序。垃圾收集器是這個難題的關(guān)鍵部分。本文就探討 Go 中的 GC。

為了更好地理解垃圾收集器的工作原理,我決定在實(shí)時應(yīng)用程序上跟蹤它的底層行為。本文將使用 eBPF uprobes 檢測 Go 垃圾收集器。這篇文章的源代碼在這里[1]。

1、前提知識

在深入研究之前,讓我們快速了解一下 uprobes、垃圾收集器的設(shè)計(jì)以及我們將使用的演示應(yīng)用程序。

為什么用 uprobes?

uprobes[2] 很酷,因?yàn)樗鼈冏屛覀儫o需修改代碼即可動態(tài)收集新信息。當(dāng)你不能或不想重新部署你的應(yīng)用程序時,這會非常有用。

函數(shù)參數(shù)、返回值、延遲和時間戳都可以通過 uprobes 收集。在這篇文章中,我將把 uprobes 部署到 Go 垃圾收集器的關(guān)鍵函數(shù)上。這讓我們能看到它在正在運(yùn)行的應(yīng)用程序中的實(shí)際表現(xiàn)。

uprobes 可以跟蹤延遲、時間戳、參數(shù)和函數(shù)的返回值片

注意:這篇文章使用的 Go 版本是 1.16。我將在 Go 運(yùn)行時中跟蹤私有函數(shù),因此這些功能在 Go 的后續(xù)版本中可能會發(fā)生變化。

垃圾回收的階段

Go 使用并發(fā)標(biāo)記和清除垃圾收集器。對于那些不熟悉這些術(shù)語的人,閱讀以下內(nèi)容,方便你理解本文其他內(nèi)容。

Go 的垃圾收集器被稱為并發(fā)的,因?yàn)樗梢园踩嘏c主程序并行運(yùn)行。換句話說,它不需要停止你程序的執(zhí)行來完成它的工作(稍后會詳細(xì)介紹)。

垃圾收集有兩個主要階段:

標(biāo)記(Mark)階段:識別并標(biāo)記程序不再需要的對象。

清除(Sweep)階段:對于標(biāo)記階段標(biāo)記為“無法訪問”的每個對象,釋放內(nèi)存以供其他地方使用。

一種節(jié)點(diǎn)著色算法。黑色表示仍在使用中。白色表示已準(zhǔn)備好清理。灰色表示仍然需要分類為黑色或白色

一個簡單的演示應(yīng)用程序

這是一個簡單的端點(diǎn)(endpoint),我將使用它來觸發(fā)垃圾收集器。它創(chuàng)建一個可變大小的字符串?dāng)?shù)組,然后通過調(diào)用 runtime.GC() 來啟動垃圾收集器。

實(shí)際代碼中,你不需要手動調(diào)用垃圾收集器,因?yàn)?Go 會自動為你處理。

http.HandleFunc("/allocate-memory-and-run-gc", func(w http.ResponseWriter, r *http.Request) {
arrayLength, bytesPerElement := parseArrayArgs(r)
arr := generateRandomStringArray(arrayLength, bytesPerElement)
fmt.Fprintf(w, fmt.Sprintf("Generated string array with %d bytes of data\n", len(arr) * len(arr[0])))
runtime.GC()
fmt.Fprintf(w, "Ran garbage collector\n")
})

2、跟蹤垃圾收集的主要階段

我們已經(jīng)了解了 uprobes 和 Go 垃圾收集器的基礎(chǔ)知識,接下來深入觀察它的行為。

跟蹤 runtime.GC()

首先,我們計(jì)劃在 Go 的 runtime 庫中的以下函數(shù)中添加 uprobes:

函數(shù)

描述

GC[3]

調(diào)用 GC

gcWaitOnMark[4]

等待標(biāo)記階段完成

gcSweep[5]

執(zhí)行清除階段

(如果你有興趣了解 uprobes 是如何生成的,這里是代碼[6]。)

部署 uprobes 后,點(diǎn)擊端點(diǎn)并生成了一個包含 10 個字符串的數(shù)組,每個字符串為 20 個字節(jié)。

$ curl '127.0.0.1:8080/allocate-memory-and-run-gc?arrayLength=10&bytesPerElement=20'
Generated string array with 200 bytes of data
Ran garbage collector

這時 uprobes 會觀察到以下事件:

在運(yùn)行垃圾收集器后,為 GC、gcWaitOnMark 和 gcSweep 收集事件

從源代碼[7]來看這是有道理的——gcWaitOnMark被調(diào)用兩次,一次是在開始下一個循環(huán)之前對前一個循環(huán)進(jìn)行驗(yàn)證。標(biāo)記階段觸發(fā)清除階段。

接下來,使用各種輸入請求 /allocate-memory-and-run-gc 端點(diǎn)對 runtime.GC 后的延遲進(jìn)行了一些測量。

arrayLength

bytesPerElement

Approximate size (B)

GC latency (ms)

GC throughput (MB/s)

100

1,000

100,000

3.2

31

1,000

1,000

1,000,000

8.5

118

10,000

1,000

10,000,000

53.7

186

100

10,000

1,000,000

3.2

313

1,000

10,000

10,000,000

12.4

807

10,000

10,000

100,000,000

96.2

1,039

跟蹤標(biāo)記和清除階段

雖然這是一個很好的高級視圖,但我們可以使用更多細(xì)節(jié)。接下來探索一些用于內(nèi)存分配、標(biāo)記和清除的輔助函數(shù),以獲取下一級信息。

這些輔助函數(shù)有參數(shù)或返回值,可以幫助我們更好地可視化正在發(fā)生的事情(例如分配的內(nèi)存頁)。

函數(shù)

描述

捕獲的信息

allocSpan[8]

分配新內(nèi)存

分配的內(nèi)存頁

gcDrainN[9]

執(zhí)行 N 個單位的標(biāo)記工作

完成的標(biāo)記工作單位

sweepone[10]

從 span 中清除內(nèi)存

清除的內(nèi)存頁

$ curl '127.0.0.1:8080/allocate-memory-and-run-gc?arrayLength=20000&bytesPerElement=4096'
Generated string array with 81920000 bytes of data
Ran garbage collector

在以更大的負(fù)載命中垃圾收集器之后,以下是原始結(jié)果:

調(diào)用垃圾收集器后,allocSpan、gcDrainN 和 sweepone 收集的事件示例

繪制為時間序列更容易解釋:

allocSpan 隨時間分配的內(nèi)存頁

gcDrain 標(biāo)記在一段時間內(nèi)完成的工作

sweepone 隨時間清除的內(nèi)存頁

現(xiàn)在我們可以看到發(fā)生了什么:

  • Go 分配了幾千內(nèi)存頁,這是正常的,因?yàn)槲覀冎苯酉蚨阎刑砑恿舜蠹s 80MB 的字符串。
  • 標(biāo)記工作拉開了序幕(注意它的單位不是頁,而是標(biāo)記工作單位)
  • 有標(biāo)記的內(nèi)存頁被清除器清除。(這應(yīng)該是所有內(nèi)存頁,因?yàn)樵谡{(diào)用完成后我們不會重用字符串?dāng)?shù)組)。

追蹤 Stop The World 事件

“Stopping the world”是指垃圾收集器暫時停止除自身之外的一切,以安全地修改狀態(tài)。我們通常更喜歡最小化 STW 階段,因?yàn)?STW 會減慢我們的程序速度(通常是在最不方便的時候……)。

一些垃圾收集器會在垃圾收集運(yùn)行的整個過程中 stop the world。這些是“非并發(fā)”垃圾收集器。雖然 Go 的垃圾收集器在很大程度上是并發(fā)的,但我們可以從代碼中看到,它在技術(shù)上確實(shí)在兩個地方 STW 了。

我們跟蹤以下函數(shù):

函數(shù)

描述

stopTheWorldWithSema[11]

停止其他 goroutine 直到??startTheWorldWithSema??被調(diào)用

startTheWorldWithSema[12]

啟動暫停的 goroutine

再次觸發(fā) GC:

$ curl '127.0.0.1:8080/allocate-memory-and-run-gc?arrayLength=10&bytesPerElement=20'
Generated string array with 200 bytes of data
Ran garbage collector

這次產(chǎn)生了如下事件:

生成啟動和停止 STW 事件

我們可以從GC事件中看到垃圾收集需要 3.1 毫秒才能完成。在我檢查了確切的時間戳之后,事實(shí)證明 STW 第一次停止了 300 μs,第二次停止了 365 μs。換句話說,~80%垃圾收集是同時執(zhí)行的。當(dāng)垃圾收集器在實(shí)際內(nèi)存壓力下自動調(diào)用時,我們預(yù)計(jì)這個比率會變得更好。

為什么 Go 垃圾收集器需要 STW?

1st Stop The World(標(biāo)記階段之前):設(shè)置狀態(tài)并打開寫屏障。寫屏障確保在 GC 運(yùn)行時正確跟蹤新的寫入(這樣它們就不會被意外釋放或保留)。

2nd Stop The World(標(biāo)記階段之后):清理標(biāo)記狀態(tài)并關(guān)閉寫屏障。

3、垃圾收集器如何調(diào)整自己的速度?

知道何時運(yùn)行垃圾收集是 Go 等并發(fā)垃圾收集器的重要考慮因素。

早期的垃圾收集器被設(shè)計(jì)為一旦達(dá)到一定的內(nèi)存消耗水平就會啟動。如果垃圾收集器是非并發(fā)的,這可以正常工作。但是使用并發(fā)垃圾收集器,主程序在垃圾收集期間仍在運(yùn)行 —— 因此可能仍在進(jìn)行內(nèi)存分配。

這意味著如果太晚運(yùn)行垃圾收集器,可能會超出內(nèi)存目標(biāo)。(Go 也不能一直運(yùn)行垃圾收集 —— GC 會從主應(yīng)用程序中奪走資源和性能。)

Go 的垃圾收集器使用 pacer[13] 來估計(jì)垃圾收集的最佳時間。這有助于 Go 滿足其內(nèi)存和 CPU 目標(biāo),而不會犧牲不必要的應(yīng)用程序性能。

pacer,可以理解為定速裝置

觸發(fā)率

Go 的并發(fā)垃圾收集器依賴于一個 pacer 來確定何時進(jìn)行垃圾收集。但它是如何做出這個決定的呢?

每次調(diào)用垃圾收集器時,pacer 都會更新其內(nèi)部目標(biāo),即下次應(yīng)該何時運(yùn)行 GC。這個目標(biāo)稱為觸發(fā)率。觸發(fā)率0.6意味著一旦堆大小增加 60%,系統(tǒng)應(yīng)該運(yùn)行垃圾收集。觸發(fā)率是CPU、內(nèi)存和其他因素共同決定的數(shù)字。

讓我們看看當(dāng)我們一次分配大量內(nèi)存時,垃圾收集器的觸發(fā)率是如何變化的。我們可以通過跟蹤函數(shù)來獲取觸發(fā)率gcSetTriggerRatio。

$ curl '127.0.0.1:8080/allocate-memory-and-run-gc?arrayLength=20000&bytesPerElement=4096'
Generated string array with 81920000 bytes of data
Ran garbage collector

觸發(fā)率隨時間的變化

從圖中可以看到,最初,觸發(fā)率相當(dāng)高。運(yùn)行時已經(jīng)確定,在程序使用 450% 或更多內(nèi)存之前,不需要進(jìn)行垃圾收集。這是有道理的,因?yàn)閼?yīng)用程序沒有做太多事情(并且沒有使用很多堆)。

然而,一旦我們請求端點(diǎn)進(jìn)行 ~81MB 堆分配時,觸發(fā)率迅速下降到 ~1?,F(xiàn)在如果增加 100% 的內(nèi)存就可以進(jìn)行垃圾收集(因?yàn)槲覀兊膬?nèi)存消耗增加了)。

標(biāo)記和清除

助手當(dāng)分配內(nèi)存但不調(diào)用垃圾收集器會發(fā)生什么?接下來,請求 /allocate-memory 端點(diǎn),它和 /allocate-memory-and-gc 類似,但不調(diào)用runtime.GC()。

$ curl '127.0.0.1/allocate-memory?arrayLength=10000&bytesPerElement=10000'
Generated string array with 100000000 bytes of data

根據(jù)最近的觸發(fā)率,垃圾收集器應(yīng)該還沒有啟動。但是,我們看到標(biāo)記和清除仍然發(fā)生了:

gcDrain 標(biāo)記在一段時間內(nèi)完成的工作

sweepone 隨時間清除的內(nèi)存頁

事實(shí)證明,垃圾收集器還有另一個技巧可以防止失控的內(nèi)存增長。如果堆內(nèi)存開始增長過快,垃圾收集器將對任何分配新內(nèi)存的請求收“稅”。請求新堆分配的 Goroutines 將必須先協(xié)助垃圾收集,然后才能獲得它們所要求的東西。

這種“輔助”系統(tǒng)增加了分配的延遲,因此有助于系統(tǒng)抗壓(backpressure)。這非常重要,因?yàn)樗鉀Q了并發(fā)垃圾收集器可能引起的問題。在并發(fā)垃圾收集器中,內(nèi)存分配在垃圾收集運(yùn)行時仍進(jìn)行內(nèi)存分配。如果程序分配內(nèi)存的速度快于垃圾收集器釋放它的速度,那么內(nèi)存增長將是無限的。通過減慢(背壓)新內(nèi)存的凈分配來幫助解決這個問題。

我們可以跟蹤 gcAssistAlloc1[14] 以查看此過程的運(yùn)行情況。gcAssistAlloc1 接受一個名為 scanWork 的參數(shù),它是請求的輔助工作量。

gcAllocAssist1 在一段時間內(nèi)執(zhí)行的輔助工作量

可以看到,gcAssistAlloc1 就是 mark 和 sweep 工作的來源。它收到了完成大約 30 萬個工作單元的請求。在之前的標(biāo)記階段圖中,gcDrainN 在相同的時間段完成了大約 30 萬個標(biāo)記工作單元(只是稍微分散一點(diǎn))。

4、總結(jié)

還有很多關(guān)于 Go 中的內(nèi)存分配和垃圾收集的知識!這里有一些其他的資源可以查看:

  • Go 對小對象的特殊清除[15]
  • 通過逃逸分析[16]查看對象是分配在堆還是棧
  • sync.Pool[17],一種并發(fā)數(shù)據(jù)結(jié)構(gòu),通過池的方式共享對象來減少分配[18]

就像我們在本文例子中所做的那樣,創(chuàng)建 uprobes 通常最好在更高級別的 BPF 框架中完成。對于這篇文章,我使用了 Pixie 的 Dynamic Go 日志記錄[19]功能(仍處于 alpha 階段)。bpftrace[20] 是另一個創(chuàng)建 uprobes 的好工具。

檢查 Go 垃圾收集器行為的另一個不錯的選擇是 gc 跟蹤器。只需在你啟動程序時傳入 GODEBUG=gctrace=1。這會輸出有關(guān)垃圾收集器正在做什么的各種有用信息。

原文鏈接:https://blog.px.dev/go-garbage-collector/。

參考資料

參考資料

[1]這里: https://github.com/pixie-io/pixie-demos/tree/main/go-garbage-collector

[2]uprobes: https://jvns.ca/blog/2017/07/05/linux-tracing-systems/#uprobes

[3]GC: https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1126

[4]gcWaitOnMark: https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1201

[5]gcSweep: https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L2170

[6]代碼: https://github.com/pixie-io/pixie-demos/tree/main/go-garbage-collector

[7]從源代碼: https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go#L1126

[8]allocSpan: https://github.com/golang/go/blob/go1.16/src/runtime/mheap.go#L1124

[9]gcDrainN: https://github.com/golang/go/blob/go1.16/src/runtime/mgcmark.go#L1095

[10]sweepone: https://github.com/golang/go/blob/go1.16/src/runtime/mgcsweep.go#L188

[11]stopTheWorldWithSema: https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L1073

[12]startTheWorldWithSema: https://github.com/golang/go/blob/go1.16/src/runtime/proc.go#L1151

[13]pacer: https://go.googlesource.com/proposal/+/a216b56e743c5b6b300b3ef1673ee62684b5b63b/design/44167-gc-pacer-redesign.md

[14]gcAssistAlloc1: https://github.com/golang/go/blob/go1.16/src/runtime/mgcmark.go#L504

[15]特殊清除: https://github.com/golang/go/blob/master/src/runtime/mgc.go#L93

[16]逃逸分析: https://medium.com/a-journey-with-go/go-introduction-to-the-escape-analysis-f7610174e890

[17]sync.Pool: https://pkg.go.dev/sync#Pool

[18]減少分配: https://medium.com/swlh/go-the-idea-behind-sync-pool-32da5089df72

[19]Dynamic Go 日志記錄: https://docs.px.dev/tutorials/custom-data/dynamic-go-logging/

[20]bpftrace: https://github.com/iovisor/bpftrace

本文轉(zhuǎn)載自微信公眾號「幽鬼」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系幽鬼公眾號。

責(zé)任編輯:武曉燕 來源: 幽鬼
相關(guān)推薦

2017-05-18 15:02:36

AndroidGC原理JVM內(nèi)存回收

2025-01-02 14:50:34

MyBatis開發(fā)緩存

2011-12-22 14:27:11

2013-07-15 11:03:52

802.11ac技術(shù)802.11ac

2009-11-27 10:37:41

GPRS路由

2010-02-04 16:52:01

多層交換技術(shù)

2009-11-12 14:32:00

BGP路由協(xié)議

2009-12-09 10:07:19

Linux靜態(tài)路由

2010-08-04 09:43:28

Flex應(yīng)用程序

2010-11-29 11:22:36

SYBASE數(shù)據(jù)庫日志

2021-09-29 09:24:21

GCGo STW

2009-12-09 13:35:09

靜態(tài)路由配置

2009-11-20 09:56:27

軟交換路由技術(shù)

2010-09-29 14:54:34

J2MEHashtable

2023-06-27 08:37:35

Java反射動態(tài)代理機(jī)制

2010-09-15 14:00:06

position屬性DIV

2009-11-06 13:27:47

寬帶接入網(wǎng)

2009-12-23 16:40:51

寬帶路由器

2009-10-19 18:26:44

網(wǎng)絡(luò)綜合布線工程

2021-06-18 09:17:10

探究Node前端開發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號