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

Go 內(nèi)存優(yōu)化與垃圾收集

開發(fā) 后端
本文介紹了如何通過微調(diào)GOGC和GOMEMLIMIT在性能和內(nèi)存效率之間取得平衡,并盡量避免OOM的產(chǎn)生。

Go提供了自動(dòng)化的內(nèi)存管理機(jī)制,但在某些情況下需要更精細(xì)的微調(diào)從而避免發(fā)生OOM錯(cuò)誤。本文將討論Go的垃圾收集器、應(yīng)用程序內(nèi)存優(yōu)化以及如何防止OOM(Out-Of-Memory)錯(cuò)誤。

Go中的堆(Heap)棧(Stack)

我不會(huì)詳細(xì)介紹垃圾收集器如何工作,已經(jīng)有很多關(guān)于這個(gè)主題的文章和官方文檔(比如A Guide to the Go Garbage Collector[2]和源碼[3])。但是,我會(huì)提到一些有助于理解本文主題的基本概念。

你可能已經(jīng)知道,Go的數(shù)據(jù)可以存儲(chǔ)在兩個(gè)主要的內(nèi)存存儲(chǔ)中: 棧(stack)和堆(heap)。

通常,棧存儲(chǔ)的數(shù)據(jù)的大小和使用時(shí)間可以由Go編譯器預(yù)測(cè),包括函數(shù)局部變量、函數(shù)參數(shù)、返回值等。

棧是自動(dòng)管理的,遵循后進(jìn)先出(LIFO)原則。當(dāng)調(diào)用函數(shù)時(shí),所有相關(guān)數(shù)據(jù)都放在棧的頂部,函數(shù)結(jié)束時(shí),這些數(shù)據(jù)將從棧中刪除。棧不需要復(fù)雜的垃圾收集機(jī)制,其內(nèi)存管理開銷最小,在棧中檢索和存儲(chǔ)數(shù)據(jù)的過程非???。

然而,并不是所有數(shù)據(jù)都可以存儲(chǔ)在棧中。在執(zhí)行過程中動(dòng)態(tài)更改的數(shù)據(jù)或需要在函數(shù)范圍之外訪問的數(shù)據(jù)不能放在棧上,因?yàn)榫幾g器無法預(yù)測(cè)其使用情況,這種數(shù)據(jù)應(yīng)該存儲(chǔ)在堆中。

與棧不同,從堆中檢索數(shù)據(jù)并對(duì)其進(jìn)行管理的成本更高。

棧里放什么,堆里放什么?

正如前面提到的,棧用于具有可預(yù)測(cè)大小和壽命的值,例如:

  • 在函數(shù)內(nèi)部聲明的局部變量,例如基本數(shù)據(jù)類型變量(例如數(shù)字和布爾值)。
  • 函數(shù)參數(shù)。
  • 函數(shù)返回后不再被引用的返回值。

Go編譯器在決定將數(shù)據(jù)放在棧中還是堆中時(shí)會(huì)考慮各種細(xì)微差別。

例如,預(yù)分配大小為64 KB的數(shù)據(jù)將存儲(chǔ)在棧中,而大于64 KB的數(shù)據(jù)將存儲(chǔ)在堆中。這同樣適用于數(shù)組,如果數(shù)組超過10 MB,將存儲(chǔ)在堆中。

可以使用逃逸分析(escape analysis)來確定特定變量的存儲(chǔ)位置。

例如,可以通過命令行編譯參數(shù)-gcflags=-m來分析應(yīng)用程序:

go build -gcflags=-m main.go

如果使用-gcflags=-m參數(shù)編譯下面的main.go:

package main

func main() {
  var arrayBefore10Mb [1310720]int
  arrayBefore10Mb[0] = 1

  var arrayAfter10Mb [1310721]int
  arrayAfter10Mb[0] = 1

  sliceBefore64 := make([]int, 8192)
  sliceOver64 := make([]int, 8193)
  sliceOver64[0] = sliceBefore64[0]
}

