06 июл. 2025 г.·7 мин

Профилирование памяти Go при пиках трафика: walkthrough по pprof

Профилирование памяти Go помогает справляться с внезапными пиками трафика. Практическое руководство по pprof для поиска горячих точек аллокаций в JSON, сканировании БД и middleware.

Профилирование памяти Go при пиках трафика: walkthrough по pprof

Что делают внезапные пики трафика с памятью Go-сервиса

Под «скачком памяти» в продакшне редко понимают одно простое число. Вы можете увидеть, что RSS (память процесса) быстро растёт, в то время как Go-heap почти не двигается, или heap увеличивается и падает волнами по мере работы GC. Одновременно зачастую растёт латентность, потому что рантайм тратит больше времени на уборку.

Типичные паттерны в метриках:

  • RSS растёт быстрее, чем вы ожидали, и иногда не полностью опадает после пика
  • Heap in-use поднимается, затем падает резкими циклами по мере более частых запусков GC
  • Скорость аллокаций прыгает (байт в секунду)
  • Время пауз GC и загрузка CPU под GC увеличиваются, даже если каждая пауза небольшая
  • Латентность запросов растёт, а хвостовая латентность становится шумной

Пики трафика увеличивают пер-запрос аллокации, потому что «малые» потери масштабируются линейно с нагрузкой. Если один запрос выделяет дополнительно 50 KB (временные JSON-буферы, объекты для сканирования строк, данные контекста middleware), то при 2000 RPS вы подаёте аллокатору примерно 100 MB в секунду. Go справляется с большим объёмом, но GC всё равно должен пробежать по этим короткоживущим объектам и освободить их. Когда аллокации опережают уборку, целевой размер heap растёт, RSS следует за ним, и вы можете упереться в лимиты по памяти.

Симптомы знакомы: OOM-убийства от оркестратора, резкие скачки латентности, больше времени в GC и сервис, который выглядит «занятым», даже если CPU не загружен полностью. Может начаться трешинг GC: сервис остаётся жив, но постоянно аллоцирует и собирает сборщик, и пропускная способность падает в самый нужный момент.

pprof помогает быстро ответить на один вопрос: какие пути кода аллоцируют больше всего, и нужны ли эти аллокации? Heap-профиль показывает, что удерживается прямо сейчас. Виды, ориентированные на аллокации (например, alloc_space), показывают, что создаётся и тут же выбрасывается.

pprof не даст объяснение каждому байту RSS. RSS включает больше, чем Go-heap (стэки, метаданные рантайма, OS-mappings, cgo-аллокации, фрагментация). pprof лучше всего указывает на горячие точки аллокаций в вашем Go-коде, а не доказывает точный контейнерный суммарный объём памяти.

Настройка pprof безопасно (шаг за шагом)

pprof проще всего использовать через HTTP-эндпоинты, но эти эндпоинты раскрывают много про сервис. Относитесь к ним как к админ-фиче, а не к публичному API.

1) Добавьте эндпоинты pprof

В Go самая простая настройка — запускать pprof на отдельном admin-сервере. Так маршруты профилирования не попадают в основной роутер и middleware.

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	go func() {
		// Admin only: bind to localhost
		log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
	}()

	// Your main server starts here...
	// http.ListenAndServe(":8080", appHandler)
	select {}
}

Если вы не можете открыть второй порт, можно монтировать маршруты pprof в основной сервер, но их проще случайно открыть. Отдельный админ-порт — более безопасный вариант по умолчанию.

2) Защитите доступ перед деплоем

Начните с контролей, которые сложно испортить. Привязка к localhost означает, что эндпоинты недоступны из интернета, если кто-то дополнительно не выставит этот порт.

Короткий чеклист:

  • Запускайте pprof на админ-порту, а не на основном пользовательском
  • Привязывайтесь к 127.0.0.1 (или приватному интерфейсу) в продакшне
  • Добавьте allowlist на сетевом крае (VPN, bastion или внутренний подсеть)
  • Требуйте аутентификацию, если край может её обеспечить (basic auth или токен)
  • Убедитесь, что вы можете получить профили, которые планируете использовать: heap, allocs, goroutine

3) Соберите и откатайте безопасно

Сделайте изменение маленьким: добавьте pprof, выпустите и подтвердите, что он доступен только оттуда, откуда вы ожидаете. Если есть staging — протестируйте сначала, симулируя нагрузку и сняв heap и allocs профили.

