DarkRiDDeR15 мин

Разбор request в D-сервисе: allocation, условная GC-граница и безопасный откат

DLangBackendНадёжность

В trace учебного request heavy-вариант пересёк allocation budget 6 и получил отметку gc-boundary. Рядом есть FFI и I/O boundaries, а invalid input уходит в error response. Самая дорогая ошибка здесь — назвать отметку реальной паузой, обвинить внешнюю систему без вызова или сразу менять конфигурацию runtime. Тогда исчезают и причина, и возможность безопасно откатить change. Цена ошибки — потратить время на изменение runtime без доказанного источника паузы.

Это не отчёт о production-инциденте. В нём нет реального профиля, сервера, D compiler, DRuntime, HTTP, foreign code или I/O. Есть один детерминированный in-memory request, два варианта одной обработки и error input. Его цель — собрать evidence в правильном порядке: result, stage trace, заданные units, выбранная boundary и только потом действие. Такая дисциплина полезна до того, как появятся цифры настоящего инструмента.

Собираем evidence до изменения runtime

Первый набор evidence небольшой: request id, нормализованный вход, success body либо error body, последовательность stages, allocation/work units модели, budget и список external boundaries. Значения, похожие на время, здесь запрещены: fixture не показывает миллисекунды, CPU или память процесса. Если соседняя система говорит о паузе, это отдельный факт с отдельным источником, а не расшифровка записи gc-boundary.

В valid варианте body остаётся {"requestId":"runtime-d-training-42","total":42}. Heavy-вариант получает 12 allocation units и не проходит budget 6. Reuse-вариант получает 3 units и проходит budget. Оба имеют 9 work units. Такое доказательство ещё не отвечает, какой код нужно менять в D. Оно только запрещает одновременно менять contract handler и считать, что уменьшение model units уже стало оптимизацией процесса.

Как классифицировать наблюдение до исправления
НаблюдениеЧто фактически естьВероятная границаПервое безопасное действие
heavy > budget12 units при budget 6, body равен reuseучебные временные состояниясохранить два trace и проверить локальный reuse change при том же output
есть gc-boundaryпересечён порог 8 units, поле not-a-runtime-pauseграница для будущего profilingне объявлять pause; выбрать реальный метод измерения отдельно
есть FFI/I/O boundaryinvocation: not-performedконтракт внешнего вызова ещё не проверенне обвинять зависимость; собрать owner, input/output и отдельный test
validation-errorstatus 400 и encode-errorошибка входа до computeсохранить error input и не применять success-path оптимизацию как исправление
result различаетсяsuccess body не совпадаетнарушен contract handlerостановить локальную оптимизацию и вернуть равный результат прежде budget

Читаем error route так же внимательно, как success

Проверка только счастливого запроса всегда оставляет дыру. В fixture invalid operation divide декодируется, отвергается validation и проходит через encode-error. Он не доходит до compute, FFI или I/O. Это даёт ясную отрицательную проверку: изменение buffer reuse не должно заставить ошибочный вход исчезнуть, стать успехом или неожиданно пересечь external boundary.

const report = runDRuntimeServiceFixture();
const failed = report.invalidRequest;

console.log(failed.response);
// { ok: false, status: 400, body: '{"error":"invalid-operation"}' }
console.log(failed.trace.map((entry) => entry.stage));
// decode, validation-error, encode-error

if (!failed.model.errorRouteVisible) {
  throw new Error("error route disappeared from the training trace");
}

Такой error trace нужен и при разборе возможного allocation growth. Если heavy-вариант создаёт дополнительные units до validation, а invalid request больше не виден, нельзя сказать «мы сократили pressure». Возможно, мы просто перестали обрабатывать некорректный вход тем же контрактом. Поэтому fixture одновременно проверяет same handler result для success, явный status 400 для error и отсутствие compute/FFI/I/O в отрицательной ветке.

Отделяем условную GC-границу от причины паузы

У записи gc-boundary ровно одно значение: заданный счётчик пересёк заданный порог. Эта запись полезна, потому что показывает место, где команда договорилась задать фактический вопрос. Она не говорит, был ли GC, как он работал, остановил ли потоки, сколько длилась пауза и связана ли она с request. Документация D описывает automatic memory management, но переход от общего механизма к конкретному симптому всегда требует tool output и условий запуска.

Безопасный отчёт поэтому выглядит скромно: «в учебной модели 12 units пересекают budget 6; boundary помечена; фактического profile нет». Небезопасный отчёт добавляет к этому «из-за GC endpoint зависает». Второй текст звучит увереннее, но у него нет входа, версия, метод, график или trace реального процесса. Практическая работа начинается с первого текста: он позволяет назначить следующий эксперимент и не переписать систему по впечатлению.

Диагностический маршрут для учебного request в D-сервисе: сначала сохранить result, error и trace; затем проверить равенство handler result, allocation budget, модельную GC-границу и неисполняемые FFI/I/O boundaries. При любом несоответствии выбрать обратимое локальное действие, а не менять runtime глобально.
Маршрут разделяет contract handler, учебный allocation budget и внешнюю границу. Ни одна карточка не называет model unit профилем или pause.

Rollback-safe действие: откатываем гипотезу, не evidence

Если heavy и reuse возвращают один body, а отличается только model allocation, допустима маленькая обратимая правка: временное состояние в одном участке заменяется на повторное использование, а valid и invalid traces остаются рядом с change. Откат в таком случае — вернуть локальный путь и снова получить прежний trace. Нельзя называть rollback-safe глобальное отключение GC, смену allocator или переписывание foreign code без теста внешнего договора: такие действия расширяют границу и могут скрыть исходный симптом.

Если body различается, error route исчезает или boundary стала выполнять внешний вызов, действие другое: остановить оптимизацию, сохранить samples и восстановить прежний contract handler. Здесь важнее не сделать «быстрый» patch, а не потерять смысл ошибки. После этого можно отдельно решать, нужна ли новая схема данных, другой API или реальное измерение. Смешивать этот разбор с одной настройкой runtime нельзя: разная причина требует разного owner и обратимости.

Выбор обратимого действия
Подтверждённая ситуацияЧто меняемЧто сохраняемЧего не делаем
равный output, heavy выше budget, reuse внутриодин локальный temporary pathоба valid trace, budget и same-result assertionне объявляем оптимизацию production без profile
условная GC boundary без real evidenceничего в runtimeinput, trace, версия будущего инструментане включаем/выключаем GC по модели
FFI/I/O boundary подозрительнаотдельный contract test границыintent, owner, expected error/resultне приписываем внешнему коду вызов, которого не было
invalid input изменил маршрутвозвращаем validation/error contractinvalid sample и error traceне сравниваем allocation до восстановления error route
успешный body изменилсяостанавливаем локальную правкуexpected body и diff resultне прячем разницу за новой версией response

Источники обозначают границы, а не готовый диагноз

Для терминов D я сверяю official language specification: automatic memory management, @nogc и ABI. Для исторической точки использован опубликованный до октября 2021 release D 2.097.2 и exact DRuntime source snapshot его тега. Ни один из этих источников не содержит profile данного request, потому что такого request не существовало. Поэтому они поддерживают только аккуратные утверждения о языке и границе ответственности.

Такой подход особенно важен для FFI. ABI объясняет, почему граница не исчезает из архитектуры, но не описывает particular foreign function, её ownership или возможный blocking. Аналогично источник о GC помогает назвать механизм, но не превращает 12 учебных units в измеренную pause. Чем выше соблазн сделать вывод о реальной среде, тем важнее отдельно сохранить command, версию, вход, платформу и исходные results.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Сохраните один success или error request вместе с result/body. Не исправляйте runtime до появления конкретного input.
  2. Причина-гипотеза. Разделите contract, model allocation, model GC boundary, FFI boundary и I/O boundary. Одно наблюдение не обязано объяснять остальные.
  3. Проверка результата. Сравните success body и status two variants; затем прогоните invalid input и убедитесь, что validation-error/encode-error сохранились.
  4. Проверка бюджета. Если result одинаков, проверьте allocation units против явно записанного budget. Смотрите на GC entry только как на пометку модели.
  5. Действие. При локальном budget difference меняйте один обратимый temporary path. При FFI/I/O выбирайте отдельный contract test. При разном result или error route сначала восстановите handler contract.
  6. Проверка после действия. Повторите те же valid и invalid samples, сохраните traces и только затем назначьте реальный profiler с описанным окружением.

Ограничения и следующий проверяемый шаг

Полевой маршрут не даёт production SLA, benchmark result, замер паузы, масштабирование, rate, memory footprint, сведения о compiler flags или DRuntime configuration. Он не исполняет D, FFI, file, network, HTTP, database, process, GC или profiler. Условные units не имеют единицы времени и не должны попадать в dashboard как metric. Откат в таблице — образец безопасного порядка, а не команда для чужой инфраструктуры.

Следующий проверяемый шаг для своего сервиса — написать один integration-level trace вокруг выбранного handler без чувствительных данных, сохранить valid и invalid inputs, а затем выбрать ровно одну реальную границу для измерения. Если trace показывает внешний вызов, сначала тестируем его договор. Если одновременно меняются result и units, возвращаем contract. Если остаётся один вопрос про allocation, тогда можно сравнить фактические данные в зафиксированной среде. Так расследование не обещает лишнего и оставляет путь к следующему доказательству.

Проверяемые источники

  • D 2.097.2: source snapshot of Automatic Memory Management specification — неизменяемый исходник официальной спецификации из тега v2.097.2; он задаёт терминологию automatic memory management, но не профиль конкретного запущенного сервиса
  • D 2.097.2: source snapshot of @nogc function specification — неизменяемый исходник официальной спецификации атрибута @nogc; он ограничивает проверяемый D-код и не доказывает свойства внешней библиотеки или операции ввода-вывода
  • D 2.097.2: source snapshot of Application Binary Interface specification — неизменяемый исходник официальной границы ABI для взаимодействия с C ABI целевой системы; наличие границы не говорит о стоимости или безопасности конкретного foreign call
  • D 2.097.2: официальный release record — выпуск опубликован 9 августа 2021 года и служит проверяемой дооктябрьской точкой для терминов; статья не выводит из номера версии benchmark или runtime profile
  • DRuntime 2.097.2: immutable snapshot core/memory.d — точная фиксация исходника DRuntime, на которую указывал тег v2.097.2; используется для исторической проверки терминов, не как замена измерения приложения