Горутины не умирают молча: как заставить LLM читать pprof и race-отчёты так, как это делает дежурный инженер

Представьте: три часа ночи, алерт на 47 ГБ RSS у сервиса, который «ничего не делает». Вы открываете pprof, видите 12 000 горутин — и понимаете, что цифра ничего не объясняет. go tool pprof показывает, где они живут, но не почему они там. Race detector выдаёт стектрейс из 80 строк, где две из них — ваши, а остальные — рантайм и сторонняя библиотека.

Именно в этот момент LLM из «игрушки для генерации boilerplate» превращается в напарника по диагностике — но только если вы даёте ей правильный вход. Не «найди утечку», а «вот вывод pprof, вот как устроен воркер, скажи, какой из четырёх паттернов здесь ломается». Ниже — 12 промтов, разбитых по уровню: от быстрого чтения симптома до проверки гипотез, которые вы бы сами проверяли час.

Базовый уровень: перевести симптом в гипотезу

1. Расшифровать дамп горутин в человеческие категории

Задача: у вас есть debug=1 дамп горутин, но 12 000 стеков сливаются в кашу. Нужно разбить их на паттерны, а не читать по одному.

Промт:

Ты — Go-инженер, специализирующийся на runtime и профилировании. Ниже — вывод `go tool pprof -traces` (или текстовый дамп `runtime.Stack(buf, true)`) из сервиса, у которого RSS растёт линейно и не падает после ночного пика.

Разбей все горутины на смысловые группы по стеку (не по количеству). Для каждой группы укажи:

1) где горутина «висит» (какой вызов блокирующий: select, chan recv, chan send, sync.WaitGroup.Wait, sync.Mutex.Lock, time.Sleep, netpoll);
2) растёт ли число горутин этой группы со временем и почему;
3) это утечка или нормальный пул (например, http.Server держит keep-alive соединения);
4) минимальный эксперимент, который подтвердит или опровергнет гипотезу за один запуск.

Не предлагай «добавить логирование» как первый шаг. Сначала — что уже видно в стеке.

[вставьте дамп]

Пример результата: LLM выделяет группу из 8 900 горутин, застрявших в chan send на буферизованном канале размера 100 — и объясняет, что продюсер пишет быстрее консьюмера, а консьюмер упал в context.WithTimeout, но не завершил цикл. Вы получаете не «где», а «почему».

Что проверить первым Если в стеке больше половины горутин висят на одном и том же chan send или chan recv — это почти всегда не утечка, а отсутствие backpressure. Начинайте не с кода, а с вопроса «кто должен был остановиться и не остановился».

2. Прочитать race-отчёт и отделить своё от чужого

Задача: race detector выдал отчёт, где фигурируют поля из вашего пакета и из encoding/json или ORM. Нужно понять, где реальная гонка, а где — следствие.

Промт:

Ниже — отчёт `go test -race` / `go run -race` для сервиса на Go 1.2x. Разбери его по шагам:

1) какие две горутины участвуют и какой адрес памяти общий;
2) какое поле или переменная реально гоночная (а не объект целиком);
3) какая из записей «первая» по времени и что это говорит о порядке инициализации;
4) отметь места, где стек уходит в стандартную библиотеку или сторонний пакет — там гонка обычно следствие, а не причина;
5) предложи минимальное исправление (мьютекс, атомик, канал, копирование значения) и объясни, почему остальные варианты здесь хуже.

Покажи, как выглядел бы отчёт, если бы исправление сработало (что именно исчезло бы из стека).

[вставьте отчёт]

Пример результата: модель указывает, что гонка идёт не на map, а на поле config.reloadAt типа time.Time, которое пишется из горутины-релоадера без синхронизации и читается в hot path. Исправление — atomic.Pointer[config] вместо мьютекса, потому что читателей много, писатель один.

3. Проверить контексты: кто не получил cancel

Задача: горутины живут дольше запроса, потому что context.WithCancel создали, но cancel() не вызвали на одной из веток.

Промт:

Проверь этот Go-код на утечки, связанные с context:

1) каждый ли `context.WithCancel` / `WithTimeout` / `WithDeadline` имеет гарантированный вызов cancel (включая пути с ошибкой и ранним return);
2) не передаётся ли `context.Background()` или `context.TODO()` в место, где должен быть родительский контекст запроса;
3) нет ли горутины, которая читает `ctx.Done()`, но не имеет выхода по другому условию (цикл без проверки ctx на каждой итерации);
4) где контекст хранится в структуре (это антипаттерн — объясни, чем он плох именно здесь).

