8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 225,
|
||
"slug": "editorial-2021-10-practice-d-runtime-service",
|
||
"title": "D-runtime в сервисе: как найти лишнее выделение на одном запросе",
|
||
"excerpt": "Если endpoint обвиняют в медленном D-runtime, начните с одного запроса: зафиксируйте результат, отделите GC от FFI и I/O, а затем проверьте гипотезу измерением, а не настройкой вслепую.",
|
||
"contentHtml": "<p>Endpoint иногда отвечает заметно дольше обычного, а в профиле рядом с ним появляется GC. Команда называет причиной «медленный D-runtime» и меняет глобальные настройки сборщика. Симптом может исчезнуть на коротком тесте, но контракт запроса и границы внешних вызовов остаются непроверенными. Цена ошибки — лишний риск в каждом запросе: можно получить больше памяти, длиннее паузы или несовместимость с библиотекой, не доказав, что исправлен нужный участок.</p>\n<p>Надёжнее начать с одного request path. Зафиксируйте вход, успешный ответ и ошибочный путь. Затем отделите временные объекты от GC boundary, FFI и I/O. Сначала нужно доказать, что два варианта делают одну работу и возвращают один результат. Только после этого имеет смысл обсуждать allocation, настройки DRuntime или переписывание горячего участка.</p>\n<h2>Тезис: runtime — это граница вопроса, а не причина</h2>\n<p>Слово «runtime» скрывает несколько разных механизмов. D-код может создать временный массив. Сборщик может увидеть доступные объекты. Foreign function может выделить память по собственному контракту. I/O может заблокировать поток. Эти события находятся рядом в трассе запроса, но не имеют одной проверки.</p>\n<p>Узкий вопрос выглядит так: «При одинаковых входе и ответе создаёт ли этот этап больше временного состояния, чем выбранный бюджет?» Такой вопрос допускает проверку. Вопрос «D тормозит?» не говорит, что измерять и какое действие будет правильным.</p>\n<h2>Механизм одного request path</h2>\n<p>Разделите запрос на четыре этапа: <code>decode</code>, <code>validate</code>, <code>compute</code> и <code>encode</code>. Для каждого запишите вход и выход. Рядом отметьте границы, которые не принадлежат учебному примеру: вызов C-библиотеки, сеть, файл или база данных. Граница должна остаться в trace даже тогда, когда вызов ещё не выполняется.</p>\n<p>В учебной модели allocation units — условные единицы счёта. Они не равны байтам, времени CPU, latency или числу запусков GC. Бюджет 6 означает только одно: выбранный вариант превышает порог, заданный примером. Реальный порог нужно выбрать для конкретного endpoint и подтвердить инструментом в конкретной версии компилятора, DRuntime и окружения.</p>\n<table><caption>Карточка проверки одного запроса</caption><thead><tr><th>Поле</th><th>Что фиксируем</th><th>Что проверяем</th></tr></thead><tbody><tr><td>Вход</td><td><code>sum(19, 23)</code>, request id</td><td>Оба варианта получают одинаковые данные</td></tr><tr><td>Ответ</td><td><code>{"requestId":"runtime-d-42","total":42}</code></td><td>Оптимизация не меняет контракт</td></tr><tr><td>Работа</td><td>Одинаковое число work units</td><td>Сравнение не прячет другую алгоритмическую работу</td></tr><tr><td>Allocation</td><td>Условные units и budget</td><td>Видно превышение выбранного порога</td></tr><tr><td>Границы</td><td>GC, FFI, I/O с явным статусом</td><td>Неисполненный внешний вызов не принимают за измеренный</td></tr></tbody></table>\n<p>Вариант <code>allocation-heavy</code> может получить 12 units, а вариант <code>reuse</code> — 3. Если оба возвращают тот же JSON и выполняют то же число work units, модель выделяет одну гипотезу: промежуточное состояние. Она не доказывает, что второй вариант быстрее на production-трафике.</p>\n<h2>Пример: сохраняем ответ и меняем только промежуточное состояние</h2>\n<p>Ниже показан маленький пример на D. Он ограничен вычислением суммы. В нём нет HTTP, сериализации, GC-профайлера и FFI. Комментарии помечают места, где реальный сервис должен добавить отдельную проверку.</p>\n<pre><code>struct Response {\n string requestId;\n int total;\n}\n\nResponse handle(int left, int right) {\n // Учебный контракт: результат зависит только от входа.\n return Response(\"runtime-d-42\", left + right);\n}\n\n@nogc int compute(int left, int right) {\n // Здесь нет операций, которые требуют GC-аллокации.\n return left + right;\n}\n\nvoid requestTrace() {\n // decode -> validate -> compute -> encode\n // FFI: not performed; I/O: not performed.\n auto response = handle(19, 23);\n assert(response.total == 42);\n}</code></pre>\n<p>Аннотация <code>@nogc</code> полезна как ограничение на вызываемый D-код: компилятор проверяет конструкции и вызовы, которые могут потребовать GC-аллокации. Но она не делает безопасной неизвестную внешнюю функцию. Она также не доказывает отсутствие пауз сборщика во всём процессе. Другой поток может работать с GC, а FFI-библиотека может иметь собственный allocator. Поэтому атрибут — часть границы, а не итоговый performance report.</p>\n<p>Если в реальном коде <code>encode</code> создаёт строку, это нужно увидеть в trace и подтвердить профилем. Нельзя вывести размер выделения из одного только названия функции. Нельзя считать вызов C безопасным по факту успешной компиляции: проверьте calling convention, layout, ownership, освобождение памяти и возможность блокировки.</p>\n<figure><img src=\"/assets/editorial/2021/d-runtime-request-path-2021.svg\" alt=\"Схема request path в D-сервисе: decode, validation, compute, encode, а также границы GC, FFI и I/O\"><figcaption>Маршрут одного запроса. Граница GC на схеме означает точку проверки, а не подтверждённую паузу. FFI и I/O отмечены как внешние границы, пока вызов не измерен.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина-гипотеза</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Растёт allocation на успешном запросе</td><td>Временное состояние создаётся на decode или encode</td><td>Сравнить одинаковый input, output, work units и allocation trace</td><td>Убрать промежуточную копию или переиспользовать буфер; повторить проверку контракта</td></tr><tr><td>В трассе есть GC boundary</td><td>Выбранный путь пересёк условный порог</td><td>Проверить реальный профиль с версией compiler, DRuntime и платформой</td><td>Изменять локальный участок только после измерения; не менять глобальную настройку вслепую</td></tr><tr><td>После FFI меняется задержка</td><td>Библиотека выделяет память или блокирует поток</td><td>Проверить ABI, ownership, allocator и время вызова отдельно</td><td>Согласовать контракт с владельцем библиотеки; не приписывать эффект GC</td></tr><tr><td>Endpoint медленный только при ошибке</td><td>В error path остаётся дополнительная сериализация или retry</td><td>Прогнать невалидный input и сравнить trace с успешным путём</td><td>Исправить error path и добавить отдельный критерий для него</td></tr><tr><td>Учебный тест зелёный, сервис всё ещё медленный</td><td>Модель не содержит HTTP, concurrency, backpressure или I/O</td><td>Воспроизвести один живой endpoint с реальным профилем</td><td>Сохранить модель как гипотезу, а ответ получить инструментом на нужной среде</td></tr></tbody></table>\n<h2>Порядок проверки</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Назовите endpoint, вход, status, body и условие, при котором проявляется проблема. Не начинайте с ярлыка «runtime D».</li><li><strong>Сузьте границу.</strong> Выберите один участок: временные объекты, GC, FFI или I/O. Остальные границы запишите как неизвестные или неисполненные.</li><li><strong>Сохраните контракт.</strong> Сравните успешный и ошибочный путь. Для успешного пути зафиксируйте body, для ошибочного — status и форму ошибки.</li><li><strong>Соберите базовый trace.</strong> Запишите вход, work units, allocation units, выбранный budget и версии compiler, DRuntime, ОС и библиотеки.</li><li><strong>Проверьте альтернативу.</strong> Измените только промежуточное состояние. Если меняются body или work units, сравнение allocation преждевременно.</li><li><strong>Измерьте реальную среду.</strong> Запустите подходящий profiler или benchmark на том же endpoint. Учебные units не подменяют байты, CPU time, pauses и latency.</li><li><strong>Проверьте отрицательный путь.</strong> Повторите тест с невалидным input, ошибкой FFI и отказом I/O, если эти границы входят в endpoint.</li><li><strong>Сформулируйте действие.</strong> Меняйте локальный код только при подтверждённой гипотезе. После изменения повторите базовый и отрицательный сценарии.</li></ol>\n<h2>Ограничения</h2>\n<p>Такой разбор не моделирует поведение всей системы. Он не описывает распределение памяти, алгоритм сборщика, scheduler, конкуренцию потоков, backpressure, сетевые повторы, сериализацию настоящего протокола, лимиты контейнера или нагрузку пользователей. Он не подтверждает production-результат и не заменяет нагрузочный тест.</p>\n<p><code>@nogc</code> ограничивает D-код, но не отменяет правила внешнего ABI и не запрещает другой части процесса запускать сборку. FFI boundary требует отдельного договора о типах и владении памятью. I/O boundary требует отдельного измерения ожидания и поведения при обрыве. Если эти условия неизвестны, честное действие — оставить их неизвестными и назначить проверку, а не заполнить пробел предположением.</p>\n<h2>Критерий готовности</h2>\n<p>Проверка закрыта, когда для одного входа сохранены базовый trace и trace после изменения; успешный ответ совпадает; error path проверен; выбранная причина подтверждена подходящим измерением; версии и условия запуска записаны; FFI и I/O имеют явный статус; а действие не опирается на учебные allocation units как на production-метрику. Если хотя бы одно условие не выполнено, результат — новая гипотеза, а не доказанное исправление.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dlang.org/spec/garbage.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Automatic Memory Management</a> — правила GC-аллокаций и ограничение <code>@nogc</code>.</li><li><a href=\"https://dlang.org/spec/function.html#nogc-functions\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: No-GC Functions</a> — проверяемые ограничения атрибута и его границы.</li><li><a href=\"https://dlang.org/spec/interfaceToC.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Interfacing to C</a> — calling convention, типы и ручные границы C-вызовов.</li></ul>"
|
||
}
|