Для продакшна раскатывайте постепенно (один инстанс или небольшой срез трафика). Если pprof настроен неверно, радиус поражения останется маленьким, пока вы исправляете проблему.

Снимайте правильные профили во время пика

Во время пика один снимок редко достаточен. Захватите небольшую временную шкалу: несколько минут до пика (baseline), во время пика (impact) и несколько минут после (recovery). Так проще отделить реальные изменения аллокаций от обычного прогрева.

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

Берите и heap-профиль, и профиль, ориентированный на аллокации. Они отвечают на разные вопросы:

  • Heap (inuse) показывает, что живо и удерживает память в момент съёмки
  • Аллокации (alloc_space или alloc_objects) показывают, что активно создаётся, даже если быстро освобождается

Практический шаблон захвата: снимите один heap-профиль, затем allocs-профиль, затем повторите через 30—60 секунд. Две точки во время пика помогут понять, стабильна ли подозрительная ветка или ускоряется.

# examples: adjust host/port and timing to your setup
curl -o heap_during.pprof "http://127.0.0.1:6060/debug/pprof/heap"
curl -o allocs_30s.pprof "http://127.0.0.1:6060/debug/pprof/allocs?seconds=30"

Параллельно с pprof-файлами записывайте несколько runtime-метрик, чтобы объяснить, что делал GC в тот же момент. Достаточно размера heap, количества GCs и времени пауз. Даже короткая строка лога на момент съёмки помогает связать «аллокации выросли» с «GC начал работать постоянно».

Ведите заметки инцидента: версия сборки (commit/tag), версия Go, важные флаги, изменения конфигурации и какой трафик был (эндпоинты, арендаторы, размеры payload). Эти детали часто важны при сравнении профилей и понимании смещения микса запросов.

Как читать heap и alloc-профили

Heap-профиль отвечает на разные вопросы в зависимости от представления.

Inuse space показывает, что удерживается в памяти в момент съёмки. Используйте его для поиска утечек, долгоживущих кэшей или запросов, которые оставляют объекты.

Alloc space (итоговые аллокации) показывает, что выделялось за период, даже если затем освобождалось быстро. Используйте его, когда пики вызывают высокую работу GC, скачки латентности или OOM из-за churn.

Выборка важна. Go не записывает каждую аллокацию; он сэмплирует аллокации (контролируется через runtime.MemProfileRate), поэтому маленькие частые аллокации могут быть недопредставлены и числа — оценки. Самые крупные нарушители всё равно выделяются, особенно при пиках. Смотрите на тренды и главных вкладчиков, а не на идеальную точность учёта.

Наиболее полезные представления pprof:

  • top: быстрый просмотр, кто доминирует в inuse или alloc (смотрите flat и cumulative)
  • list : источники аллокаций на уровне строк внутри горячей функции
  • graph: пути вызовов, объясняющие, как вы туда попали

Деффы — где это становится практичным. Сравните baseline-профиль (нормальный трафик) со spike-профилем, чтобы выделить, что изменилось, вместо того чтобы гоняться за фоновым шумом.

Проверяйте находки небольшой правкой перед масштабным рефакторингом:

  • Переиспользуйте буфер (или добавьте небольшой sync.Pool) в горячем пути
  • Сократите создание пер-запрос объектов (например, избегайте промежуточных map при JSON)
  • Перепрофилируйте под той же нагрузкой и убедитесь, что diff уменьшился там, где ожидали

Если числа двигаются в нужную сторону, вы нашли реальную причину, а не просто страшный отчёт.

Поиск горячих точек аллокаций в JSON-энкодинге

Экспортируйте и изучайте всё
Экспортируйте исходники бэкенда и запускайте ваши привычные инструменты, включая pprof и нагрузочные тесты.
Экспортировать исходники

Во время пиков JSON-работа может стать серьёзной статьёй расхода памяти, потому что она выполняется на каждом запросе. Горячие точки JSON часто проявляются как множество мелких аллокаций, которые сильнее нагружают GC.

Тревожные признаки в pprof

Если heap или allocation view указывают на encoding/json, внимательно посмотрите, что вы туда передаёте. Эти паттерны обычно увеличивают аллокации:

  • Использование map[string]any (или []any) для ответов вместо типизированных структур
  • Многократный маршалинг одного и того же объекта (например, логирование и отправка в ответ)
  • Pretty printing с json.MarshalIndent в продакшене
  • Построение JSON через временные строки (fmt.Sprintf, конкатенация) перед маршалингом
  • Конвертация больших []byte в string (или обратно) только ради соответствия API

