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 GC в Go — это конкурентный, неперемещающий (non-moving), tri-color mark-sweep сборщик с очень короткими STW-паузами (обычно sub-millisecond). Конкурентность корректна благодаря **hybrid write barrier** (Dijkstra insertion + Yuasa deletion), появившемуся в Go 1.8, который позволил убрать повторное STW-сканирование стеков. Темп сборки регулирует **GC pacer**, балансируя момент старта цикла так, чтобы heap не превысил цель `GOGC` (или мягкий лимит `GOMEMLIMIT` с 1.19); при отставании mark-фазы горутины-мутаторы платят **mark assist**. Аллокация идёт через многоуровневую иерархию `mcache → mcentral → mheap` с разбиением по size classes. ## Теория ### Зачем вообще конкурентный GC и почему non-moving Цель дизайна Go GC — не максимальный throughput, а **минимальная латентность пауз**. Это фундаментальный выбор: команда Go сознательно жертвует частью CPU-throughput ради того, чтобы паузы оставались в районе долей миллисекунды и не росли с размером кучи. GC в Go **non-moving** (неперемещающий, non-compacting). Объекты не двигаются по куче во время сборки. Причины: - **Совместимость с C через cgo**: указатель, переданный в C-код, не должен внезапно изменить значение. - **Простота interior pointers**: в Go легально держать указатель внутрь объекта (на поле структуры, на элемент массива). Перемещающий коллектор должен был бы корректно обновлять такие указатели. - **unsafe.Pointer и аппаратные предположения**: код полагается на стабильность адресов. Цена non-moving — **фрагментация кучи**. Go борется с ней через size classes (см. ниже): объекты группируются по классам размеров, что резко снижает внешнюю фрагментацию, оставляя ограниченную внутреннюю. ### Tri-color abstraction (трёхцветная маркировка) Алгоритм оперирует тремя логическими множествами объектов: | Цвет | Смысл | |------|-------| | **Белый** (white) | Кандидат на удаление. В начале цикла все объекты белые. В конце mark-фазы оставшиеся белые — мусор. | | **Серый** (grey) | Достижим (помечен живым), но его исходящие указатели ещё не просканированы. Лежит в work queue. | | **Чёрный** (black) | Достижим и полностью просканирован — все его дети уже серые или чёрные. | Процесс: корни (стеки горутин, глобальные переменные) делаются серыми. Дальше воркеры берут серый объект, сканируют его указатели (делая указуемых серыми), и красят сам объект в чёрный. Когда серых не осталось — всё чёрное живо, всё белое — мусор. В реальности «цвета» не хранятся в самих объектах как поле. Реализованы они так: - **Mark bits** — битовая карта в метаданных span/heapArena (чёрный vs белый). - **Серость** = присутствие в **work queue** (gcWork: локальные буферы per-P + глобальный пул). Объект серый ⇔ помечен И ещё в очереди на сканирование. ### Tri-color invariant: strong и weak Корректность конкурентного mark держится на инвариантах. Опасность одна: **мутатор может спрятать белый объект от сборщика**, перевесив единственный указатель на него под чёрный объект и удалив исходный путь. - **Strong tri-color invariant**: ни один чёрный объект не указывает на белый. Если поддерживать строго — белый недостижим из уже «закрытых» чёрных, и его не пропустят. - **Weak tri-color invariant**: чёрный может указывать на белый, но тогда у этого белого должен существовать **серый объект, ведущий к нему** (по цепочке белых). То есть белый ещё достижим через незавершённую работу. Write barrier нужен именно чтобы при записи указателя сохранить один из этих инвариантов. ### Write barrier: Dijkstra insertion + Yuasa deletion = hybrid (Go 1.8) Write barrier — это маленький фрагмент кода, вставляемый компилятором перед записью указателя в куче (`*slot = ptr`). **Dijkstra-style insertion barrier** поддерживает strong invariant. При записи `*slot = ptr` он красит **записываемый** объект `ptr` в серый (shade). Проблема: insertion barrier на стеках слишком дорог (стеки горячие, барьер на каждую запись локального указателя убил бы производительность). Поэтому в Go ≤1.7 стеки оставляли без барьера, а в конце требовалось **STW-перепроверка (re-scan) всех стеков**, что давало паузы, растущие с числом/размером горутин. **Yuasa-style deletion barrier** поддерживает weak invariant. При перезаписи указателя он красит **старое** значение слота (то, что вот-вот будет затёрто) — снимок «как было». Это сохраняет достижимость объектов, на которые ссылались на момент старта. **Hybrid write barrier (Go 1.8, Austin Clements):** ```go // Псевдокод того, что вставляет компилятор перед *slot = ptr writePointer(slot, ptr): shade(*slot) // Yuasa: затеняем СТАРОЕ значение if current_stack is grey: shade(ptr) // Dijkstra: затеняем НОВОЕ значение *slot = ptr ``` Ключевой выигрыш: hybrid barrier обеспечивает инвариант **«объект, на который указывает чёрный стек (уже просканированный), и все объекты, на которые он указывал на момент сканирования, считаются достижимыми»**. Это означает, что **стек, будучи просканированным один раз, становится чёрным навсегда — re-scan стеков с STW больше не нужен**. Именно это сократило паузы mark termination на порядок (с потенциально миллисекунд+ до <100 мкс). > Тонкость, которую любят на интервью: барьер срабатывает только на записи указателей **в кучу/в видимые сборщику места**. Записи в локальные переменные на стеке барьером не покрываются — корректность обеспечивается тем, что стек либо ещё не сканировался (серый, будет просканирован целиком), либо уже чёрный (и тогда новые указатели приходят из уже учтённых объектов или защищены Dijkstra-частью). ### Фазы GC-цикла ``` ... выполнение программы ... │ ├─[STW] Sweep termination — добиваем недосвипанные span'ы предыдущего цикла │ Включаем write barrier, переводим мир в режим маркировки │ ├─ Mark setup (часть под STW) — подготовка корней, включение барьера │ ├─ Concurrent Mark — параллельно с мутаторами: │ - dedicated mark workers (≈25% GOMAXPROCS) │ - mark assist на горутинах-аллокаторах │ - сканирование стеков, глобалов, обход графа объектов │ ├─[STW] Mark termination — финализация: drain остатков work queue, │ выключение write barrier, подсчёт, подготовка к sweep │ └─ Concurrent Sweep — освобождение белых span'ов лениво, параллельно с работой программы (по требованию аллокатора) ``` Две STW-паузы за цикл — **sweep termination** и **mark termination**. Обе спроектированы как очень короткие (десятки микросекунд в типичном случае). Между ними mark идёт **конкурентно**. Sweep также конкурентен и в значительной степени **ленив**: span чистится в момент, когда из него хотят аллоцировать. ```bash # Увидеть фазы и паузы в реальном времени: GODEBUG=gctrace=1 ./app # Пример строки: # gc 14 @2.123s 1%: 0.018+1.2+0.025 ms clock, 0.14+0.30/1.1/0+0.20 ms cpu, 4->4->2 MB, 5 MB goal, 8 P # ^STW ^conc ^STW ^heap_start->peak->live ^goal ``` Разбор `0.018+1.2+0.025 ms clock`: первая цифра — STW sweep termination, средняя — конкурентный mark, последняя — STW mark termination. ### GC Pacer — когда запускать цикл Pacer решает **в какой момент** стартовать конкурентный mark, чтобы успеть закончить до того, как куча перерастёт цель. Если стартовать слишком поздно — мутаторы наплодят аллокаций быстрее, чем GC помечает, и куча превысит goal. Слишком рано — лишний CPU и частые циклы. Цель кучи (heap goal) по умолчанию: ``` heap_goal = live_heap_after_last_mark * (1 + GOGC/100) ``` При `GOGC=100` (дефолт) цикл нацелен на то, чтобы новая куча выросла вдвое относительно живого объёма прошлого цикла. С 1.19 цель учитывает ещё и `GOMEMLIMIT`: ``` heap_goal = min( GOGC-based_goal, memory_limit_based_goal ) ``` Pacer — это контроллер с обратной связью (PI-controller в редизайне Go 1.18), который оценивает «scan work» и подбирает момент старта и долю assist так, чтобы mark финишировал примерно при достижении 95% пути к goal. ### Mutator assist (помощь от горутин) Если конкурентный mark отстаёт от темпа аллокаций, горутина, которая аллоцирует, обязана «отработать долг»: перед получением памяти она выполняет порцию mark-работы пропорционально объёму запрашиваемой аллокации. Это **mark assist**. ```go // Логика на уровне runtime (упрощённо): // при аллокации g накапливает assist debt; // если debt > 0 — горутина сама сканирует объекты, прежде чем продолжить. ``` Assist — это механизм обратного давления: чем агрессивнее программа аллоцирует во время GC, тем больше CPU у неё забирают на помощь сборщику, не давая куче «убежать». Высокий assist в gctrace — сигнал, что аллокаций слишком много / GOGC слишком низкий. ### Иерархия аллокатора: mcache → mcentral → mheap Аллокатор Go основан на идеях TCMalloc. | Уровень | Что это | Локальность | Блокировка | |---------|---------|-------------|------------| | **mcache** | Кэш свободных объектов на каждый **P** (логический процессор) | per-P | **без блокировок** (P принадлежит одной M в момент времени) | | **mcentral** | Центральный список span'ов одного size class на всю программу | глобально на класс | mutex на класс | | **mheap** | Глобальная куча, управляет всеми span'ами и виртуальной памятью | глобально | глобальный lock | Путь аллокации мелкого объекта: горутина → `mcache` своего P (lock-free) → если пусто, берём span из `mcentral` нужного класса → если и там пусто, `mheap` нарезает новый span (при необходимости запросив страницы у ОС через `mmap`). - **Tiny allocator** (объекты <16 байт без указателей, напр. маленькие строки): субаллоцируются в один 16-байтный блок, чтобы не плодить накладные расходы. - **Small** (≤32 KB): через size classes. - **Large** (>32 KB): аллоцируются напрямую из mheap отдельными span'ами. ### Span и size classes **Span (mspan)** — непрерывный участок страниц (кратный 8 KB), нарезанный на объекты **одного** size class. Это базовая единица управления памятью и метаданных GC (mark bits, alloc bits хранятся на уровне span). **Size classes** — фиксированный набор «округлений» размеров (~70 классов). Любой запрос округляется вверх до ближайшего класса. Это устраняет внешнюю фрагментацию ценой ограниченной внутренней (waste). ```bash # Полная таблица генерируется здесь: # $GOROOT/src/runtime/sizeclasses.go ``` ``` // фрагмент таблицы (class, размер объекта, размер span, объектов в span, max waste %) // class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 5 48 8192 170 32 31.52% // 32 512 8192 16 0 1.99% // 67 32768 32768 1 0 0.00% ``` Запрос на 33 байта попадёт в класс «48» → 15 байт внутренней фрагментации. Понимание этого критично для оптимизации: укладка структур в границы size class экономит память на больших слайсах объектов. ### Эволюция GC по версиям | Версия | Изменение | |--------|-----------| | **Go 1.3** | Полностью precise (точный) mark-sweep, но ещё STW. | | **Go 1.5** | **Конкурентный** mark-sweep. Цель «GC pause <10ms». Появился GC pacer. | | **Go 1.6** | Снижение пауз, лучшая работа с большими кучами. | | **Go 1.7** | Дальнейшая оптимизация mark termination. | | **Go 1.8** | **Hybrid write barrier** (Dijkstra + Yuasa). Убрали STW re-scan стеков → паузы стабильно <1 ms, часто <100 мкс. | | **Go 1.9–1.11** | Полировка, sweep/scan оптимизации. | | **Go 1.12** | Улучшение возврата памяти ОС, переработка mark termination, более ровные паузы. | | **Go 1.13** | `MADV_FREE` по умолчанию на Linux (позже частично откатили в 1.16 на `MADV_DONTNEED` из-за непонятных метрик RSS). | | **Go 1.14** | **Asynchronous preemption** (вытеснение по сигналам) — горутины в плотных циклах без вызовов функций теперь могут быть остановлены для STW; раньше такой цикл мог затянуть паузу. | | **Go 1.18** | Переработанный **PI-controller pacer** (Michael Knyszek). | | **Go 1.19** | **Soft memory limit `GOMEMLIMIT`** — мягкий лимит на total memory, GC учитывает его в цели кучи. | ### Asynchronous preemption (почему важно для пауз) До Go 1.14 вытеснение горутин было **кооперативным** — точка вытеснения вставлялась только при вызове функций (проверка stack preempt). Горутина с тугим циклом без вызовов (`for { i++ }`) не могла быть остановлена, и STW-фаза ждала её, раздувая паузу. С 1.14 рантайм шлёт горутине сигнал (`SIGURG` на Unix), обработчик сохраняет состояние и вытесняет в произвольной (безопасной для precise GC) точке. Это делает STW-паузы предсказуемыми независимо от поведения кода. ### Настройка: GOGC и GOMEMLIMIT (кратко) ```bash # GOGC: процент роста кучи между циклами. Дефолт 100. GOGC=200 ./app # реже GC, больше RAM, выше throughput GOGC=50 ./app # чаще GC, меньше RAM, больше CPU на сборку GOGC=off ./app # отключить GC (только для коротких batch-задач!) # GOMEMLIMIT (>=1.19): мягкий потолок суммарной памяти рантайма. GOMEMLIMIT=4GiB ./app ``` ```go import "runtime/debug" debug.SetGCPercent(50) // = GOGC debug.SetMemoryLimit(4 << 30) // = GOMEMLIMIT, 4 GiB ``` Подробный разбор GOMEMLIMIT/GOGC-стратегий — в отдельной заметке по тюнингу памяти; здесь важно: **GOMEMLIMIT — soft limit**, GC будет работать всё агрессивнее по мере приближения к нему вплоть до почти-непрерывного GC, но **не вызовет OOM-kill сам и не падает с ошибкой при превышении** (в отличие от hard limit). Анти-паттерн: ставить `GOMEMLIMIT` равным физической памяти контейнера без запаса — рискуете death-spiral, когда GC жрёт 100% CPU, пытаясь удержать лимит. ## Подводные камни / gotchas - **GOGC=off в долгоживущем сервисе** → неконтролируемый рост RSS и OOM. Допустимо только для коротких CLI/batch. - **`runtime.GC()` вручную** почти всегда ошибка: это блокирующий, полный, частично-STW цикл. Использовать лишь в бенчмарках/тестах. - **GC death spiral с GOMEMLIMIT**: лимит слишком близок к фактической рабочей памяти → GC работает непрерывно, CPU 100%, прогресса нет. Лечится поднятием лимита или снижением живой кучи. - **Высокий mark assist в gctrace** — не «GC медленный», а «программа аллоцирует быстрее, чем GC успевает»; смотреть на allocation rate и escape analysis, а не на сам GC. - **Финализаторы (`runtime.SetFinalizer`)** задерживают освобождение объекта минимум на один цикл (объект с финализатором не освобождается в том же цикле, в котором стал недостижим) и ломают порядок освобождения циклов. Не использовать для управления критичными ресурсами — только `defer`/`Close`. - **Память не сразу возвращается ОС**: RSS может оставаться высоким после падения живой кучи (`MADV_FREE`/scavenger). RSS != live heap. Мерить `runtime.ReadMemStats` (HeapInuse/HeapReleased), а не только `top`. - **Фрагментация из-за size classes**: массив структур размером «чуть больше класса» тратит память впустую. Профилируйте размер через `unsafe.Sizeof` и таблицу sizeclasses. - **Указатели удерживают целые span'ы**: один живой объект в большом span не даёт освободить span. Утечка через срез, удерживающий backing array большого слайса — классика (`s = bigSlice[:1]` держит весь backing). ## Вопросы на собеседовании **В:** Почему в Go GC неперемещающий, и какова цена этого решения? **О:** Non-moving выбран ради совместимости с cgo (указатели, переданные в C, должны быть стабильны), поддержки interior pointers и unsafe-кода, а также простоты реализации write barrier без необходимости обновлять все указатели при перемещении. Цена — фрагментация кучи, которую Go компенсирует размещением по size classes (резко снижает внешнюю фрагментацию). Также non-moving означает, что аллокатор не может делать дешёвый bump-pointer allocation на всю кучу, поэтому используется segregated-fit по классам через mcache/mcentral/mheap. **В:** Что такое hybrid write barrier и какую проблему он решил в Go 1.8? **О:** Это комбинация Dijkstra insertion barrier (затеняет новый записываемый указатель, поддерживая strong invariant) и Yuasa deletion barrier (затеняет старое, перезаписываемое значение, поддерживая weak invariant). Главная проблема, которую он решил: до 1.8 стеки не имели барьера (он слишком дорог для горячих стеков), поэтому в конце mark требовался STW-перескан всех стеков, и пауза росла с числом/размером горутин. Hybrid barrier обеспечивает инвариант, при котором просканированный стек можно считать «чёрным навсегда», устранив повторный STW-скан и сократив паузы mark termination на порядок. **В:** Объясни strong и weak tri-color invariants. **О:** Strong invariant: ни один чёрный объект не ссылается на белый — гарантирует, что белый (мусор-кандидат) недостижим из «закрытых» чёрных. Weak invariant ослабляет: чёрный может ссылаться на белый, но при условии, что к этому белому существует путь через серый объект (т.е. он всё ещё в очереди на обработку и не будет потерян). Write barrier поддерживает один из инвариантов, чтобы мутатор не смог «спрятать» живой белый объект, перевесив единственную ссылку на него под уже просканированный чёрный объект. **В:** Сколько STW-пауз в одном GC-цикле и где они? **О:** Две: sweep termination (в начале, добивает sweep предыдущего цикла и включает барьер/режим маркировки) и mark termination (в конце mark, drain остатков очереди, выключение барьера). Между ними mark идёт конкурентно, sweep после — конкурентно и лениво. Обе паузы спроектированы как очень короткие, обычно десятки микросекунд; их можно увидеть в `GODEBUG=gctrace=1` как первую и третью цифры в `X+Y+Z ms clock`. **В:** Что делает GC pacer и что такое mark assist? **О:** Pacer решает, в какой момент стартовать конкурентный mark, чтобы успеть завершить его до того, как куча достигнет heap goal (по умолчанию `live*(1+GOGC/100)`, с 1.19 — min с лимитом по GOMEMLIMIT). Это контроллер с обратной связью (PI-controller с 1.18). Mark assist — механизм обратного давления: если mark отстаёт от темпа аллокаций, горутина, которая аллоцирует, обязана сама выполнить порцию mark-работы пропорционально размеру аллокации, прежде чем получить память. Это не даёт куче «убежать» за goal под нагрузкой. **В:** Как объекты размещаются в памяти — расскажи про mcache/mcentral/mheap и size classes. **О:** Трёхуровневая иерархия в стиле TCMalloc. mcache — per-P кэш свободных объектов, аллокация из него идёт без блокировок (P в момент времени принадлежит одной M). Если в mcache пусто — берётся span из mcentral (центральный список span'ов нужного size class, под мьютексом класса). Если пусто и там — mheap нарезает новый span, при необходимости запросив страницы у ОС. Size classes — ~70 фиксированных размеров, любой запрос округляется вверх до ближайшего; это устраняет внешнюю фрагментацию ценой ограниченной внутренней. Span — непрерывный набор страниц, нарезанный на объекты одного класса, и единица хранения mark/alloc bits. Объекты >32 KB идут напрямую из mheap. **В:** Почему high allocation rate важнее для производительности, чем «скорость GC»? **О:** GC-нагрузка пропорциональна количеству и размеру живых указуемых объектов и темпу аллокаций. Большинство «GC-проблем» на самом деле — проблемы аллокаций: лишние escape на кучу, отсутствие переиспользования (sync.Pool), копирование. Высокий mark assist и частые циклы — следствие, а не причина. Поэтому senior сначала смотрит на escape analysis (`go build -gcflags=-m`), pprof alloc-профиль и снижает аллокации, и только потом крутит GOGC/GOMEMLIMIT. **В:** Что изменила asynchronous preemption в 1.14 и при чём тут GC? **О:** До 1.14 вытеснение было кооперативным — только в точках вызова функций. Горутина с тугим циклом без вызовов не могла быть остановлена, и STW-фаза GC (или планировщик) ждала её, раздувая паузу до неопределённого времени. С 1.14 рантайм посылает сигнал (SIGURG на Unix), и горутина вытесняется в безопасной для precise GC точке. Это сделало STW-паузы предсказуемыми независимо от структуры пользовательского кода. **В:** Почему RSS процесса не падает сразу после того, как живая куча уменьшилась? **О:** Освобождённая память не возвращается ОС немедленно. Sweep делает span доступным для повторного использования внутри процесса, а фактический возврат страниц делает фоновый scavenger через `madvise` (`MADV_FREE` или `MADV_DONTNEED`). При `MADV_FREE` страницы помечаются как освобождаемые, но физически остаются за процессом, пока ОС не понадобится память — поэтому RSS в `top` может казаться завышенным. Правильная метрика живой памяти — `runtime.ReadMemStats` (HeapAlloc/HeapInuse/HeapReleased), а не RSS. ## На что копают на senior+ - **Точная механика hybrid barrier**: какой именно из двух «shade» поддерживает какой инвариант, и почему именно комбинация позволяет НЕ перескан стеков. Умение нарисовать сценарий потери объекта без барьера (мутатор перевешивает указатель с белого пути на чёрный объект) и показать, как каждая часть барьера его ловит. - **Pacer как контроллер**: понимание, что это feedback loop, оценивающий scan work, и что переход на PI-controller в 1.18 решал колебания/overshoot старого proportional-пейсера. - **Связь GOMEMLIMIT и death spiral**: умение объяснить, когда мягкий лимит приводит к 100% CPU на GC, и как это диагностировать/чинить (вместе с `GOGC=off`+GOMEMLIMIT как осознанный паттерн «лимит-главный» в контейнерах). - **Где живут «цвета»**: что серость — это присутствие в work queue (gcWork, per-P буферы + глобальный пул), а чёрный/белый — mark bits в метаданных span; цвет не хранится в объекте. - **Стоимость барьера для компилятора**: write barrier вставляется только на запись указателей и только когда барьер «включён» (флаг в рантайме), записи не-указателей и записи во время выключенного барьера идут напрямую; знание про `runtime.gcWriteBarrier` и почему запись указателя дороже записи int. - **Финализаторы, weak pointers (1.24+) и порядок освобождения**: почему финализаторы задерживают сбор на цикл и не дружат с циклическими ссылками. - **Чтение gctrace вслух**: разбор каждого поля строки `gc N @t Ns P%: a+b+c ms clock, ... X->Y->Z MB, G MB goal, P P` и постановка диагноза по числам. - **Tiny allocator и size class waste**: как уложить структуру под границу класса, чтобы сэкономить память на больших коллекциях, со ссылкой на `runtime/sizeclasses.go`.