偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

通過(guò) eBPF 深入探究 Go GC

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

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

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

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

1、前提知識(shí)

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

為什么用 uprobes?

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

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

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

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

垃圾回收的階段

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

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

垃圾收集有兩個(gè)主要階段:

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

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

一種節(jié)點(diǎn)著色算法。黑色表示仍在使用中。白色表示已準(zhǔn)備好清理?;疑硎救匀恍枰诸?lèi)為黑色或白色

一個(gè)簡(jiǎn)單的演示應(yīng)用程序

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

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

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ǔ)知識(shí),接下來(lái)深入觀察它的行為。

跟蹤 runtime.GC()

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

函數(shù)

描述

GC[3]

調(diào)用 GC

gcWaitOnMark[4]

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

gcSweep[5]

執(zhí)行清除階段

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

部署 uprobes 后,點(diǎn)擊端點(diǎn)并生成了一個(gè)包含 10 個(gè)字符串的數(shù)組,每個(gè)字符串為 20 個(gè)字節(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

這時(shí) uprobes 會(huì)觀察到以下事件:

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

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

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

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)記和清除階段

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

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

函數(shù)

描述

捕獲的信息

allocSpan[8]

分配新內(nèi)存

分配的內(nèi)存頁(yè)

gcDrainN[9]

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

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

sweepone[10]

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

清除的內(nèi)存頁(yè)

$ 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 收集的事件示例

繪制為時(shí)間序列更容易解釋?zhuān)?/p>

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

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

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

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

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

追蹤 Stop The World 事件

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

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

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

函數(shù)

描述

stopTheWorldWithSema[11]

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

startTheWorldWithSema[12]

啟動(dòng)暫停的 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)生了如下事件:

生成啟動(dòng)和停止 STW 事件

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

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

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

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

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

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

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

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

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

pacer,可以理解為定速裝置

觸發(fā)率

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

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

讓我們看看當(dāng)我們一次分配大量?jī)?nèi)存時(shí),垃圾收集器的觸發(fā)率是如何變化的。我們可以通過(guò)跟蹤函數(shù)來(lái)獲取觸發(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ā)率隨時(shí)間的變化

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

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

標(biāo)記和清除

助手當(dāng)分配內(nèi)存但不調(diào)用垃圾收集器會(huì)發(fā)生什么?接下來(lái),請(qǐng)求 /allocate-memory 端點(diǎn),它和 /allocate-memory-and-gc 類(lèi)似,但不調(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)該還沒(méi)有啟動(dòng)。但是,我們看到標(biāo)記和清除仍然發(fā)生了:

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

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

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

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

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

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

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

4、總結(jié)

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

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

就像我們?cè)诒疚睦又兴龅哪菢?,?chuàng)建 uprobes 通常最好在更高級(jí)別的 BPF 框架中完成。對(duì)于這篇文章,我使用了 Pixie 的 Dynamic Go 日志記錄[19]功能(仍處于 alpha 階段)。bpftrace[20] 是另一個(gè)創(chuàng)建 uprobes 的好工具。

檢查 Go 垃圾收集器行為的另一個(gè)不錯(cuò)的選擇是 gc 跟蹤器。只需在你啟動(dòng)程序時(shí)傳入 GODEBUG=gctrace=1。這會(huì)輸出有關(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)載自微信公眾號(hào)「幽鬼」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系幽鬼公眾號(hào)。

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

2017-05-18 15:02:36

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

2025-01-02 14:50:34

MyBatis開(kāi)發(fā)緩存

2011-12-22 14:27:11

2013-07-15 11:03:52

802.11ac技術(shù)802.11ac

2010-02-04 16:52:01

多層交換技術(shù)

2009-11-12 14:32:00

BGP路由協(xié)議

2009-11-27 10:37:41

GPRS路由

2010-08-04 09:43:28

Flex應(yīng)用程序

2009-12-09 10:07:19

Linux靜態(tài)路由

2010-11-29 11:22:36

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

2021-09-29 09:24:21

GCGo STW

2009-12-09 13:35:09

靜態(tài)路由配置

2009-11-20 09:56:27

軟交換路由技術(shù)

2023-06-27 08:37:35

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

2010-09-15 14:00:06

position屬性DIV

2010-09-29 14:54:34

J2MEHashtable

2009-10-19 18:26:44

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

2009-11-06 13:27:47

寬帶接入網(wǎng)

2021-07-05 22:13:09

Node內(nèi)存控制

2009-12-23 16:40:51

寬帶路由器
點(diǎn)贊
收藏

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