Для каждой находки: строка, последствие в рантайме, исправление. Если проблем нет — скажи прямо, не выдумывай.

[вставьте код]

Пример результата: LLM находит ветку, где после if err != nil { return err } cancel не вызывается, а горутина-обработчик продолжает держать соединение с БД. Вы получаете точную строку и понимание, что таймер контекста продолжает тикать до дедлайна — при тысяче запросов в секунду это накапливается.

Продвинутый уровень: гипотезы и эксперименты

4. Смоделировать сценарий, при котором WaitGroup не досчитается

Задача: wg.Add вызывается внутри горутины, а не до её запуска — классика, которая «иногда работает».

Промт:

Вот функция, использующая sync.WaitGroup и каналы. Найди все места, где порядок Add/Done/Wait может привести к панике или зависанию. Для каждого:

1) опиши конкретный сценарий исполнения (какая горутина что делает в каком порядке), при котором происходит сбой;
2) объясни, почему тесты этого не ловят (обычно — из-за планировщика и GOMAXPROCS);
3) предложи исправление и покажи тест, который воспроизводит проблему стабильно (с `-count=100` или `-race`).

Не переписывай код целиком — покажи точечный дифф.

[вставьте код]

Пример результата: модель описывает гонку между wg.Add(1) внутри горутины и wg.Wait() в главной — и даёт тест с runtime.Gosched() в нужном месте, который падает в 9 из 10 прогонов. Это уже воспроизводимая проблема, а не «иногда падает на проде».

5. Найти закрытие канала, которого не должно быть

Задача: panic: send on closed channel в проде, но не в тестах. Обычно канал закрывает не тот, кто пишет.

Промт:

Проверь этот код на корректность работы с каналами:

1) кто владеет каналом (создаёт), кто пишет, кто читает, кто закрывает — построй таблицу;
2) есть ли путь, где канал закрывается дважды или закрывается писателем при нескольких продюсерах;
3) есть ли читатель, который продолжает слать в канал после close (например, в defer или в retry-цикле);
4) как выглядит корректный паттерн «один писатель, N читателей» и «N писателей, один читатель» для этого конкретного случая.

Для каждого нарушения покажи минимальный воспроизводящий пример.

[вставьте код]

Пример результата: LLM строит таблицу владения и показывает, что два воркера пишут в один канал, а закрывает его третий по таймауту. Исправление — sync.Once вокруг close или отдельный канал завершения.

Ключевое различие Утечка горутин и гонка данных — разные болезни с разными симптомами. Утечка растёт по памяти и числу горутин и не падает после нагрузки; гонка может жить годами и проявиться один раз на 100 000 запросов. Промт для одного почти никогда не помогает для другого — не смешивайте их в одном запросе.

6. Связать рост памяти с конкретным аллокатором

Задача: heap-профиль показывает рост, но неясно, это утечка объекта или просто GC не успевает.

Промт:

У меня есть `go tool pprof -http` heap-профиль (inuse_space) и график RSS. Помоги отличить утечку от давления GC:

1) какие признаки в heap-профиле говорят об утечке (растущий inuse_space в конкретной функции между двумя снимками), а какие — о нормальной работе;
2) как сравнить два профиля (`-base`) и что искать в diff;
3) какие типы объектов чаще всего «висят» из-за ссылок в глобальных структурах, кешах без TTL и замыканиях;
4) предложи команду `go tool pprof` и что в её выводе будет решающим.

Не говори «используйте pprof» — я уже его использую.

Пример результата: модель объясняет, что стабильный inuse_space при растущем RSS — это фрагментация или GOGC, а не утечка; и что diff двух профилей с интервалом в 10 минут покажет функцию, которая накопила объекты. Вы получаете критерий, а не ссылку на документацию.

7. Проверить пул соединений и его «невидимые» горутины

Задача: database/sql держит горутины, которые вы не создавали, и они маскируют вашу утечку.

Промт:

В дампе горутин есть группы из пакетов database/sql, net/http и стороннего клиента. Объясни:

1) какие горутины создаются этими библиотеками штатно и сколько их ожидается при N соединениях;
2) какие настройки пула (MaxOpenConns, MaxIdleConns, ConnMaxLifetime) влияют на число горутин и как;
3) как отличить «библиотека держит штатно» от «мой код не вернул соединение в пул»;
4) что искать в стеке, чтобы понять, что соединение не закрыто (например, отсутствие rows.Close, незакрытый tx).

[вставьте фрагмент дампа]

Пример результата: LLM показывает, что 40 горутин в database/sql.(*DB).connectionOpener — норма при MaxIdleConns=40, а 900 горутин в (*Rows).Next — это забытый rows.Close() в обработчике ошибки.

Экспертный уровень: заставить LLM спорить с вами

8. Сгенерировать тест, который ловит утечку за секунды

Задача: воспроизвести утечку в CI, а не ждать продакшена.

Промт:

Напиши Go-тест, который стабильно обнаруживает утечку горутин в этой функции. Требования:

1) используй `runtime.NumGoroutine()` или `goleak` (uber-go/goleak) — объясни, какой вариант надёжнее и почему;
2) тест должен падать на текущей реализации и проходить после исправления, которое ты предложишь;
3) учти, что фоновые горутины рантайма и тестового фреймворка не должны давать ложных срабатываний;
4) покажи вывод теста при падении — что именно увидит разработчик в CI.

[вставьте функцию]

Пример результата: тест с goleak.VerifyNone(t) и явным time.Sleep для планировщика, который падает с сообщением «found unexpected goroutines» и стеком виновника. Плюс замечание: NumGoroutine() без baseline даёт ложные срабатывания в параллельных тестах.

9. Проверить гипотезу «это не наш код, а библиотека»

Задача: подозрение, что утечка в стороннем пакете, но доказать это сложно.

Промт:

Я подозреваю, что утечка горутин идёт из сторонней библиотеки, а не из моего кода. Помоги построить доказательство:

1) как изолировать библиотеку в отдельном тесте и измерить число горутин до и после;
2) какие признаки в стеке указывают на библиотеку (имя пакета, сигнатура функции, замыкание);
3) как проверить версию библиотеки и найти, исправлено ли это в более новой (что искать в changelog и issues);
4) если утечка подтверждена — какие обходные пути есть без форка (обёртка, ограничение параллелизма, явное закрытие).

Не утверждай, что это баг библиотеки, пока не показан воспроизводящий тест.

Пример результата: модель предлагает минимальный main, который подключает библиотеку, делает один вызов и печатает runtime.NumGoroutine() до и после с time.Sleep(2*time.Second). Если число растёт — доказательство есть, и вы идёте в issues с готовым репро.

10. Разобрать дедлок по двум мьютексам

Задача: сервис завис, SIGQUIT дал дамп, где все горутины спят на sync.Mutex.Lock.

Промт:

Ниже — дамп горутин, снятый по SIGQUIT во время зависания. Найди цикл ожидания:

1) построй граф «горутина → мьютекс, который она держит → мьютекс, которого ждёт»;
2) покажи цикл, если он есть, и укажи порядок захвата в каждом месте;
3) объясни, как этот дедлок мог не проявляться в тестах (разный порядок захвата в двух путях);
4) предложи правило порядка захвата и как его проверить (например, единая функция, которая всегда берёт A, потом B).

Если цикла нет — скажи, что это не дедлок, а starvation или блокировка на канале.

Пример результата: граф показывает цикл A→B в одном обработчике и B→A в другом; модель предлагает вынести захват в одну функцию и добавляет в тест параллельный вызов обоих путей с -race.

11. Спроектировать нагрузочный эксперимент, который воспроизводит race

Задача: гонка есть, но ловится раз в сутки. Нужно ускорить её проявление.

Промт:

У меня есть подозрение на гонку между двумя горутинами, но `-race` в обычных тестах её не ловит. Предложи эксперимент:

1) как увеличить вероятность гонки (GOMAXPROCS, число итераций, `-count`, `runtime.Gosched`, задержки в критических местах);
2) как использовать `-race` в сочетании с нагрузкой, не убив CI по времени;
3) какие метрики снимать, чтобы понять, что гонка воспроизвелась (не только падение);
4) как оформить это как отдельный «медленный» тест, который не блокирует основной пайплайн.

Дай конкретные флаги и структуру теста, а не общие советы.

Пример результата: модель предлагает тест с t.Parallel(), -race -count=200, runtime.GOMAXPROCS(4) и искусственной задержкой между read и write через time.Sleep(time.Microsecond). Race detector срабатывает в большинстве прогонов, тест помечен //go:build race и идёт отдельным job в CI.

