Irbisa · Шпаргалка

Рантайм, планировщик и GC

G-M-P, вытеснение, escape-анализ, GOGC, GOMEMLIMIT, pprof

Senior-разработчик · 25 вопросов

  1. 01

    Кто освобождает память в Go и может ли Go-программа всё-таки течь?

    Память освобождает сборщик мусора: в Go нет free и ручного освобождения (встроенный delete лишь удаляет ключ из map), а где живёт значение (на стеке или в куче), решает компилятор.

  2. 02

    Как работает планировщик Go: что такое G, M и P и зачем нужен P?

    Рантайм выполняет множество горутин (G) на нескольких потоках ОС (M).

  3. 03

    С GOMAXPROCS(1) эта программа на Go 1.13 зависала навсегда, а сегодня печатает done. Планировщик Go кооперативный или вытесняющий и что изменилось?

    func main() {	runtime.GOMAXPROCS(1) 	go func() {		for {			// no function calls, no allocations		}	}() 	time.Sleep(10 * time.Millisecond)	fmt.Println("done")}

    Вытесняющий, начиная с Go 1.14.

  4. 04

    Что делает sysmon в рантайме Go и почему он работает без P?

    sysmon представляет собой служебный поток рантайма, который крутится в цикле без P и следит за планировщиком снаружи: забирает P у потоков, застрявших в системных вызовах, вытесняет слишком долго работающие горутины и не даёт остановиться сетевому поллеру и GC.

  5. 05

    У этой рекурсии нет базового случая. Что напечатает программа, до какого размера вырос стек горутины и почему recover её не спасает?

    func depth(n int) int {	return depth(n+1) + 1} func main() {	defer func() {		if r := recover(); r != nil {			fmt.Println("recovered:", r)		}	}()	fmt.Println(depth(0))}

    Она напечатает runtime: goroutine stack exceeds 1000000000-byte limit и fatal error: stack overflow и завершится с кодом 2.

  6. 06

    Что задаёт GOMAXPROCS и как с ним взаимодействует лимит CPU контейнера до и после Go 1.25?

    GOMAXPROCS задаёт число P, то есть сколько потоков могут одновременно выполнять код Go.

  7. 07

    Как найти в Go-сервисе функцию, которая жжёт CPU, с помощью pprof: как его включить, снять профиль, прочитать top и flame graph и сколько это стоит?

    Подключите net/http/pprof на закрытом порту, снимите под реальной нагрузкой go tool pprof -http=:8081 'http://127.0.0.1:6060/debug/pprof/profile?seconds=30' и читайте top и flame graph по накопленному времени.

    import (	"log"	"net/http"	_ "net/http/pprof" // registers /debug/pprof/* on http.DefaultServeMux)…
  8. 08

    Что такое escape analysis? Какие из этих функций кладут значение в кучу и как это проверить?

    type Point struct{ X, Y int } var global *Point func local() int {	p := &Point{1, 2}	return p.X} func returned() *Point {	p := Point{1, 2}	return &p} func stored()       { global = &Point{3, 4} }func printed(n int) { fmt.Println(n) }

    Escape analysis (анализ побега) представляет собой решение компилятора для каждого значения: может ли оно пережить функцию, которая его создала.

  9. 09

    Как работает сборщик мусора в Go: трёхцветная разметка, барьер записи и когда останавливается мир?

    В Go конкурентный, неперемещающий трёхцветный mark-and-sweep: он размечает достижимые объекты, пока ваши горутины работают, останавливает мир только дважды за цикл, обычно заметно меньше чем на миллисекунду, и лениво подметает (sweep) память по мере её повторного использования.

  10. 10

    Что задаёт GOGC, почему сервис с живой кучей 100 МБ занимает около 200 МБ и что будет, если значение поднять?

    GOGC задаёт, насколько куча может вырасти сверх живых данных до следующего цикла: цель составляет примерно живая куча + (живая куча + стеки + глобальные переменные) × GOGC/100.

  11. 11

    Что печатают GODEBUG=gctrace=1 и schedtrace=1000 и как читать строку gctrace?

    gctrace=1 печатает в stderr строку на каждый цикл GC (время пауз и разметки, размеры кучи и долю CPU на GC); schedtrace=1000 раз в секунду печатает состояние планировщика.

    $ GODEBUG=gctrace=1,schedtrace=1000 ./service 2> runtime.log gc 42 @12.513s 4%: 0.031+3.1+0.052 ms clock, 0.25+0.40/6.1/9.0+0.42 ms cpu, 118->121->62 MB, 124 MB goal, 0 MB stacks, 0 MB globals, 8 P SCHED 13004ms: gomaxprocs=8 idleprocs=0 threads=14 spinningthreads=0 needspinning=0 idlethreads=4 runqueue=37 [12 9 30 4 18 22 7 15]
  12. 12

    Найдите баг: сервис хранит лишь 256-байтовое превью каждой загрузки, но живая куча растёт на мегабайты с каждым файлом.

    var mu sync.Mutexvar previews = map[string][]byte{} // first bytes, shown in the file list func saveUpload(id string, r io.Reader) error {	data, err := io.ReadAll(r) // uploads are 1–20 MB	if err != nil {		return err	}	if err := store(id, data); err != nil {		return err	}	mu.Lock()	previews[id] = data[:min(len(data), 256)]	mu.Unlock()	return nil}

    data[:256] создаёт лишь новый заголовок над многомегабайтным массивом, который вернул io.ReadAll, поэтому каждое 256-байтовое превью держит достижимой всю загрузку: map растёт на размер каждого файла, а не на 256 байт.

  13. 13

    Что происходит с потоком ОС, когда горутина блокируется на канале, на чтении из сокета и на чтении файла или вызове cgo?

    Поток держат только последние два случая.

    // Regular-file reads and cgo calls each hold an OS thread while they run:// bound them, or one burst leaves you with thousands of threads.type diskPool struct{ sem chan struct{} } func newDiskPool(n int) *diskPool { return &diskPool{sem: make(chan struct{}, n)} }…
  14. 14

    Как компилятор Go решает, что встраивать (inline), и почему встраивание может изменить, уходит ли значение в кучу?

    Компилятор встраивает функцию, тело которой стоит не более 80 узлов синтаксического дерева.

    type Point struct{ X, Y int } // -m: can inline NewPointfunc NewPoint(x, y int) *Point { return &Point{X: x, Y: y} } //go:noinline…
  15. 15

    Как устроен аллокатор памяти Go (классы размеров, mcache, mcentral, mheap) и как он ограничивает фрагментацию, не перемещая объекты?

    Аллокатор Go, построенный по образцу TCMalloc, округляет каждый маленький объект до одного из примерно 68 классов размеров и выдаёт его из кэша своего P без блокировок.

  16. 16

    Сервис тратит треть CPU в runtime.mallocgc и воркерах GC. Как сократить аллокации на горячем пути и как доказать каждое исправление?

    Стоимость GC зависит от темпа аллокаций, а не от размера кучи, поэтому лечится это сокращением аллокаций: найдите главные места через -alloc_space, исправьте их и докажите каждую правку цифрой allocs/op до и после.

    type Row struct {	Name  string	Count int64} var bufs = sync.Pool{New: func() any { return new(bytes.Buffer) }}…
  17. 17

    Найдите баг: под нагрузкой некоторые ответы API содержат JSON другого запроса.

    var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }} func encode(v any) ([]byte, error) {	buf := bufPool.Get().(*bytes.Buffer)	buf.Reset()	defer bufPool.Put(buf) 	if err := json.NewEncoder(buf).Encode(v); err != nil {		return nil, err	}	return buf.Bytes(), nil}

    buf.Bytes() возвращает срез над собственной памятью буфера из пула, а отложенный Put отдаёт этот буфер следующему вызывающему, пока текущий ещё держит срез.

  18. 18

    Профиль кучи ровно держится на 300 МБ, но RSS контейнера стоит на 1,4 ГБ, и runtime.GC() его не двигает. Где память?

    RSS не совпадает с кучей. В него входят ещё стеки горутин, служебные структуры рантайма и свободные страницы кучи, которые аллокатор оставил себе, а не вернул ОС, тогда как профиль кучи показывает только выборку живых объектов на момент последней сборки.

    // metrics.Read does not stop the world; runtime.ReadMemStats does, briefly.var samples = []metrics.Sample{	{Name: "/memory/classes/total:bytes"},         // everything the runtime has mapped	{Name: "/memory/classes/heap/released:bytes"}, // returned to the OS: not in RSS	{Name: "/memory/classes/heap/objects:bytes"},  // live objects plus unswept garbage	{Name: "/memory/classes/heap/free:bytes"},     // free pages the process still holds…
  19. 19

    Как найти утечку памяти в Go-сервисе и когда в профиле кучи смотреть inuse_space, а когда alloc_space?

    Убедитесь, что живая куча растёт от цикла к циклу GC, затем сравните два профиля inuse_space, снятых с разницей в несколько минут: то, что выросло, и есть утечка.

    # live heap now, by allocation sitego tool pprof -inuse_space 'http://127.0.0.1:6060/debug/pprof/heap?gc=1' # two snapshots ten minutes apart, then show only what grewcurl -so before.pb.gz 'http://127.0.0.1:6060/debug/pprof/heap?gc=1'sleep 600…
  20. 20

    Под на 2 ГиБ с живой кучей около 700 МБ получает OOM-kill на всплесках трафика, а старый ранбук предлагает балласт на 1 ГиБ. Что настроить вместо него и что эта настройка считает?

    Поставьте GOMEMLIMIT чуть ниже лимита контейнера (около 1,8 ГиБ для пода на 2 ГиБ) и оставьте GOGC=100.

    // Read the cgroup v2 limit and keep headroom for what GOMEMLIMIT does not count.func setMemoryLimit(fraction float64) {	b, err := os.ReadFile("/sys/fs/cgroup/memory.max") // "max" when unlimited	if err != nil {		return	}…
  21. 21

    Под нагрузкой пропускная способность обваливается, а gctrace показывает циклы подряд и assist больше фоновой разметки. Что такое mark assist и как это исправить?

    Mark assist заставляет горутину, которая аллоцирует во время разметки, сделать долю разметки, прежде чем её аллокация вернётся.

    // Is the collector being paid for by request goroutines?// Counters are cumulative since start: read twice and compare the deltas.// "dedicated" is the dedicated background marking workers.func gcShares() (assist, background float64) {	s := []metrics.Sample{		{Name: "/cpu/classes/gc/mark/assist:cpu-seconds"},…
  22. 22

    Архитектор отвергает Go для 40-гигабайтного кэша в памяти: «GC обязан обойти 40 ГБ с остановленным миром». Что на самом деле останавливается и что действительно растёт с кучей?

    Никто не обходит кучу с остановленным миром: разметка конкурентная, а две паузы за цикл делают небольшую фиксированную работу, обычно заметно меньше миллисекунды.

    // wrong: ten million pointers the marker follows on every cycletype slowCache struct{ m map[string]*Entry } // right: no pointers inside, so the GC does not scan the data at alltype cache struct {	index map[uint64]extent // hashed key -> position; no pointers in key or value…
  23. 23

    Что изменилось в сборщике мусора в Go 1.26 с Green Tea и что команде проверить при обновлении?

    Go 1.26 сделал Green Tea алгоритмом разметки по умолчанию: он ставит в очередь целые спаны (страницы по 8 КБ) и сканирует размеченные объекты страницы вместе, а не по одному.

  24. 24

    p99 составляет 800 мс, а CPU-профиль почти ничего из этого не объясняет. Какие профили показывают, чего ждали запросы, и когда нужен go tool trace?

    CPU-профиль снимает только выполняющиеся горутины, поэтому ожидание в него не попадает никогда.

    var fr = trace.NewFlightRecorder(trace.FlightRecorderConfig{	MinAge:   10 * time.Second, // keep at least the last 10 s	MaxBytes: 64 << 20,}) func main() {…
  25. 25

    Чего стоит вызов cgo планировщику и почему C нельзя сохранять Go-указатель, тем более uintptr, после возврата из вызова?

    Вызов cgo держит свой поток ОС всё время выполнения и стоит десятки наносекунд против примерно одной для вызова Go.

    /*#include <stddef.h>static void *kept;static void register_buffer(void *p, size_t n) { kept = p; }static void unregister_buffer(void) { kept = NULL; }*/…