json.Marshal всегда выделяет новый []byte под весь вывод. json.NewEncoder(w).Encode(v) обычно избегает одного большого буфера, потому что пишет в io.Writer, но всё равно может аллоцировать внутренние структуры, особенно если v заполнен any, map-ами или структурами с большим количеством указателей.

Быстрые фиксы и эксперименты

Начните с типизированных структур для формы ответа. Они уменьшают работу с reflection и избегают упаковки полей в interface{}.

Затем уберите ненужные пер-запрос временные объекты: переиспользуйте bytes.Buffer через sync.Pool (с осторожностью), не форматируйте отступы в продакшене и не маршальте заново только ради логов.

Небольшие эксперименты, подтверждающие, что виноват JSON:

  • Замените map[string]any на struct для одного горячего эндпойнта и сравните профили
  • Поменяйте json.Marshal на Encoder, который пишет прямо в ответ
  • Уберите MarshalIndent или форматирование для дебага и перепрофилируйте под той же нагрузкой
  • Пропустите JSON-кодирование для неизменных кэшированных ответов и измерьте снижение

Поиск горячих точек аллокаций при сканировании запросов к БД

Когда память прыгает при пике, чтение из БД — частый неожиданный виновник. Легко сосредоточиться на времени SQL, но шаг сканирования может аллоцировать много на каждую строку, особенно если вы сканируете в гибкие типы.

Частые нарушители:

  • Сканирование в interface{} (или map[string]any) и позволение драйверу выбирать типы
  • Конвертация []byte в string для каждого поля
  • Использование nullable-обёрток (sql.NullString, sql.NullInt64) в больших результатах
  • Вытягивание больших текстовых/blob колонок, которые не всегда нужны

Один паттерн, который тихо сжигает память — сканирование данных строки в временные переменные, а затем копирование в настоящую структуру (или построение map на строку). Если вы можете сканировать прямо в структуру с конкретными полями, вы избегаете лишних аллокаций и проверок типов.

Размер пачки и пагинация меняют форму памяти. Получение 10 000 строк в слайс аллоцирует под рост слайса и для каждой строки — всё сразу. Если хендлеру нужна только страница, вынесите это в запрос и держите размер страницы стабильным. Если нужно обрабатывать много строк, стримьте их и агрегируйте небольшие сводки вместо хранения каждой строки.

Большие текстовые поля требуют внимания. Многие драйверы возвращают текст как []byte. Конвертация в string копирует данные, поэтому если вы делаете это по каждой строке, аллокации могут взорваться. Если значение нужно не всегда — отложите конвертацию или сканируйте меньше колонок для данного эндпойнта.

Чтобы подтвердить, кто аллоцирует — драйвер или ваш код — смотрите, что доминирует в профилях:

  • Если фреймы указывают на ваш mapping-код — фокусируйтесь на целях сканирования и конвертациях
  • Если фреймы указывают в database/sql или драйвер — сначала уменьшите строки и колонки, затем рассмотрите опции драйвера
  • Смотрите и alloc_space, и alloc_objects; много мелких аллокаций хуже, чем несколько больших

Пример: endpoint «list orders» делает SELECT * и сканирует в []map[string]any. Во время пика каждый запрос строит тысячи маленьких map и строк. Изменение запроса на выбор только нужных колонок и сканирование в []Order{ID int64, Status string, TotalCents int64} часто сразу снижает аллокации. Та же идея применима и к сгенерированным Go-бэкендам от AppMaster: горячая точка обычно в том, как вы формируете и сканируете данные, а не в самой БД.

Паттерны middleware, которые тихо аллоцируют на запрос

Снимайте нагрузку внутренними приложениями
Создавайте внутренние инструменты, которые снимают нагрузку с основного API в периоды пикового трафика.
Попробовать AppMaster

Middleware кажется дешёвым, потому что «это просто обёртка», но она выполняется на каждом запросе. При пике маленькие пер-запрос аллокации быстро суммируются и отражаются как рост скорости аллокаций.