Где чаще ошибаются Самый частый провал промта — просить LLM «найти все проблемы» в куске кода без симптома. Модель начинает выдумывать. Давайте ей артефакт: дамп, отчёт, профиль, лог — тогда ответ опирается на данные, а не на фантазию.

12. Проверить архитектуру на устойчивость к утечкам до релиза

Задача: не ловить утечки в проде, а спроектировать так, чтобы их было видно сразу.

Промт:

Вот архитектура сервиса (опишу словами): HTTP-хендлеры, пул воркеров, фоновая горутина-релоадер конфига, кеш с TTL, клиент к внешнему API. Проведи аудит на устойчивость к утечкам горутин и гонкам:

1) для каждого компонента — какие горутины он создаёт и кто их завершает;
2) где нет явного владельца жизненного цикла (кто должен вызвать cancel/close);
3) какие метрики и проверки добавить, чтобы утечка была видна в первые минуты (число горутин, размер очередей, время ожидания в каналах);
4) какие два теста-«стража» стоит добавить в CI, чтобы регрессия не прошла.

Дай таблицу «компонент → риск → как заметить → как исправить». Без общих слов про «лучшие практики».

Пример результата: таблица, где у кеша с TTL нет фоновой очистки — записи живут вечно; у релоадера нет выхода по контексту — он переживает shutdown; у воркеров нет backpressure — очередь растёт. И два теста: goleak на graceful shutdown и проверка, что после отмены контекста число горутин вернулось к baseline.

Что реально даёт LLM в диагностике, а что нет

Trade-off честный. LLM отлично читает артефакты: дампы, отчёты, профили, стектрейсы — и находит паттерны быстрее, чем человек с 80 строками race-отчёта в терминале. Но она не запускает ваш код и не знает вашу архитектуру, пока вы её не опишете. Если дать ей только «найди утечку» без данных — получите список банальностей про закрытие каналов.

Режим отказа тоже предсказуем: модель уверенно называет причиной то, что видно в стеке, но стек показывает место блокировки, а не логическую ошибку, которая к ней привела. Поэтому последний шаг всегда за вами — воспроизвести в тесте. Промты 8–11 как раз про это: превратить гипотезу в падающий тест.

И неочевидное: самые полезные промты — не «найди проблему», а «построй граф», «построй таблицу владения», «покажи, как выглядел бы отчёт после исправления». Структурированный вывод заставляет модель опираться на данные, а вам даёт артефакт, который можно показать команде на ревью.

Начните с одного: возьмите последний дамп горутин из прода и прогоните через промт №1. Если после ответа у вас появилась гипотеза, которую можно проверить тестом за 15 минут, — подход работает. Если нет — добавьте в промт данных: конкретный стек, настройки пула, версию Go. Диагностика начинается не с вопроса «что не так», а с вопроса «вот что я вижу — что это значит».

← Все статьи

Спросить AI по этой статье

Задайте вопрос по этой статье — ASI Biont ответит, опираясь на её текст.

Читайте также

ИИ для повседневной жизни: как построить реальный семейный бюджет с ChatGPT и Claude (без кода, 2026)

27 октября 2026

Сколько времени нужно на иностранный язык: как ИИ и кривая забывания дают разговорный английский за год при 20 минутах в день

27 октября 2026

Цифровая гигиена за вечер: как найти утечки персональных данных и удалить информацию о себе из интернета

27 октября 2026

Юнит-тесты на Python с LLM: 12 промтов от pytest-фикстур до Hypothesis, которые закрывают непокрытые ветки

27 октября 2026

Cambridge Primary English (0058): почему начальные классы решают, будет ли ребёнок когда-либо читать бегло — и как на самом деле работает программа

27 октября 2026

Full-Stack AI Engineer: как RAG-пайплайны, векторные базы данных и развертывание LLM на самом деле работают в продакшене

28 октября 2026

Сверка актов вручную или промптом: где LLM экономит бухгалтеру часы, а где создаёт риски

12 ноября 2026

Продакт-менеджмент и рост: стратегия, метрики и решение, которое действительно меняет ситуацию

11 ноября 2026

Эмоциональный интеллект: как измерить базовый уровень EQ, вести дневник эмоций и составить 30-дневный план саморегуляции

28 октября 2026