Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Runtime и память · Уровень: Senior+ ## TL;DR `GOGC` (по умолчанию 100) управляет частотой сборки мусора через целевой размер кучи: `heap_target = live_heap * (1 + GOGC/100)` — то есть при 100 GC запускается, когда куча удваивается относительно живых данных после предыдущей сборки. `GOMEMLIMIT` (Go 1.19+) задаёт мягкий лимит на общую память рантайма и заставляет GC работать чаще по мере приближения к нему, страхуя от OOM. Канонический паттерн для контейнеров — `GOGC=off` (или высокий) + `GOMEMLIMIT`, установленный на ~90-95% от лимита пода, что даёт минимум GC при нормальной нагрузке и автоматическое уплотнение при пиках. Главная опасность — «death spiral», когда лимит занижен и GC начинает крутиться непрерывно, выжигая CPU. ## Теория Go использует конкурентный mark-and-sweep сборщик с tricolor-маркировкой и write barrier. Сборщик работает почти полностью параллельно с приложением (concurrent), останавливая мир (STW) лишь на короткие фазы (включение/выключение write barrier, обычно суммарно < 1 мс на современных версиях). Ключевой вопрос тюнинга — **не как** работает GC, а **когда** он запускается и сколько памяти позволяет накопить между циклами. Это определяет фундаментальный trade-off: **CPU против RAM**. ### GOGC и расчёт целевой кучи `GOGC` — это процент роста кучи, который допускается перед запуском следующей сборки, относительно объёма живых данных после предыдущей. ``` heap_target = live_heap + live_heap * (GOGC / 100) = live_heap * (1 + GOGC/100) ``` | GOGC | Множитель target | Поведение | |---|---|---| | 50 | 1.5× live | GC чаще, меньше RAM, больше CPU | | 100 (default) | 2× live | Баланс: куча удваивается между сборками | | 200 | 3× live | GC реже, больше RAM, меньше CPU | | 400 | 5× live | Заметная экономия CPU, большой расход RAM | | off | ∞ | GC по росту кучи отключён полностью | Пример: если после сборки живых данных (reachable) 100 МБ, то при `GOGC=100` следующая сборка стартует при достижении кучей ~200 МБ. При `GOGC=200` — при ~300 МБ. ```bash # Установка через переменную окружения export GOGC=200 # Полное отключение GC по росту кучи (опасно без GOMEMLIMIT!) export GOGC=off ``` ```go import "runtime/debug" // Программно. Возвращает предыдущее значение. old := debug.SetGCPercent(200) // Эквивалент GOGC=off debug.SetGCPercent(-1) ``` #### Под капотом: pacer За решение «когда запускать GC» отвечает **GC pacer**. Он не ждёт буквального достижения `heap_target` — он запускает сборку чуть раньше, оценивая скорость аллокаций, чтобы конкурентная маркировка успела завершиться **до** того, как куча реально дорастёт до цели. Если маркировка не успевает (аллокации идут быстрее, чем mark), pacer включает **mark assist**: горутины, которые аллоцируют, вынуждаются помогать маркировке пропорционально объёму аллокаций. Mark assist — это и есть тот «налог на CPU», который вы платите при агрессивном GC; в pprof CPU-профиле он виден как `runtime.gcAssistAlloc`. ### GOMEMLIMIT (Go 1.19+) `GOMEMLIMIT` задаёт **мягкий (soft) лимит** на общий объём памяти, которым управляет рантайм Go. Это не та память, что в `heap_target` от GOGC — это **общая** память рантайма. ```bash export GOMEMLIMIT=4GiB # суффиксы: B, KiB, MiB, GiB (степени 2) export GOMEMLIMIT=4000000 # просто байты export GOMEMLIMIT=off # снять лимит (значение по умолчанию) ``` ```go import "runtime/debug" // 4 ГиБ. Возвращает предыдущий лимит. debug.SetMemoryLimit(4 << 30) // Снять лимит debug.SetMemoryLimit(math.MaxInt64) ``` #### Что входит в лимит: total memory vs heap GOMEMLIMIT ограничивает **не только кучу живых объектов**, а почти всю память, учтённую рантаймом: | Компонент | Входит в GOMEMLIMIT? | |---|---| | Heap (живые + ещё не собранные объекты) | Да | | Stacks горутин | Да | | Метаданные GC (`GCSys`), span/heap-метаданные | Да | | Внутренние структуры рантайма (`OtherSys`, `MSpanSys`...) | Да | | `os/exec`, mmap'ы вне рантайма, C-память (cgo) | **Нет** | Соответствие полям `runtime.MemStats`: лимит сравнивается примерно с `Sys - HeapReleased` (то есть с retained-памятью процесса с точки зрения рантайма). Поэтому при установке GOMEMLIMIT нужно оставлять запас на то, что рантайм **не** контролирует (cgo, буферы ядра, page cache). #### Как GOMEMLIMIT взаимодействует с GOGC Ментальная модель: GOGC задаёт **обычный** темп GC, а GOMEMLIMIT — **потолок**. GC запускается по тому из двух триггеров, который наступит раньше: ``` trigger = min( heap_target_from_GOGC, // live * (1 + GOGC/100) target_from_GOMEMLIMIT // граница, выводимая из лимита ) ``` - Пока куча мала относительно лимита — работает GOGC (нормальный, нечастый GC). - По мере приближения к GOMEMLIMIT pacer начинает запускать GC всё чаще, эффективно понижая «виртуальный GOGC», чтобы не пробить лимит. - При `GOGC=off` + `GOMEMLIMIT` GC вообще не запускается по росту кучи — только по приближению к лимиту. Это и есть рекомендованный паттерн: «не трогай GC, пока память не станет дефицитом». ``` Память │ ┌──────── GOMEMLIMIT (потолок) │ │ GC учащается, "виртуальный GOGC" падает │ ╱╲ ╱╲ ╱│ │ ╱╲ ╱╲ ╱ ╲ ╱ ╲ ╱ │ ← здесь рулит GOMEMLIMIT │ ╱ ╲╱ ╲ ╲╱ │ ╱ ← здесь рулит GOGC (пилообразный рост до 2× live) └────────────────────────────────── время ``` #### Death spiral: почему GOMEMLIMIT — soft limit GOMEMLIMIT намеренно сделан мягким: рантайм **не упадёт** и **не откажет в аллокации** при достижении лимита (в отличие от жёсткого лимита, который вызвал бы OOM/ошибку). Вместо этого он будет всё агрессивнее запускать GC. Опасность: если живых данных (working set) уже почти столько же, сколько лимит, GC не сможет освободить достаточно памяти — и попадёт в **death spiral (GC thrashing)**: сборщик запускается практически непрерывно, приложение бóльшую часть CPU тратит на mark assist, throughput падает почти до нуля, но память так и держится у лимита. Чтобы этого избежать, рантайм вводит **минимальный лимит на долю CPU для GC**: GC не позволяется потреблять более ~50% CPU (усреднённо в скользящем окне). Если для удержания лимита нужно больше 50% CPU — рантайм **сознательно превышает GOMEMLIMIT**, отдавая приоритет прогрессу приложения. Итог: при заниженном лимите вы получите либо death spiral до этого потолка, либо превышение лимита и затем настоящий OOM от cgroup/ядра. Поэтому **GOMEMLIMIT не заменяет правильный sizing**: working set должен с запасом помещаться под лимит. ### Trade-off: CPU против памяти Это единственный фундаментальный компромисс тюнинга GC: | Стратегия | CPU на GC | Использование RAM | Когда | |---|---|---|---| | Низкий GOGC (50) | Высокий | Низкое | RAM дефицитна, CPU в избытке | | Дефолт (GOGC=100) | Средний | Среднее | Общий случай | | Высокий GOGC (200-400) | Низкий | Высокое | CPU дефицитен, RAM в избытке | | GOGC=off + GOMEMLIMIT | Минимальный при норме, растёт у лимита | Близко к лимиту | Контейнеры с фиксированным лимитом памяти | Реже GC → больше «мусора» накапливается между сборками → больше пиковая RAM, но меньше суммарно потрачено CPU на маркировку. Чаще GC → меньше RAM, но больше CPU. GOMEMLIMIT ломает дилемму «либо-либо»: позволяет держать GOGC высоким для экономии CPU при нормальной нагрузке, но автоматически переходить в экономию RAM у потолка. ### Ballast trick (исторический, до GOMEMLIMIT) До Go 1.19 не было способа сказать «используй до N памяти». Трюк с балластом эмулировал это, искусственно завышая `live_heap`: ```go // ИСТОРИЧЕСКИЙ ХАК, НЕ ИСПОЛЬЗОВАТЬ В Go 1.19+ func main() { // 10 ГБ "балласта". Виртуальная память выделяется, но физическая — // нет (страницы нулевые, lazy), пока к ним не обращаются. ballast := make([]byte, 10<<30) runtime.KeepAlive(ballast) // не дать оптимизатору выкинуть // Теперь live_heap всегда >= 10 ГБ, поэтому при GOGC=100 // GC сработает только при ~20 ГБ кучи → редкие сборки. _ = ballast } ``` Как это работало: `make([]byte, N)` создаёт огромный срез, который всегда жив. GC считает его частью live heap, поэтому `heap_target = (live + ballast) * 2` оказывается большим, и сборки происходят редко. Физическая память почти не расходовалась благодаря демандной подкачке (overcommit + zero pages). Famous: статья Twitch об экономии 30% CPU и 9× меньше GC через балласт. **Сегодня балласт устарел и вреден**: GOMEMLIMIT решает ту же задачу честно, не вводя GC в заблуждение, не занимая виртуальное адресное пространство, корректно работая с pacer и не ломая учёт памяти. Если видите ballast в legacy-коде — это кандидат на замену `GOMEMLIMIT`. ## Подводные камни / gotchas - **GOMEMLIMIT без знания working set = death spiral или OOM**. Лимит должен быть выше пикового живого объёма с запасом, иначе GC начнёт крутиться непрерывно (или, упёршись в 50% CPU-cap, всё равно пробьёт лимит → OOM от cgroup). - **GOGC=off без GOMEMLIMIT = гарантированный OOM** на любой растущей нагрузке: GC по росту кучи отключён, ничто не остановит рост. Эту пару нельзя разрывать. - **GOMEMLIMIT не покрывает cgo/mmap/ядерные буферы**. Если приложение активно использует cgo или большие mmap-файлы, оставляйте под это отдельный запас сверх лимита. - **GOMAXPROCS должен соответствовать CPU-лимиту cgroup**. По умолчанию Go (до 1.25) видит все ядра хоста, а не лимит пода → неверный sizing GC и throttling. Используйте `automaxprocs` от Uber (Go 1.25 учитывает cgroup CPU-квоту автоматически). Это напрямую влияет на CPU-cap GC и mark assist. - **GiB vs GB**: суффиксы GOMEMLIMIT — степени двойки (`GiB` = 2^30). `GOMEMLIMIT=1GB` — это ошибка парсинга (нет суффикса `GB`); используйте `GiB`/`MiB` или голые байты. - **Соотношение лимита пода и GOMEMLIMIT**: ставьте GOMEMLIMIT на ~90-95% от `resources.limits.memory`, чтобы оставить запас на неучтённую память и избежать срабатывания OOM-killer раньше, чем GC успеет среагировать (он мягкий и реагирует не мгновенно). - **Высокий GOGC ≠ всегда хорошо**: при бурстовых аллокациях пик RAM может оказаться неприемлемо высоким даже кратковременно — а OOM-killer не прощает даже кратких пиков. - **`debug.SetGCPercent(-1)` ≠ `SetMemoryLimit`**: отключение процента не отключает GC по лимиту памяти — они независимы. - **Балласт ломает метрики**: `MemStats.HeapAlloc` будет завышен на размер балласта, искажая мониторинг. ## Вопросы на собеседовании **В:** Как именно GOGC определяет момент запуска GC? **О:** GOGC задаёт допустимый процент роста кучи относительно живых данных после предыдущей сборки: целевой размер кучи `heap_target = live_heap * (1 + GOGC/100)`. При дефолтном GOGC=100 это значит, что GC сработает примерно когда куча удвоится относительно живого объёма. Но фактически запуск происходит чуть раньше: за это отвечает GC pacer, который по скорости аллокаций оценивает, когда нужно стартовать конкурентную маркировку, чтобы она успела завершиться до достижения target. Если не успевает — включается mark assist, заставляющий аллоцирующие горутины помогать маркировке. **В:** Чем GOMEMLIMIT отличается от GOGC и как они работают вместе? **О:** GOGC задаёт относительный, пилообразный темп GC (доля роста кучи), а GOMEMLIMIT — абсолютный потолок на общую память рантайма. GC запускается по тому из двух триггеров, что наступает раньше: `min(heap_target от GOGC, граница от GOMEMLIMIT)`. Пока память далеко от лимита, рулит GOGC и сборки редкие; по мере приближения к лимиту pacer учащает GC, эффективно понижая «виртуальный GOGC». Канонический паттерн — `GOGC=off` + `GOMEMLIMIT`: GC не дёргается по росту кучи вообще и срабатывает только когда память становится дефицитом, что минимизирует CPU при нормальной нагрузке. **В:** Что такое GC death spiral и как рантайм от него защищается? **О:** Death spiral (GC thrashing) — ситуация, когда working set почти равен GOMEMLIMIT: GC запускается всё чаще, пытаясь удержать лимит, но освобождать почти нечего, поэтому он крутится практически непрерывно, а приложение бóльшую часть CPU тратит на mark assist, при этом throughput падает почти до нуля. Защита: рантайм ограничивает долю CPU на GC примерно 50% в скользящем окне. Если для удержания лимита требуется больше — рантайм сознательно превышает GOMEMLIMIT, отдавая приоритет прогрессу приложения. Это делает GOMEMLIMIT мягким: вместо зависания вы получите превышение лимита (и, возможно, OOM от cgroup). Вывод: GOMEMLIMIT не заменяет корректный sizing, working set должен с запасом помещаться под лимит. **В:** Почему GOMEMLIMIT называют «мягким» лимитом и почему он не гарантирует от OOM? **О:** Мягкий — потому что при достижении лимита рантайм не падает и не отказывает в аллокации, а лишь агрессивнее запускает GC. Это лучше жёсткого лимита для пиковых нагрузок: кратковременный всплеск можно пережить за счёт временного учащения GC, а не падения. Но он не гарантирует от OOM по двум причинам: (1) лимит покрывает только память, учтённую рантаймом Go — cgo, mmap, ядерные буферы остаются снаружи; (2) при CPU-cap в 50% рантайм предпочтёт превысить лимит, лишь бы не уморить приложение. Поэтому GOMEMLIMIT ставят с запасом ниже жёсткого лимита cgroup (~90-95%) и оставляют место под неучтённую память. **В:** Какую память покрывает GOMEMLIMIT — только heap? **О:** Нет, почти всю память, учтённую рантаймом: кучу (живые + ещё не собранные объекты), стеки горутин, метаданные GC и span/heap-метаданные, внутренние структуры рантайма. В терминах MemStats это примерно `Sys - HeapReleased`. Не покрывает то, что вне рантайма Go: память, выделенную через cgo, mmap-файлы, буферы ядра, page cache. Поэтому при использовании cgo нужен дополнительный запас сверх лимита. **В:** Что такое ballast trick и почему он устарел? **О:** До Go 1.19 не было способа задать целевой объём памяти. Балласт — это большой неиспользуемый срез (`make([]byte, 10<<30)`), который держат живым через `runtime.KeepAlive`. GC считает его частью live heap, поэтому `heap_target` оказывается большим и сборки происходят редко — экономия CPU. Физическая память почти не тратится благодаря демандной подкачке нулевых страниц. С Go 1.19 балласт устарел и вреден: GOMEMLIMIT решает ту же задачу честно — не обманывает pacer, не занимает виртуальное адресное пространство, корректно учитывает реальную память и не искажает метрики (`HeapAlloc` от балласта раздувается). Балласт в коде — кандидат на замену GOMEMLIMIT. **В:** Опишите CPU/RAM trade-off в тюнинге GC. **О:** Это базовый компромисс: чем реже GC, тем больше мусора копится между сборками — выше пиковая RAM, но меньше суммарного CPU на маркировку; чем чаще GC, тем ниже RAM, но выше CPU (и больше mark assist, тормозящего аллоцирующие горутины). GOGC прямо двигает эту точку: GOGC=50 — экономим RAM ценой CPU, GOGC=400 — наоборот. GOMEMLIMIT снимает дилемму «либо-либо»: можно держать высокий GOGC (экономия CPU при норме) и при этом не бояться пика, потому что у потолка GC автоматически учащается, переходя в режим экономии RAM. **В:** Как настроить GC для Go-сервиса в Kubernetes-поде с лимитом 4Gi памяти и 2 CPU? **О:** Ставлю `GOMEMLIMIT` примерно на 90-95% от лимита пода — около `3600MiB` (запас на неучтённую рантаймом память и на то, что GC реагирует не мгновенно). `GOGC` либо оставляю дефолтным, либо ставлю `off`, если хочу минимум GC при норме и полностью полагаюсь на GOMEMLIMIT как на единственный триггер. Обязательно выставляю GOMAXPROCS по CPU-лимиту (через `automaxprocs` или Go 1.25, который читает cgroup-квоту), иначе GC будет неправильно дозировать CPU и под получит throttling. Перед продом проверяю под нагрузкой, что working set с запасом ниже GOMEMLIMIT, чтобы не словить death spiral, и мониторю `go_memstats_*` + GC CPU fraction. ## На что копают на senior+ - **Точная формула `heap_target = live*(1+GOGC/100)`** и понимание, что pacer стартует раньше target, а не на нём. - **Mark assist**: умение объяснить, что «дорогой GC» в CPU-профиле — это `gcAssistAlloc`, и почему он появляется при отставании маркировки. - **Взаимодействие GOGC и GOMEMLIMIT** как `min(...)` двух триггеров; понимание паттерна `GOGC=off + GOMEMLIMIT` и почему именно так. - **Death spiral и 50% CPU-cap**: знание, что лимит мягкий и рантайм осознанно его превысит, отдав приоритет приложению. - **Граница покрытия GOMEMLIMIT**: что туда входит (heap, stacks, GC-метаданные) и что нет (cgo, mmap, ядро). - **GOMAXPROCS vs cgroup**: связь sizing CPU с поведением GC и throttling в k8s; automaxprocs / Go 1.25. - **История ballast**: способны ли объяснить механику обмана pacer и почему GOMEMLIMIT сделал его устаревшим. - **Практический sizing**: умение назвать конкретные значения для контейнера (90-95% от лимита) и обосновать запас. - **Метрики и валидация**: какие поля MemStats и какие Prometheus-метрики смотреть, чтобы подтвердить, что тюнинг работает, а не вызвал thrashing.