Logging-мидлварь — частый источник: форматирование строк, построение map-ов полей или копирование заголовков для красивого вывода. Хелперы генерации request ID аллоцируют, когда создают ID, конвертируют его в строку, а затем кладут в context. Даже context.WithValue может аллоцировать, если вы кладёте в него новые объекты (или новые строки) на каждый запрос.

Сжатие и обработка тела запроса — ещё один частый виновник. Если middleware читает весь body, чтобы «заглянуть» или проверить, вы получите большой буфер на запрос. Gzip-мидлварь может аллоцировать много, если она каждый раз создаёт новые ридеры и райтеры вместо переиспользования буферов.

Auth и session-слои похожи. Если каждый запрос парсит токены, декодирует cookies в base64 или загружает session blob в новые структуры, вы получаете постоянный churn, даже когда работа handler-а лёгкая.

Tracing и метрики могут аллоцировать больше, чем ожидается, когда ярлыки строятся динамически. Конкатенация имён маршрутов, user-agent или tenant ID в новые строки на каждый запрос — классическая скрытая стоимость.

Паттерны, которые часто выглядят как «смерть от тысячи порезов»:

  • Построение логов через fmt.Sprintf и новые map[string]any на запрос
  • Копирование заголовков в новые map или слайсы для логов или подписи
  • Аллокация новых gzip-буферов и ридеров/райтеров вместо пулов
  • Создание метрик с высокой кардинальностью (много уникальных строк)
  • Сохранение новых структур в context на каждый запрос

Чтобы изолировать стоимость middleware, сравните два профиля: с полной цепочкой и с временно отключённым middleware или заменённым на no-op. Простой тест — health-эндпойнт, который должен быть почти без аллокаций. Если /health сильно аллоцирует во время пика, значит проблема не в handler-е.

Если вы используете Go-бэкенды, сгенерированные AppMaster, то то же правило применимо: измеряйте сквозные фичи (логирование, auth, tracing) и относитесь к пер-запрос аллокациям как к бюджету, который можно аудировать.

Фиксы, которые обычно быстро окупаются

Сохраняйте код чистым по мере итераций
Перегенерируйте чистый Go-код при изменении требований, не оставляя в проекте старых путей с высокой памятью.
Генерировать код

Когда у вас есть heap и allocs представления из pprof, приоритизируйте изменения, которые уменьшают пер-запрос аллокации. Цель не в хитрых трюках, а в том, чтобы горячий путь создавал меньше короткоживущих объектов, особенно под нагрузкой.

Начните с безопасных, скучных выигрышей

Если размеры предсказуемы — предвыделяйте. Если эндпойнт обычно возвращает ~200 элементов, создавайте слайс с capacity 200, чтобы он не рос и не копировался несколько раз.

Избегайте построения строк в горячих путях. fmt.Sprintf удобен, но часто аллоцирует. Для логирования предпочитайте структурированные поля и переиспользуйте небольшой буфер там, где это уместно.

Если вы генерируете большие JSON-ответы, подумайте о стриминге вместо того, чтобы строить один огромный []byte или string в памяти. Обычный сценарий пика: запрос приходит, вы читаете большой body, строите большой ответ — память прыгает, пока GC не догонит.

Быстрые изменения, которые обычно чётко видны в профилях до/после:

  • Предвыделяйте слайсы и map-и, когда знаете диапазон размеров
  • Замените тяжёлые операции fmt в обработке запросов на более дешёвые альтернативы
  • Стримьте большие JSON-ответы (кодируйте напрямую в response writer)
  • Используйте sync.Pool для переиспользуемых объектов одинаковой формы (буферы, энкодеры) и возвращайте их корректно
  • Установите лимиты на запросы (body size, payload size, page size), чтобы ограничить крайние случаи

Используйте sync.Pool аккуратно

sync.Pool помогает при повторной аллокации одних и тех же объектов, например bytes.Buffer на запрос. Он может навредить, если вы пулируете объекты с непредсказуемыми размерами или забываете их ресетить — тогда большие backing-массивы остаются живы.

Измеряйте «до» и «после» на тех же нагрузках:

  • Снимите allocs-профиль в окне пика
  • Внедрите одно изменение за раз
  • Прогоните тот же микс запросов и сравните total allocs/op
  • Наблюдайте хвостовую латентность, а не только память

Если вы используете генерированные Go-бэкенды AppMaster, эти фиксы всё равно применимы к кастомному коду вокруг хендлеров, интеграций и middleware — именно там обычно прячутся аллокации, вызванные пиками.