結(jié)果是:

# command-line-arguments
./main.go:3:6: can inline main
./main.go:7:6: moved to heap: arrayAfter10Mb
./main.go:10:23: make([]int, 8192) does not escape
./main.go:11:21: make([]int, 8193) escapes to heap

可以看到arrayAfter10Mb數(shù)組被移動(dòng)到堆中,因?yàn)榇笮〕^了10MB,而arrayBefore10Mb仍然留在棧中(對(duì)于int變量,10MB等于10 * 1024 * 1024 / 8 = 1310720個(gè)元素)。

此外,sliceBefore64沒有存儲(chǔ)在堆中,因?yàn)樗拇笮⌒∮?4KB,而sliceOver64被存儲(chǔ)在堆中(對(duì)于int變量,64KB等于64 * 1024 / 8 = 8192個(gè)元素)。

要了解更多關(guān)于在堆中分配的位置和內(nèi)容,可以參考malloc.go源碼[4]。

因此,使用堆的一種方法是盡量避免用它!但是,如果數(shù)據(jù)已經(jīng)落在堆中了呢?

與棧不同,堆的大小是無限的,并且不斷增長(zhǎng)。堆存儲(chǔ)動(dòng)態(tài)創(chuàng)建的對(duì)象,如結(jié)構(gòu)體、分片和映射,以及由于其限制而無法放入棧中的大內(nèi)存塊。

在堆中重用內(nèi)存并防止其完全阻塞的唯一工具是垃圾收集器。

淺談垃圾收集器的工作原理

垃圾收集器(GC)是一種專門用于識(shí)別和釋放動(dòng)態(tài)分配內(nèi)存的系統(tǒng)。

Go使用基于跟蹤和標(biāo)記和掃描算法的垃圾收集算法。在標(biāo)記階段,垃圾收集器將應(yīng)用程序正在使用的數(shù)據(jù)標(biāo)記為活躍堆。然后,在清理階段,GC遍歷所有未標(biāo)記為活躍的內(nèi)存并復(fù)用。

垃圾收集器不是免費(fèi)工作的,需要消耗兩個(gè)重要的系統(tǒng)資源: CPU時(shí)間和物理內(nèi)存。

垃圾收集器中的內(nèi)存由以下部分組成:

  • 活躍堆內(nèi)存(在前一個(gè)垃圾收集周期中標(biāo)記為"活躍"的內(nèi)存)
  • 新的堆內(nèi)存(尚未被垃圾收集器分析的堆內(nèi)存)
  • 存儲(chǔ)元數(shù)據(jù)的內(nèi)存,與前兩個(gè)實(shí)體相比,這些元數(shù)據(jù)通常微不足道。

垃圾收集器所消耗的CPU時(shí)間與其工作細(xì)節(jié)有關(guān)。有一種稱為"stop-the-world"的垃圾收集器實(shí)現(xiàn),它在垃圾收集期間完全停止程序執(zhí)行,導(dǎo)致CPU時(shí)間被花在非生產(chǎn)性工作上。

在Go里,垃圾收集器并不是完全"stop-the-world",而是與應(yīng)用程序并行執(zhí)行其大部分工作(例如標(biāo)記堆)。

但是,垃圾收集器的操作仍然有一些限制,并且會(huì)在一個(gè)周期內(nèi)多次完全停止工作代碼的執(zhí)行,想要了解更多可以閱讀源碼[5]。

如何管理垃圾收集器

在Go中可以通過某些參數(shù)管理垃圾收集器: GOGC環(huán)境變量或runtime/debug包中的等效函數(shù)SetGCPercent。

GOGC參數(shù)確定將觸發(fā)垃圾收集的新未分配堆內(nèi)存相對(duì)于活躍內(nèi)存的百分比。

GOGC的默認(rèn)值是100,意味著當(dāng)新內(nèi)存達(dá)到活躍堆內(nèi)存的100%時(shí)將觸發(fā)垃圾收集。

