Files
progcode/editorial/agent-rewrites/068.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 68,
"slug": "editorial-2026-02-mechanism-resilience",
"title": "Механика устойчивости: сначала ограничьте каскад, потом настраивайте retry",
"excerpt": "Timeout не ограничивает объём работы. Устойчивый маршрут задаёт одного владельца retry, конечный fan-out, именованный fallback и точку остановки до расширения каскада.",
"contentHtml": "<p>Внешний сервис начинает отвечать медленно. На входе растёт очередь. Gateway повторяет запрос по timeout, адаптер повторяет его ещё раз, а выбор реплики отправляет работу на несколько узлов. Каждый механизм выглядит разумно отдельно. Вместе они увеличивают поток к уже перегруженной зависимости. Пользователь получает задержку или ошибку. Оператор видит несколько причин и не знает, где остановить цепочку. Цена ошибки — не один лишний запрос. Это занятые соединения, память под незавершённые операции и потеря мощности именно в момент отказа.</p>\n<p>Тезис статьи простой: устойчивость начинается с границы дополнительной работы. Сначала нужно определить логический запрос, одного владельца retry, максимальное число попыток, ширину выбора реплики и результат после исчерпания бюджета. Только после этого имеют смысл timeout, backoff и fallback. Если каждый слой может запустить следующую попытку, система не имеет одного механизма восстановления. Она имеет усилитель нагрузки.</p>\n<p>Ниже используется учебная fixed-модель. В ней два логических запроса, один именованный временный timeout, две попытки на запрос, fan-out равен одному и fallback имеет одну условную единицу работы. Модель не открывает сеть, не измеряет latency, не знает реальных реплик и не подтверждает production-устойчивость. Она проверяет только структуру решения и умеет остановиться, когда структура нарушена.</p>\n<figure><img src=\"/assets/editorial/2026/resilience-2026-degradation-mode-matrix.svg\" alt=\"Матрица режимов деградации: timeout, ограниченная попытка, именованный fallback и остановка каскада\" loading=\"lazy\" /><figcaption>Учебная матрица разделяет исход, разрешённое действие, добавленную работу и точку остановки. Она не описывает топологию настоящего сервиса.</figcaption></figure>\n<h2>Механизм: считать логическую работу</h2>\n<p>Считать нужно не только HTTP-запросы. Единица анализа — логический запрос пользователя или вызывающего сервиса. Он может породить несколько исходящих действий. Для каждого действия запишите причину запуска и право запускать следующее действие. Это сразу разделяет последовательный retry и fan-out.</p>\n<p>Retry запускает новый вызов после определённого исхода. Fan-out создаёт несколько направлений для одной попытки. Репликация задаёт множество доступных имён, но не обязана выбирать их все. Fallback меняет контракт результата. Он может вернуть неполный ответ, но не должен незаметно создавать собственную политику повторов. Limit point запрещает следующий переход. Если он срабатывает после fan-out, он уже не ограничивает первую волну работы.</p>\n<div class=\"table-scroll\"><table><caption>Величины, которые должны иметь отдельную границу</caption><thead><tr><th scope=\"col\">Величина</th><th scope=\"col\">Учебное значение</th><th scope=\"col\">Что проверяет</th><th scope=\"col\">Ошибка при смешении</th></tr></thead><tbody><tr><td>logical requests</td><td>2</td><td>единицу сравнения</td><td>сравнение разных объёмов работы</td></tr><tr><td>retry owner</td><td>edge</td><td>кто имеет право повторить вызов</td><td>два слоя запускают вложенные повторы</td></tr><tr><td>max attempts</td><td>2</td><td>конечный предел попыток</td><td>retry превращается в цикл</td></tr><tr><td>max fan-out</td><td>1</td><td>одно имя реплики за попытку</td><td>один запрос размножается по пулу</td></tr><tr><td>fallback work</td><td>1</td><td>стоимость деградированного результата</td><td>запасной путь считают бесплатным</td></tr><tr><td>limit point</td><td>до route expansion</td><td>момент запрета следующего действия</td><td>счётчик фиксирует проблему постфактум</td></tr></tbody></table></div>\n<p>В bounded-cascade-v1 есть три возможных имени реплики, но на одну попытку выбирается только одно. Для двух логических запросов и максимум двух попыток верхняя граница учебной работы равна четырём route attempts. Это не QPS, не прогноз CPU и не оценка времени ответа. Она нужна, чтобы проверить, что новая защита не добавила скрытую ветвь.</p>\n<h2>Пример и отрицательный путь</h2>\n<p>Положительный сценарий начинается с logical-01. Он обращается к primary-a и получает temporary-timeout. Единственный владелец retry, edge, списывает одну попытку. Следующий вызов идёт к primary-b. Второй логический запрос получает fixed-optional-result-unavailable; вместо нового поиска он возвращает named-summary с результатом fixed-degraded-summary. У fallback нет своего retry. После нулевого бюджета limit point возвращает фиксированный деградированный результат и не расширяет маршрут.</p>\n<pre><code>const candidate = createFixedResilienceScenario('retry-amplification-v1');\nconst result = assessFixedResilienceScenario(candidate);\nconsole.log({ status: result.status, reason: result.reasons[0], handoff: result.handoff });\n// stop-retry-amplification\n// retry-must-have-one-owner-and-a-fixed-two-attempt-bound</code></pre>\n<p>Этот фрагмент показывает отрицательный путь. В retry-amplification-v1 включены два владельца retry: edge и adapter. Проверка не выбирает «лучший» слой и не моделирует задержку. Она отказывает сразу. Причина сильнее локальной настройки: у логического запроса должно быть одно право создавать следующую попытку и конечный предел. Такой отказ полезен в review. Следующее действие однозначно: убрать дублирование или уточнить границу ответственности. Нельзя компенсировать конфликт ещё одним timeout.</p>\n<p>Тот же fail-closed подход нужен для неизвестного сценария, неименованного fallback и неограниченного fan-out. Если вход нельзя сопоставить с fixed record, проверка не должна подставлять значения по умолчанию. Неизвестное поведение — это повод остановиться, а не разрешить самый широкий маршрут.</p>\n<h2>Симптомы и проверка</h2>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После timeout растёт число исходящих вызовов</td><td>retry есть на gateway и в адаптере</td><td>найти owner в каждой политике</td><td>оставить одного owner, остальные слои сделать pass-through</td></tr><tr><td>Одна операция видна на нескольких репликах</td><td>fan-out не ограничен на попытку</td><td>посчитать выбранные имена в trace</td><td>задать числовой maxFanout и проверять его до запуска</td></tr><tr><td>Fallback увеличивает задержку</td><td>запасной путь сам делает поиск или retry</td><td>развернуть его переходы и work units</td><td>назвать стоимость, убрать вложенный retry или выключить fallback</td></tr><tr><td>Лимит есть в конфигурации, но каскад уже расширился</td><td>limit point стоит после route expansion</td><td>сравнить порядок trace transitions</td><td>перенести ограничение перед созданием следующего маршрута</td></tr><tr><td>Результаты тестов нельзя сравнить</td><td>изменились нагрузка или failure injection</td><td>сверить logical load и ключ сценария</td><td>вернуться к тому же baseline или объявить сравнение недействительным</td></tr></tbody></table>\n<h2>Почему timeout и статус ошибки не решают задачу</h2>\n<p>Timeout отвечает на вопрос «сколько ждать этот вызов». Он не отвечает на вопрос «кто имеет право создать следующий». Короткий timeout может даже повысить нагрузку, если каждый слой интерпретирует его как разрешение на повтор. Статус 503 сообщает, что сервис временно не готов обработать запрос, но не выбирает retry owner и не доказывает идемпотентность действия. Retry-After задаёт подсказку для времени ожидания, а не общий бюджет каскада.</p>\n<p>Backoff тоже не является лимитом. Он раздвигает попытки во времени, но оставляет их количество и владельца. Если несколько слоёв применяют независимый backoff, суммарное число переходов остаётся неясным. Поэтому сначала фиксируйте право и число попыток. Затем выбирайте расписание. Для операций с побочными эффектами отдельно проверяйте идемпотентность и компенсацию. В этой учебной модели таких эффектов нет.</p>\n<h2>Порядок действий</h2>\n<ol><li>Назовите логический запрос и перечислите все исходящие действия, которые он может породить.</li><li>Назначьте одного владельца retry. Запретите остальным слоям запускать второй цикл.</li><li>Назовите retryable outcomes и задайте конечное число попыток.</li><li>Запишите доступные реплики, но ограничьте число выбранных имён на одну попытку.</li><li>Опишите fallback как отдельный результат с именем, условием и стоимостью.</li><li>Поставьте limit point до retry, fallback и route expansion, если эти переходы создают новую работу.</li><li>Добавьте trace transition для расхода бюджета и terminal action после нуля.</li><li>Прогоните положительный и отрицательный fixed-сценарии на одинаковой логической нагрузке.</li><li>Только после этого перенесите контракт в настоящий тест с безопасными данными, отменой и наблюдением.</li></ol>\n<h2>Ограничения модели</h2>\n<p>Модель не знает о реальной ёмкости, очередях, дедлайнах, cancellation, сетевых сбоях, распределённом состоянии, правах доступа или пользовательском ущербе. Числа два и один выбраны для учебной проверки. Их нельзя переносить в конфигурацию сервиса. Положительный статус означает, что fixed-карточка удовлетворяет формальным ограничениям. Он не означает availability, recovery time, безопасный rollout или допустимую бизнес-деградацию.</p>\n<p>Отдельно ограничен сам способ сравнения. Нельзя сопоставлять сценарий с двумя логическими запросами со сценарием с другой нагрузкой и делать вывод о причине. Нельзя менять тип failure injection и сохранять прежний baseline. Нельзя считать trace доказательством скорости: trace показывает порядок и факт переходов, а latency требует измерения. Если один из этих фактов неизвестен, правильный результат — остановка и новый вопрос, а не расширение предположений.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Механизм готов к отдельному инженерному тесту, если для одного фиксированного сценария можно показать пять вещей: один retry owner; конечный maxAttempts; числовой maxFanout; fallback с именем, условием и запретом вложенного retry; limit point до расширения маршрута. Trace должен показывать расход бюджета и один terminal action после его исчерпания. Тест должен пройти положительный record и отклонить retry-amplification-v1 с причиной, которую можно прочитать без догадки.</p>\n<p>Если хотя бы один пункт не выполняется, не добавляйте новую защиту. Сначала уточните владельца, границу или контракт результата. После этого можно отдельно планировать нагрузочный тест и проверку в среде с реальной телеметрией. Эта статья даёт механизм чтения каскада, а не обещание, что учебная схема переживёт отказ в production.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://sre.google/sre-book/addressing-cascading-failures/\" target=\"_blank\" rel=\"noopener noreferrer\">Google SRE Book: Addressing Cascading Failures</a> — официальный разбор положительной обратной связи, перегрузки и усиления эффекта retry. Источник не измеряет эту fixed-модель и не подтверждает её production-поведение.</li><li><a href=\"https://github.com/grpc/proposal/blob/master/A6-client-retries.md\" target=\"_blank\" rel=\"noopener noreferrer\">gRPC A6: Client Retries</a> — официальный design proposal с параметрами maxAttempts, backoff, throttling и pushback. Документ относится к gRPC и не задаёт политику для произвольного HTTP-сервиса.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — стандартная семантика 503 и Retry-After. RFC не определяет владельца retry, идемпотентность конкретной операции или предел fan-out.</li></ul>"
}