Частые ошибки с pprof и ложные тревоги

Самый быстрый способ потерять день — оптимизировать не то. Если сервис медленный — начните с CPU. Если его убивает OOM — начните с heap. Если он выживает, но GC работает постоянно — смотрите на скорость аллокаций и поведение GC.

Ещё одна ловушка — посмотреть только на “top” и считать, что всё ясно. “Top” скрывает контекст. Всегда смотрите стеки вызовов (или flame graph), чтобы увидеть, кто вызвал аллокатор. Исправление часто находится на один-два фрейма выше горячей функции.

Также следите за путаницей inuse vs churn. Запрос может аллоцировать 5 MB короткоживущих объектов, вызвать дополнительный GC и в итоге иметь только 200 KB inuse. Если смотреть только inuse, вы пропустите churn. Если смотреть только total allocations, можно оптимизировать то, что никогда не остаётся в памяти и не создаёт риск OOM.

Короткие проверки здравого смысла перед изменением кода:

  • Убедитесь, что вы в правильном представлении: heap inuse для удержания, alloc_space/alloc_objects для churn
  • Сравнивайте стеки, а не только имена функций (encoding/json часто симптом)
  • Воспроизводите трафик реалистично: те же эндпоинты, размеры payload, заголовки, конкуренция
  • Захватите baseline и spike-профиль, затем сделайте diff

Нереалистичные нагрузочные тесты дают ложные тревоги. Если ваш тест посылает маленькие JSON, а прод — 200 KB, вы оптимизируете не ту ветку. Если тест возвращает одну строку, вы никогда не увидите сканирование, которое проявляется при 500 строках.

Не гонитесь за шумом. Если функция появляется только в spike-профиле (а не в baseline) — это серьёзная подсказка. Если она встречается в обоих на одинаковом уровне — возможно, это нормальная фоновая работа.

Реалистичное прохождение инцидента

Деплойте туда, где нужно
Деплойте туда, где нужно — в облако или self-host, так вы сможете настраивать лимиты памяти и доступ к профайлингу.
Начать создание

В понедельник утром вышла промо-рассылка и ваш Go API начал получать в 8 раз больше трафика. Первый симптом — не краш. RSS растёт, GC работает активнее, p95 латентности скачет. Самый горячий эндпойнт — GET /api/orders, потому что мобильное приложение обновляет его при каждом открытии экрана.

Вы снимаете два снимка: один в спокойный момент (baseline) и один во время пика. Захватите одинаковый тип heap-профиля в оба раза, чтобы сравнение было корректным.

Порядок действий, который работает на практике:

  • Снимите baseline heap-профиль и запишите текущие RPS, RSS и p95 латентности
  • Во время пика снимите ещё один heap-профиль и allocs-профиль в том же 1–2 минутном окне
  • Сравните топ-аллокаторов между двумя профилями и сфокусируйтесь на том, что выросло сильнее всего
  • Идите от самой тяжёлой функции к её вызвавшим, пока не дойдёте до пути хендлера
  • Внесите одно небольшое изменение, задеплойте на один инстанс и повторно профилируйте

В нашем случае spike-профиль показал, что большинство новых аллокаций шли из JSON-энкодинга. Хендлер строил map[string]any строк, затем вызывал json.Marshal для слайса map-ов. Каждый запрос создавал много короткоживущих строк и interface-значений.

Самый маленький безопасный фикс — перестать строить map-ы. Сканируйте строки БД прямо в типизированную struct и кодируйте этот слайс. Ничего больше не меняется: те же поля, та же форма ответа, те же коды статусов. После развёртывания на одном инстансе аллокации в JSON-пути упали, время GC сократилось, и латентность стабилизировалась.

Только затем выкатывайте постепенно, наблюдая память, GC и ошибки. Если вы используете no-code платформу вроде AppMaster, это напоминание сохранять модели ответов типизированными и согласованными — это помогает избегать скрытых затрат на аллокации.

Последующие шаги, чтобы предотвратить следующий пик памяти

После стабилизации пика сделайте следующий менее драматичным. Относитесь к профилированию как к повторяемой тренировке.

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

Добавьте лёгкий мониторинг давления аллокаций до того, как вы получите OOM: размер heap, циклы GC в секунду и байты, аллоцируемые на запрос. Поймать «аллокации на запрос выросли на 30% неделю к неделе» часто полезнее, чем ждать жёсткой тревоги по памяти.

