<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          通過 eBPF 深入探究 Go GC

          共 7807字,需瀏覽 16分鐘

           ·

          2022-02-26 15:29

          點(diǎn)擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

          對程序員來說,內(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)用程序。垃圾收集器是這個(gè)難題的關(guān)鍵部分。本文就探討 Go 中的 GC。

          為了更好地理解垃圾收集器的工作原理,我決定在實(shí)時(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ù)參數(shù)、返回值、延遲和時(shí)間戳都可以通過 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ù)版本中可能會發(fā)生變化。

          垃圾回收的階段

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

          • https://agrim123.github.io/posts/go-garbage-collector.html
          • https://en.wikipedia.org/wiki/Tracing_garbage_collection
          • https://go.dev/blog/ismmkeynote
          • https://www.iecc.com/gclist/GC-algorithms.html

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

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

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

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

          一種節(jié)點(diǎn)著色算法。黑色表示仍在使用中。白色表示已準(zhǔn)備好清理?;疑硎救匀恍枰诸悶楹谏虬咨?br>

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

          這是一個(gè)簡單的端點(diǎn)(endpoint),我將使用它來觸發(fā)垃圾收集器。它創(chuàng)建一個(gè)可變大小的字符串?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)并生成了一個(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 會觀察到以下事件:

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

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

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

          arrayLengthbytesPerElementApproximate size (B)GC latency (ms)GC throughput (MB/s)
          1001,000100,0003.231
          1,0001,0001,000,0008.5118
          10,0001,00010,000,00053.7186
          10010,0001,000,0003.2313
          1,00010,00010,000,00012.4807
          10,00010,000100,000,00096.21,039

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

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

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

          函數(shù)描述捕獲的信息
          allocSpan[8]分配新內(nèi)存分配的內(nèi)存頁
          gcDrainN[9]執(zhí)行 N 個(gè)單位的標(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 收集的事件示例

          繪制為時(shí)間序列更容易解釋:

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

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

          sweepone 隨時(shí)間清除的內(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”是指垃圾收集器暫時(shí)停止除自身之外的一切,以安全地修改狀態(tài)。我們通常更喜歡最小化 STW 階段,因?yàn)?STW 會減慢我們的程序速度(通常是在最不方便的時(shí)候……)。

          一些垃圾收集器會在垃圾收集運(yùn)行的整個(gè)過程中 stop the world。這些是“非并發(fā)”垃圾收集器。雖然 Go 的垃圾收集器在很大程度上是并發(fā)的,但我們可以從代碼中看到,它在技術(shù)上確實(shí)在兩個(gè)地方 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í)間戳之后,事實(shí)證明 STW 第一次停止了 300 μs,第二次停止了 365 μs。換句話說,~80%垃圾收集是同時(shí)執(zhí)行的。當(dāng)垃圾收集器在實(shí)際內(nèi)存壓力下自動調(diào)用時(shí),我們預(yù)計(jì)這個(gè)比率會變得更好。

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

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

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

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

          知道何時(shí)運(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ì)垃圾收集的最佳時(shí)間。這有助于 Go 滿足其內(nèi)存和 CPU 目標(biāo),而不會犧牲不必要的應(yīng)用程序性能。

          pacer,可以理解為定速裝置

          觸發(fā)率

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

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

          讓我們看看當(dāng)我們一次分配大量內(nèi)存時(shí),垃圾收集器的觸發(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ā)率隨時(shí)間的變化

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

          然而,一旦我們請求端點(diǎn)進(jìn)行 ~81MB 堆分配時(shí),觸發(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)記在一段時(shí)間內(nèi)完成的工作

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

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

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

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

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

          可以看到,gcAssistAlloc1 就是 mark 和 sweep 工作的來源。它收到了完成大約 30 萬個(gè)工作單元的請求。在之前的標(biāo)記階段圖中,gcDrainN 在相同的時(shí)間段完成了大約 30 萬個(gè)標(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] 是另一個(gè)創(chuàng)建 uprobes 的好工具。

          檢查 Go 垃圾收集器行為的另一個(gè)不錯(cuò)的選擇是 gc 跟蹤器。只需在你啟動程序時(shí)傳入 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



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。


          瀏覽 77
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一级a黄色视频 | 亚洲天堂视频在线 | 3344在线观看免费下载视频 | 丁香五月激情中文 | 天天摸日日摸狠狠添 |