當(dāng)新堆占用活躍堆的100%時(shí),將運(yùn)行垃圾收集器

我們以示例程序?yàn)槔?,通過go tool trace跟蹤堆大小的變化,我們用Go 1.20.1版本來運(yùn)行程序。

在本例中,performMemoryIntensiveTask函數(shù)使用了在堆中分配的大量?jī)?nèi)存。這個(gè)函數(shù)啟動(dòng)一個(gè)隊(duì)列大小為NumWorker的工作池,任務(wù)數(shù)量等于NumTasks。

package main

import (
 "fmt"
 "os"
 "runtime/debug"
 "runtime/trace"
 "sync"
)

const (
 NumWorkers    = 4     // Number of workers.
 NumTasks      = 500   // Number of tasks.
 MemoryIntense = 10000 // Size of memory-intensive task (number of elements).
)

func main() {
 // Write to the trace file.
 f, _ := os.Create("trace.out")
 trace.Start(f)
 defer trace.Stop()

 // Set the target percentage for the garbage collector. Default is 100%.
 debug.SetGCPercent(100)

 // Task queue and result queue.
 taskQueue := make(chan int, NumTasks)
 resultQueue := make(chan int, NumTasks)

 // Start workers.
 var wg sync.WaitGroup
 wg.Add(NumWorkers)
 for i := 0; i < NumWorkers; i++ {
  go worker(taskQueue, resultQueue, &wg)
 }

 // Send tasks to the queue.
 for i := 0; i < NumTasks; i++ {
  taskQueue <- i
 }
 close(taskQueue)

 // Retrieve results from the queue.
 go func() {
  wg.Wait()
  close(resultQueue)
 }()

 // Process the results.
 for result := range resultQueue {
  fmt.Println("Result:", result)
 }

 fmt.Println("Done!")
}

// Worker function.
func worker(tasks <-chan int, results chan<- int, wg *sync.WaitGroup) {
 defer wg.Done()

 for task := range tasks {
  result := performMemoryIntensiveTask(task)
  results <- result
 }
}

// performMemoryIntensiveTask is a memory-intensive function.
func performMemoryIntensiveTask(task int) int {
 // Create a large-sized slice.
 data := make([]int, MemoryIntense)
 for i := 0; i < MemoryIntense; i++ {
  data[i] = i + task
 }

 // Latency imitation.
 time.Sleep(10 * time.Millisecond)

 // Calculate the result.
 result := 0
 for _, value := range data {
  result += value
 }
 return result
}

跟蹤程序執(zhí)行的結(jié)果被寫入文件trace.out:

// Writing to the trace file.
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()

通過go tool trace,可以觀察堆大小的變化,并分析程序中垃圾收集器的行為。

請(qǐng)注意,go tool trace的精確細(xì)節(jié)和功能可能因go版本不同而有所差異,因此建議參考官方文檔,以獲取有關(guān)其在特定go版本中使用的詳細(xì)信息。

GOGC的默認(rèn)值

GOGC參數(shù)可以使用runtime/debug包中的debug.SetGCPercent進(jìn)行設(shè)置,GOGC默認(rèn)設(shè)置為100%。

用下面命令運(yùn)行程序:

go run main.go

程序執(zhí)行后,將會(huì)創(chuàng)建trace.out文件,可以使用go tool工具對(duì)其進(jìn)行分析。要做到這一點(diǎn),執(zhí)行命令:

go tool trace trace.out

然后可以通過打開web瀏覽器并訪問http://127.0.0.1:54784/trace來查看基于web的跟蹤查看器。

GOGC = 100

在"STATS"選項(xiàng)卡中,可以看到"Heap"字段,顯示了在應(yīng)用程序執(zhí)行期間堆大小的變化情況,圖中紅色區(qū)域表示堆占用的內(nèi)存。

在"PROCS"選項(xiàng)卡中,"GC"(垃圾收集器)字段顯示的藍(lán)色列表示觸發(fā)垃圾收集器的時(shí)刻。

一旦新堆的大小達(dá)到活動(dòng)堆大小的100%,就會(huì)觸發(fā)垃圾收集。例如,如果活躍堆大小為10 MB,則當(dāng)當(dāng)前堆大小達(dá)到10 MB時(shí)將觸發(fā)垃圾收集。

跟蹤所有垃圾收集調(diào)用使我們能夠確定垃圾收集器處于活動(dòng)狀態(tài)的總時(shí)間。

GOGC=100時(shí)的GC調(diào)用次數(shù)

示例中,當(dāng)GOGC值為100時(shí),將調(diào)用垃圾收集器16次,總執(zhí)行時(shí)間為14 ms。

更頻繁的調(diào)用GC

如果我們將debug.SetGCPercent(10)設(shè)置為10%后運(yùn)行代碼,將觀察到垃圾收集器調(diào)用的頻率更高。現(xiàn)在,如果當(dāng)前堆大小達(dá)到活躍堆大小的10%時(shí),將觸發(fā)垃圾收集。

換句話說,如果活躍堆大小為10 MB,則當(dāng)前堆大小達(dá)到1 MB時(shí)就將觸發(fā)垃圾收集。

GOGC = 10

在本例中,垃圾收集器被調(diào)用了38次,總垃圾收集時(shí)間為28 ms。

GOGC=10時(shí)的GC調(diào)用次數(shù)

可以觀察到,將GOGC設(shè)置為低于100%的值可以增加垃圾收集的頻率,可能導(dǎo)致CPU使用率增加并降低程序性能。

更少的調(diào)用GC

如果運(yùn)行相同程序,但將debug.SetGCPercent(1000)設(shè)置為1000%,我們將得到以下結(jié)果:

GOGC = 1000

可以看到,當(dāng)前堆的大小一直在增長(zhǎng),直到達(dá)到活躍堆大小的1000%。換句話說,如果活躍堆大小為10 MB,則當(dāng)前堆大小達(dá)到100 MB時(shí)將觸發(fā)垃圾收集。

GOGC=1000時(shí)的GC調(diào)用次數(shù)

在當(dāng)前情況下,垃圾收集器被調(diào)用一次并執(zhí)行2毫秒。

關(guān)閉GC

還可以通過設(shè)置GOGC=off或調(diào)用debug.SetGCPercent(-1)來禁用垃圾收集。

下面是禁用垃圾收集器而不設(shè)置GOMEMLIMIT時(shí)堆的行為:

當(dāng)GC=off時(shí),堆大小不斷增長(zhǎng)。

可以看到,在關(guān)閉GC后,應(yīng)用程序的堆大小一直在增長(zhǎng),直到程序執(zhí)行為止。

堆占用多少內(nèi)存?

在活躍堆的實(shí)際內(nèi)存分配中,通常不像我們?cè)趖race中看到的那樣定期和可預(yù)測(cè)的工作。

活躍堆隨著每個(gè)垃圾收集周期動(dòng)態(tài)變化,并且在某些條件下,其絕對(duì)值可能出現(xiàn)峰值。

例如,如果由于多個(gè)并行任務(wù)的重疊,活躍堆的大小可以增長(zhǎng)到800 MB,那么只有在當(dāng)前堆大小達(dá)到1.6 GB時(shí)才會(huì)觸發(fā)垃圾收集。

現(xiàn)代開發(fā)通常在具有內(nèi)存使用限制的容器中運(yùn)行應(yīng)用。因此,如果容器將內(nèi)存限制設(shè)置為1 GB,并且總堆大小增加到1.6 GB,則容器將失效,并出現(xiàn)OOM(out of memory)錯(cuò)誤。

讓我們模擬一下這種情況。例如,我們?cè)趦?nèi)存限制為10 MB的容器中運(yùn)行程序(僅用于測(cè)試目的)。Dockerfile:

FROM golang:latest as builder


WORKDIR /src
COPY . .


RUN go env -w GO111MODULE=on


RUN go mod vendor
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -a -installsuffix cgo -o app ./cmd/


FROM golang:latest
WORKDIR /root/
COPY --from=builder /src/app .
EXPOSE 8080
CMD ["./app"]

Docker-compose描述:

version: '3'
services:
 my-app:
   build:
     context: .
     dockerfile: Dockerfile
   ports:
     - 8080:8080
   deploy:
     resources:
       limits:
         memory: 10M

讓我們使用前面設(shè)置GOGC=1000%的代碼啟動(dòng)容器。

可以使用以下命令運(yùn)行容器:

docker-compose build
docker-compose up

幾秒鐘后,容器將崩潰,并產(chǎn)生與OOM相對(duì)應(yīng)的錯(cuò)誤。

exited with code 137

這種情況非常令人不快: GOGC只控制新堆的相對(duì)值,而容器有絕對(duì)限制。

如何避免OOM?

從1.19版本開始,在GOMEMLIMIT選項(xiàng)的幫助下,Golang引入了一個(gè)名為"軟內(nèi)存管理"的特性,runtime/debug包中名為SetMemoryLimit的類似函數(shù)(可以閱讀48409-soft-memory-limit.md[6]了解有關(guān)此選項(xiàng)的一些有趣的設(shè)計(jì)細(xì)節(jié))提供了相同的功能。

GOMEMLIMIT環(huán)境變量設(shè)置Go運(yùn)行時(shí)可以使用的總體內(nèi)存限制,例如: GOMEMLIMIT = 8MiB。要設(shè)置內(nèi)存值,需要使用大小后綴,在本例中為8 MB。

讓我們啟動(dòng)將GOMEMLIMIT境變量設(shè)置為8MiB的容器。為此,我們將環(huán)境變量添加到docker-compose文件中:

version: '3'
services:
 my-app:
    environment:
      GOMEMLIMIT: "8MiB"
   build:
     context: .
     dockerfile: Dockerfile
   ports:
     - 8080:8080
   deploy:
     resources:
       limits:
         memory: 10M

現(xiàn)在,當(dāng)啟動(dòng)容器時(shí),程序運(yùn)行沒有任何錯(cuò)誤。該機(jī)制是專門為解決OOM問題而設(shè)計(jì)的。

這是因?yàn)閱⒂肎OMEMLIMIT=8MiB后,會(huì)定期調(diào)用垃圾收集器,并將堆大小保持在一定限制內(nèi),結(jié)果就是會(huì)頻繁調(diào)用垃圾收集器以避免內(nèi)存過載。

運(yùn)行垃圾收集器以使堆大小保持在一定的限制內(nèi)。

成本是什么?

GOMEMLIMIT是強(qiáng)有力的工具,但也可能適得其反。

在上面的堆跟蹤圖中可以看到這種場(chǎng)景的一個(gè)示例。

當(dāng)總內(nèi)存大小由于活躍堆或持久程序泄漏的增長(zhǎng)而接近GOMEMLIMIT時(shí),將開始根據(jù)該限制不斷調(diào)用垃圾收集器。

由于頻繁調(diào)用垃圾收集器,應(yīng)用程序的運(yùn)行時(shí)可能會(huì)無限增加,從而消耗應(yīng)用程序的CPU時(shí)間。

這種行為被稱為死亡螺旋[7],可能導(dǎo)致應(yīng)用程序性能下降,與OOM錯(cuò)誤不同,這種問題很難檢測(cè)和修復(fù)。

這正是GOMEMLIMIT機(jī)制作為軟限制起作用的原因。

Go不能100%保證GOMEMLIMIT指定的內(nèi)存限制會(huì)被嚴(yán)格執(zhí)行,而是會(huì)允許使用超出限制的內(nèi)存,并防止頻繁調(diào)用垃圾收集器的情況。

為了實(shí)現(xiàn)這一點(diǎn),需要對(duì)CPU使用設(shè)置限制。目前,這個(gè)限制被設(shè)置為所有處理器時(shí)間的50%,CPU窗口為2 * GOMAXPROCS秒。