Поместите проверки раньше, с небольшим нагрузочным тестом в CI на репрезентативном эндпойнте. Маленькие изменения ответа могут удвоить аллокации, если они вызывают лишние копии — лучше выявить это до продакшена.

Если у вас генерируемый Go-бэкенд — экспортируйте код и профилируйте его так же. Сгенерированный код всё ещё — Go-код, и pprof укажет на реальные функции и строки.

Если требования часто меняются, AppMaster (appmaster.io) может быть удобным способом перестраивать и регенерировать чистые Go-бэкенды по мере эволюции приложения, а затем профилировать экспортированный код под реалистичной нагрузкой перед релизом.

Вопросы и ответы

Почему при резком пике трафика память растёт, если код не менялся?

Пик трафика обычно повышает скорость аллокаций сильнее, чем вы ожидаете. Даже маленькие временные объекты на запрос суммируются линейно с ростом RPS, что заставляет GC работать чаще и может поднять RSS, даже если «живой» heap сам по себе не большой.

Почему RSS растёт, а heap Go кажется стабильным?

Метрики heap показывают память, управляемую Go, а RSS включает в себя больше: стеки горутин, метаданные рантайма, OS-мэппинги, фрагментацию и любые вне-heap аллокации (включая некоторые cgo). Поэтому RSS и heap могут вести себя по-разному при пиках — используйте pprof, чтобы находить горячие точки аллокаций в коде, а не пытаться «подогнать» RSS точно.

С чего начать смотреть при пике: heap или alloc-профили?

Начинайте с heap-профиля, если вы подозреваете удержание объектов (что-то остаётся в памяти), и с профиля, ориентированного на аллокации (например, allocs/alloc_space), если подозреваете churn (много короткоживущих объектов). При пиках трафика чаще всего проблема — именно churn, он увеличивает нагрузку GC и хвостовую латентность.

Как безопаснее всего открыть pprof в продакшене?

Проще и безопаснее всего запускать pprof на отдельном admin-сервере, привязанном к 127.0.0.1, и делать его доступным только внутри сети. Относитесь к pprof как к админ-интерфейсу — он раскрывает внутренности сервиса.

Сколько профилей нужно снять и когда?

Снимайте короткую временную шкалу: профиль за несколько минут до пика (baseline), профиль во время пика (impact) и ещё один после (recovery). Так легче понять, что изменилось, вместо того чтобы гоняться за обычными фоновыми аллокациями.

В чём разница между inuse и alloc_space в pprof?

Используйте inuse, чтобы находить то, что реально удерживается к моменту съёмки, и alloc_space, чтобы видеть, что активно аллоцируется. Частая ошибка — смотреть только inuse и пропускать churn, который вызывает трешинг GC и скачки латентности.

Какие быстрые способы сократить аллокации, связанные с JSON?

Если encoding/json доминирует в профиле, обычно виновата форма данных, а не сама библиотека. Замените map[string]any на типизированные структуры, избегайте json.MarshalIndent в продакшене и не собирайте JSON через временные строки — это часто сразу сокращает аллокации.

Почему сканирование результатов запросов в БД может взрывать память при пиках?

Сканирование строк в гибкие цели, такие как interface{} или map[string]any, конвертация []byte в string для каждого поля и вытаскивание слишком большого числа строк/колонок — всё это сильно аллоцирует на запрос. Выбирайте только нужные колонки, используйте пагинацию и сканируйте прямо в конкретные поля структур, чтобы снизить аллокации.

Какие паттерны middleware обычно приводят к «смерти от тысячи порезов» аллокаций?

Middleware выполняется на каждом запросе, поэтому даже мелкие аллокации на запрос быстро суммируются. Формирование строк логов, генерация ID, создание gzip-ридеров/райтеров без переиспользования, динамические метрики с высокой кардинальностью и сохранение новых структур в context — это типичные тихие пожиратели памяти.

Могу ли я применять этот рабочий процесс pprof к Go-бэкендам, сгенерированным AppMaster?

Да. Подход, основанный на профилях, применим к любому Go-коду, сгенерированному или написанному вручную. Если вы экспортируете генерируемый бэкенд, можно запускать pprof, находить пути аллокаций и корректировать модели, хендлеры и общую логику, чтобы снизить аллокации до следующего пика.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster