diff --git a/editorial/agent-rewrites/223.json b/editorial/agent-rewrites/223.json index 9408e7f..91b2c2a 100644 --- a/editorial/agent-rewrites/223.json +++ b/editorial/agent-rewrites/223.json @@ -1,7 +1,7 @@ { "index": 223, "slug": "editorial-2021-10-field-d-runtime-service", - "title": "Пауза в D-сервисе: как отделить allocation от GC, FFI и I/O", - "excerpt": "Редкий пик latency в D-сервисе нельзя объяснить одним словом «runtime». Разбираем один request: сохраняем результат и error path, отделяем allocation от условной GC-границы и проверяем FFI/I/O до изменения конфигурации.", - "contentHtml": "
Сервис отвечает быстро на обычном запросе, но иногда один request выходит за ожидаемое время. В trace рядом видны обработка входа, вычисление и внешние границы. Команда замечает рост временных объектов и сразу связывает его с паузой GC. Другой инженер обвиняет FFI или запись аудита, хотя вызов ещё не доказан. Третий меняет настройки runtime до того, как сохранил исходный результат.
\nЦена ошибки — потерянная причинность. После нескольких изменений нельзя понять, что изменило latency, а что только скрыло симптом. Глобальное отключение GC может увеличить память и не устранить блокировку на I/O. Оптимизация промежуточного буфера может нарушить error response. Диагностика должна сначала сохранить контракт handler, а затем сузить одну проверяемую гипотезу.
\nТезис статьи простой: allocation, работа GC, FFI и I/O — разные наблюдаемые границы. В учебном примере ниже request получает result, stage trace и условные allocation units. Эти units не являются байтами, миллисекундами или данными production. Они нужны только для того, чтобы сравнить два пути при одинаковом результате. Реальный вывод о паузе появляется после профилирования конкретного процесса в зафиксированной среде.
\nРазделите request на decode, validation, compute и encode. Decode нормализует вход. Validation решает, можно ли выполнять операцию. Compute получает промежуточное состояние и считает результат. Encode формирует success или error response. Такой порядок делает результат проверяемым: если после оптимизации изменился body или status, обсуждать экономию allocation рано.
\nРядом с основным путём находятся внешние границы. FFI означает намерение вызвать foreign function. I/O означает намерение записать или прочитать данные. Запись границы в trace не доказывает, что вызов состоялся. Для реального вызова нужны owner, формат аргументов, lifetime, ownership, error contract, timeout и способ отмены. ABI помогает описать совместимость вызова, но не сообщает стоимость конкретной функции.
\nGC имеет ещё одну границу. D может выделять память в управляемой куче, а collector возвращает неиспользуемые объекты. Срабатывание сбора зависит от состояния процесса и настроек. В trace приложения нужно отличать факт выделения, факт наблюдаемого сбора и гипотезу о влиянии сбора на request. Эти факты нельзя заменить одним label вроде gc-boundary.
Рассмотрим вход {"requestId":"runtime-d-training-42","operation":"add","left":19,"right":23}. Обработчик должен вернуть status 200 и body {"requestId":"runtime-d-training-42","total":42}. Вариант allocation-heavy создаёт несколько промежуточных структур. Вариант reuse повторно использует локальное состояние. В модели оба выполняют 9 work units и возвращают один body.
Для сравнения зададим budget 6. Heavy получает 12 условных allocation units и пересекает budget. Reuse получает 3 units. В модельной записи heavy получает отметку gc-boundary, потому что его счётчик пересёк порог 8. Это не сообщение о том, что collector действительно остановил поток. Это только сигнал: в реальном сервисе стоит проверить allocation и GC отдельным инструментом.
struct Request {\n string requestId;\n string operation;\n int left;\n int right;\n}\n\nstruct Response {\n string requestId;\n int total;\n}\n\nResponse handle(Request request) {\n if (request.operation != "add") {\n throw new ValidationError("unsupported operation");\n }\n\n return Response(request.requestId, request.left + request.right);\n}\nКод выше — сокращённый учебный фрагмент. Он показывает контракт результата, а не устройство конкретного сервиса и не гарантирует отсутствие allocation. Реальный D compiler может оптимизировать код иначе. Вызов serializer, логгера, базы или foreign function в этот фрагмент не входит. Поэтому нельзя по нему объявлять latency или выбирать флаг runtime.
\nОтрицательный путь обязателен. Для входа с operation: "divide" validation должна вернуть status 400 и error body. Такой вход не должен доходить до compute, FFI или I/O. Если после оптимизации invalid input стал success, исчез или начал вызывать внешнюю систему, уменьшение units не имеет значения: изменился контракт ошибки.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Редкий latency spike совпал с ростом allocation | Выделение ошибочно принято за паузу collector | Сохранить process profile, версию D compiler/DRuntime, вход и распределение времени | Не менять GC по одному trace; проверить allocation и pause раздельно |
В trace есть gc-boundary | Порог модели выдан за факт работы GC | Проверить источник записи и единицу измерения | Назвать запись условным сигналом и выбрать реальный profiler |
| Есть FFI или I/O boundary | Граница записана как intent, но вызов не подтверждён | Проверить invocation, owner, аргументы, timeout и error | Добавить отдельный contract test; не обвинять зависимость без вызова |
| После reuse body изменился | Оптимизация затронула handler contract | Сравнить status, headers, success body и error body на одинаковых входах | Остановить оптимизацию и вернуть равный результат |
| Invalid input проходит compute | Сломана validation boundary или проверяется только happy path | Повторить тот же invalid sample и посмотреть stage trace | Восстановить error route до измерения allocation |
| Настройка runtime улучшила один прогон | Изменилось сразу несколько условий эксперимента | Сравнить build, платформу, лимиты, вход, concurrency и метод | Откатить широкий change и повторить одну гипотезу |
Начните с результата. Для valid input запишите status, body и request id. Для invalid input запишите status, error code и сообщение, достаточное для диагностики. Уберите секреты и персональные данные до сохранения trace. Результат связывает стадии с внешним контрактом и не даёт считать любой меньший счётчик улучшением.
\nЗатем проверьте порядок стадий. Valid request должен пройти decode, validation, compute и encode. Invalid request должен пройти decode, validation-error и encode-error. FFI и I/O должны быть либо явно вызваны с подтверждаемым результатом, либо отмечены как неисполненные намерения. Если trace смешивает эти случаи, сначала исправьте наблюдаемость.
\nПосле этого сравните два варианта только при равных условиях. Вход, response, status и work units должны совпадать. Меняется одна гипотеза: временное состояние на выбранном участке. Если одновременно изменились serializer, формат ответа и runtime flags, эксперимент не изолирован. Его результат нельзя приписать reuse.
\nСлово «пауза» требует фактического временного сигнала. Нужны timestamps или профиль с понятной методикой, а также связь участка профиля с request. Allocation count без времени не отвечает на вопрос о latency. GC-настройка без повторяемого входа не отвечает на вопрос о причине. Совпадение двух графиков во времени остаётся гипотезой, пока независимая проверка не разделит их.
\nЕсли profile не показывает GC в момент spike, не нужно доказывать первоначальную версию. Проверьте очередь, блокировку, syscall, FFI и I/O по отдельности. Если FFI entry есть только как invocation: not-performed, зависимость не является подтверждённой причиной. Если время растёт на invalid path, сначала исследуйте validation и формирование ошибки.
Если reuse уменьшил units, но изменил body, верните contract. Если body совпал, а latency не изменилась, это нормальный результат: учебный allocation signal мог не быть bottleneck. Если глобальная настройка дала улучшение только на одном наборе данных, остановите перенос вывода на другие входы. Отрицательный результат экономит больше времени, чем уверенная, но неподтверждённая причина.
\nD specification описывает автоматическое управление памятью, доступные ограничения и взаимодействие с foreign code. Она не профилирует ваш процесс. Атрибут @nogc ограничивает вызовы, которые могут выделять память через GC, но сам по себе не делает безопасными FFI, I/O или внешние allocator-ы. D ABI описывает форму взаимодействия с C ABI целевой системы, но не ownership, блокировку и latency конкретной функции.
Учебные значения 3, 12, 6 и 8 units не имеют единицы времени. Диаграмма и код не являются benchmark, нагрузочным тестом, отчётом об инциденте или результатом production. Один trace не описывает другие устройства, входы, версии compiler, режимы линковки, лимиты процесса и concurrency. Нельзя строить SLA из этого примера и нельзя переносить его verdict между runtime.
\nОтключение GC — отдельное архитектурное решение. Оно может повлиять на память, lifetime и работу других потоков. Любое такое изменение требует реального профиля, теста contract и плана возврата. Название @nogc не является разрешением убрать collector вокруг кода, который вызывает неизвестные функции или работает с внешней памятью.
Разбор готов, если выполнены пять условий. Для valid и invalid входов сохранены ожидаемые result и error route. Stage trace показывает, где заканчивается handler и начинаются внешние границы. Сравниваемые варианты имеют одинаковые input, status, body и work units. Каждый вывод о времени опирается на фактический profile с описанными условиями. После правки повторный прогон подтверждает contract и выбранное наблюдение.
\nЕсли profile не подтверждает GC, итогом должно быть «GC не доказан», а не новая догадка. Если вызов FFI или I/O не состоялся, итогом должно быть «граница не проверена», а не обвинение зависимости. Если результат различается, итогом должна быть остановка оптимизации. Такой критерий закрывает именно диагностику, а не желание назвать сервис быстрым.
\n@nogc и его границы.Сервис отвечает быстро на обычном запросе, но иногда один запрос выходит за ожидаемое время. В trace рядом видны обработка входа, вычисление и внешние границы. Команда замечает рост временных объектов и сразу связывает его с паузой GC. Другой инженер обвиняет FFI или запись аудита, хотя вызов ещё не подтверждён. Третий меняет настройки runtime, не сохранив исходный результат.
\nЦена ошибки — потерянная причинность. После нескольких изменений нельзя понять, что изменило latency, а что только скрыло симптом. Глобальное отключение GC может увеличить память и не устранить блокировку на I/O. Оптимизация промежуточного буфера может нарушить error response. Диагностика должна сначала сохранить контракт handler, а затем сузить одну проверяемую гипотезу.
\nТезис статьи простой: allocation, работа GC, FFI и I/O — разные наблюдаемые границы. В учебном примере ниже request получает result, stage trace и условные allocation units. Эти units не являются байтами, миллисекундами или данными production. Они нужны только для того, чтобы сравнить два пути при одинаковом результате. Реальный вывод о паузе появляется после профилирования конкретного процесса в зафиксированной среде.
\nРазделите request на decode, validation, compute и encode. Decode нормализует вход. Validation решает, можно ли выполнять операцию. Compute получает промежуточное состояние и считает результат. Encode формирует success или error response. Такой порядок делает результат проверяемым: если после оптимизации изменился body или status, обсуждать экономию allocation рано.
\nРядом с основным путём находятся внешние границы. FFI означает намерение вызвать foreign function. I/O означает намерение записать или прочитать данные. Запись границы в trace не доказывает, что вызов состоялся. Для реального вызова нужны owner, формат аргументов, lifetime, ownership, error contract, timeout и способ отмены. ABI помогает описать совместимость вызова, но не сообщает стоимость конкретной функции.
\nGC имеет ещё одну границу. D поддерживает необязательное автоматическое управление памятью: collector может возвращать неиспользуемую память в пул. Срабатывание сбора зависит от состояния процесса и настроек. В trace приложения нужно отличать факт выделения, факт наблюдаемого сбора и гипотезу о влиянии сбора на запрос. Эти факты нельзя заменить одним label вроде gc-boundary.
Рассмотрим вход {"requestId":"runtime-d-training-42","operation":"add","left":19,"right":23}. Обработчик должен вернуть status 200 и body {"requestId":"runtime-d-training-42","total":42}. Вариант allocation-heavy создаёт несколько промежуточных структур. Вариант reuse повторно использует локальное состояние. В модели оба выполняют 9 work units и возвращают один body.
Для сравнения зададим budget 6. Heavy получает 12 условных allocation units и пересекает budget. Reuse получает 3 units. В модельной записи heavy получает отметку gc-boundary, потому что его счётчик пересёк порог 8. Это не сообщение о том, что collector действительно остановил поток. Это только сигнал: в реальном сервисе стоит проверить allocation и GC отдельным инструментом.
enum Status : int\n{\n ok = 200,\n badRequest = 400\n}\n\nstruct Request\n{\n string requestId;\n string operation;\n int left;\n int right;\n}\n\nstruct Response\n{\n string requestId;\n int total;\n}\n\nStatus handle(const ref Request request, out Response response)\n{\n if (request.operation != "add")\n {\n response = Response(request.requestId, 0);\n return Status.badRequest;\n }\n\n response = Response(request.requestId, request.left + request.right);\n return Status.ok;\n}\nКод выше — минимальный учебный фрагмент на D. Он показывает контракт результата и две ветки status, а не устройство конкретного сервиса. Он не доказывает отсутствие allocation: это зависит от входных данных, compiler, DRuntime и окружающего кода. Вызов serializer, логгера, базы или foreign function в фрагмент не входит, поэтому по нему нельзя объявлять latency или выбирать флаг runtime.
\nОтрицательный путь обязателен. Для входа с operation: "divide" validation должна вернуть status 400 и передать error code на внешний adapter. Такой вход не должен доходить до compute, FFI или I/O. Если после оптимизации invalid input стал success, исчез или начал вызывать внешнюю систему, уменьшение units не имеет значения: изменился контракт ошибки.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Редкий latency spike совпал с ростом allocation | Выделение ошибочно принято за паузу collector | Сохранить process profile, версию D compiler/DRuntime, вход и распределение времени | Не менять GC по одному trace; проверить allocation и pause раздельно |
В trace есть gc-boundary | Порог модели выдан за факт работы GC | Проверить источник записи и единицу измерения | Назвать запись условным сигналом и выбрать реальный profiler |
| Есть FFI или I/O boundary | Граница записана как intent, но вызов не подтверждён | Проверить invocation, owner, аргументы, timeout и error | Добавить отдельный contract test; не обвинять зависимость без вызова |
| После reuse body изменился | Оптимизация затронула handler contract | Сравнить status, headers, success body и error body на одинаковых входах | Остановить оптимизацию и вернуть равный результат |
| Invalid input проходит compute | Сломана validation boundary или проверяется только happy path | Повторить тот же invalid sample и посмотреть stage trace | Восстановить error route до измерения allocation |
| Настройка runtime улучшила один прогон | Изменилось сразу несколько условий эксперимента | Сравнить build, платформу, лимиты, вход, concurrency и метод | Откатить широкий change и повторить одну гипотезу |
Начните с результата. Для valid input запишите status, body и request id. Для invalid input запишите status, error code и сообщение, достаточное для диагностики. Уберите секреты и персональные данные до сохранения trace. Результат связывает стадии с внешним контрактом и не даёт считать любой меньший счётчик улучшением.
\nЗатем проверьте порядок стадий. Valid request должен пройти decode, validation, compute и encode. Invalid request должен пройти decode, validation-error и encode-error. FFI и I/O должны быть либо явно вызваны с подтверждаемым результатом, либо отмечены как неисполненные намерения. Если trace смешивает эти случаи, сначала исправьте наблюдаемость.
\nПосле этого сравните два варианта только при равных условиях. Вход, response, status и work units должны совпадать. Меняется одна гипотеза: временное состояние на выбранном участке. Если одновременно изменились serializer, формат ответа и runtime flags, эксперимент не изолирован. Его результат нельзя приписать reuse.
\nСлово «пауза» требует фактического временного сигнала. Нужны timestamps или профиль с понятной методикой, а также связь участка профиля с request. Allocation count без времени не отвечает на вопрос о latency. GC-настройка без повторяемого входа не отвечает на вопрос о причине. Совпадение двух графиков во времени остаётся гипотезой, пока независимая проверка не разделит их.
\nЕсли profile не показывает GC в момент spike, не нужно доказывать первоначальную версию. Проверьте очередь, блокировку, syscall, FFI и I/O по отдельности. Если FFI entry есть только как invocation: not-performed, зависимость не является подтверждённой причиной. Если время растёт на invalid path, сначала исследуйте validation и формирование ошибки.
Если reuse уменьшил units, но изменил body, верните contract. Если body совпал, а latency не изменилась, это нормальный результат: учебный allocation signal мог не быть bottleneck. Если глобальная настройка дала улучшение только на одном наборе данных, остановите перенос вывода на другие входы. Отрицательный результат экономит больше времени, чем уверенная, но неподтверждённая причина.
\nD specification описывает автоматическое управление памятью, доступные ограничения и взаимодействие с foreign code. Она не профилирует ваш процесс. Функция с атрибутом @nogc не выполняет GC-аллокaции, а компилятор запрещает в ней ряд потенциально аллоцирующих операций и вызовы функций без @nogc. Это не запрет внешнего malloc и не доказательство безопасности FFI, I/O или внешнего allocator-а. D ABI описывает форму взаимодействия с C ABI целевой системы, но не ownership, блокировку и latency конкретной функции.
Учебные значения 3, 12, 6 и 8 units не имеют единицы времени. Диаграмма и код не являются benchmark, нагрузочным тестом, отчётом об инциденте или результатом production. Один trace не описывает другие устройства, входы, версии compiler, режимы линковки, лимиты процесса и concurrency. Нельзя строить SLA из этого примера и нельзя переносить его verdict между runtime.
\nОтключение GC — отдельное архитектурное решение. Оно может повлиять на память, lifetime и работу других потоков. Любое такое изменение требует реального профиля, теста contract и плана возврата. Название @nogc не является разрешением убрать collector вокруг кода, который вызывает неизвестные функции или работает с внешней памятью.
Разбор готов, если выполнены пять условий. Для valid и invalid входов сохранены ожидаемые result и error route. Stage trace показывает, где заканчивается handler и начинаются внешние границы. Сравниваемые варианты имеют одинаковые input, status, body и work units. Каждый вывод о времени опирается на фактический profile с описанными условиями. После правки повторный прогон подтверждает contract и выбранное наблюдение.
\nЕсли profile не подтверждает GC, итогом должно быть «GC не доказан», а не новая догадка. Если вызов FFI или I/O не состоялся, итогом должно быть «граница не проверена», а не обвинение зависимости. Если результат различается, итогом должна быть остановка оптимизации. Такой критерий закрывает именно диагностику, а не желание назвать сервис быстрым.
\n@nogc и его границы.