這就是為什么我們不能完全避免OOM錯(cuò)誤,而是會(huì)將其推遲到很久以后發(fā)生。

在哪里應(yīng)用GOMEMLIMIT和GOGC

如果默認(rèn)垃圾收集器設(shè)置在大多數(shù)情況下是足夠的,那么帶有GOMEMLIMIT的軟內(nèi)存管理機(jī)制可以使我們避免不愉快的情況。

使用GOMEMLIMIT內(nèi)存限制可能有用的例子:

  • 在內(nèi)存有限的容器中運(yùn)行應(yīng)用程序時(shí),最好將GOMEMLIMIT設(shè)置為保留5-10%的可用內(nèi)存。
  • 在運(yùn)行資源密集型庫或代碼時(shí),對(duì)GOMEMLIMIT進(jìn)行實(shí)時(shí)管理是有好處的。
  • 當(dāng)在容器中以腳本形式運(yùn)行應(yīng)用程序時(shí)(意味著應(yīng)用程序在一段時(shí)間內(nèi)執(zhí)行某些任務(wù),然后終止),禁用垃圾收集器但設(shè)置GOMEMLIMIT可以提高性能并防止超出容器的資源限制。

避免使用GOMEMLIMIT的情況:

  • 當(dāng)程序已經(jīng)接近其環(huán)境的內(nèi)存限制時(shí),不要設(shè)置內(nèi)存限制。
  • 在無法控制的執(zhí)行環(huán)境中部署時(shí),不要使用內(nèi)存限制,特別是在程序的內(nèi)存使用與其輸入數(shù)據(jù)成正比的情況下,例如CLI工具或桌面應(yīng)用程序。

如上所述,通過深思熟慮的方法,我們可以管理程序中的微調(diào)設(shè)置,例如垃圾收集器和GOMEMLIMIT。然而,仔細(xì)考慮應(yīng)用這些設(shè)置的策略無疑非常重要。

參考資料

  • [1]Memory Optimization and Garbage Collector Management in Go: https://betterprogramming.pub/memory-optimization-and-garbage-collector-management-in-go-71da4612a960
  • [2]A Guide to the Go Garbage Collector: https://tip.golang.org/doc/gc-guide
  • [3]mgc.go: https://go.dev/src/runtime/mgc.go
  • [4]malloc.go: https://go.dev/src/runtime/malloc.go
  • [5]mgc.go: https://go.dev/src/runtime/mgc.go
  • [6]48409-soft-memory-limit.md: https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md
  • [7]Soft Memory Limit Death Spirals: https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md#death-spirals
責(zé)任編輯:趙寧寧 來源: DeepNoMind
相關(guān)推薦

2011-12-20 10:43:21

Java

2010-02-22 08:58:35

JVM內(nèi)存模型垃圾收集

2009-06-15 16:14:40

Java垃圾收集算法GC

2020-10-26 13:42:28

Python算法垃圾

2010-03-04 10:08:54

.Net垃圾收集

2014-12-19 11:07:40

Java

2011-08-15 16:28:06

Cocoa內(nèi)存管理

2010-03-04 14:33:11

.NET垃圾收集

2024-07-15 08:00:00

2010-01-06 16:33:50

.Net Framew

2023-11-21 08:03:43

語言架構(gòu)偏移量

2017-02-21 16:40:16

Android垃圾回收內(nèi)存泄露

2009-09-02 09:23:26

.NET內(nèi)存管理機(jī)制

2021-09-07 11:23:09

智能垃圾箱物聯(lián)網(wǎng)IOT

2024-05-28 00:00:03

Java垃圾收集機(jī)制

2023-08-08 10:29:55

JVM優(yōu)化垃圾回收

2021-11-17 08:16:03

內(nèi)存控制Go

2011-05-10 16:04:45

Java垃圾收集器

2011-08-17 15:37:23

Objective-C垃圾收集

2023-12-19 21:52:51

Go垃圾回收開發(fā)
點(diǎn)贊
收藏

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