Files
progcode/editorial/agent-rewrites/009.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

2 lines
15 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":9,"slug":"editorial-2027-10-practice-long-form-interview","title":"HTTP-таймауты: как разложить общий deadline на измеримые фазы","excerpt":"Запрос может завершиться таймаутом до ответа сервера или повторить запись после неясного результата. Разбираем deadline, фазы HTTP и проверку retry без догадок.","contentHtml":"<p>Запрос к API иногда отвечает за 900 мс, а клиент прекращает ждать через 800 мс. Пользователь видит ошибку, хотя сервер мог завершить операцию. Хуже случай с записью: клиент повторяет POST, не зная, успел ли первый запрос попасть в обработчик. Цена ошибки — потерянный результат, двойное изменение состояния и лишняя нагрузка на upstream.</p><p>Симптом не говорит, где потрачено время. Один общий timeout смешивает DNS, TCP, TLS, отправку тела, ожидание первого байта и чтение ответа. Повторная попытка получает новый полный бюджет и скрывает исходную причину. Тезис статьи прост: задайте один абсолютный deadline для операции, разложите его на наблюдаемые фазы и разрешайте retry только после проверки семантики метода.</p><h2>Deadline — граница операции</h2><p>Общий deadline отвечает на вопрос: до какого момента результат имеет смысл для вызывающего кода. Фазовый timeout отвечает на другой вопрос: сколько можно ждать конкретный переход. Если дать DNS, TCP, TLS и чтению по 800 мс каждому, запрос может жить несколько секунд. Если ограничить connect 50 мс, холодное разрешение имени превратит нормальный запрос в ложный отказ.</p><p>Храните абсолютный момент окончания или остаток бюджета. Перед каждой фазой вычисляйте <code>remaining = deadline - now</code>. Если остатка нет, завершайте операцию до следующего сетевого вызова. Новый независимый таймер после истечения deadline нарушает контракт вызывающего кода.</p><h2>Механизм: что измерять</h2><p>DNS показывает время разрешения имени. TCP connect — установление соединения. TLS handshake — защищённый обмен и проверку сертификата. Request write — передачу заголовков и тела. TTFB показывает ожидание первого байта, а read — получение остального ответа. Эти интервалы отвечают на разные вопросы, поэтому один счётчик total duration не заменяет их.</p><div class=\"table-scroll\"><table><caption>Фазы HTTP-запроса и сигналы</caption><thead><tr><th scope=\"col\">Фаза</th><th scope=\"col\">Что измеряем</th><th scope=\"col\">Типичный симптом</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>DNS</td><td>Разрешение имени</td><td>Медленный первый запрос</td><td>Проверить resolver, кэш и лимит DNS</td></tr><tr><td>TCP connect</td><td>Открытие соединения</td><td>Отказ до TLS</td><td>Проверить маршрут, pool и connect timeout</td></tr><tr><td>TLS handshake</td><td>Защищённый обмен</td><td>Соединение есть, ответа нет</td><td>Проверить цепочку, crypto и TLS budget</td></tr><tr><td>Request write</td><td>Отправка тела</td><td>Зависает upload</td><td>Проверить размер, backpressure и write timeout</td></tr><tr><td>TTFB/read</td><td>Ответ и тело</td><td>Клиент ждёт сервер</td><td>Сопоставить server time, read timeout и payload</td></tr></tbody></table></div><h2>Симптом → причина → проверка → действие</h2><table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Timeout без статуса HTTP</td><td>Истёк deadline до первого байта или клиент закрыл сокет</td><td>Сравнить timestamps DNS, connect, TLS, TTFB и server log</td><td>Разделить фазовые интервалы и не называть отказ HTTP-статусом</td></tr><tr><td>Первый запрос медленный, следующие быстрые</td><td>Cold DNS, TCP или TLS скрыты pool</td><td>Сравнить cold и warm соединения</td><td>Измерять установление соединения отдельно от чтения</td></tr><tr><td>POST повторяет изменение</td><td>Retry запущен без доказанной идемпотентности</td><td>Проверить метод, operation-id и результат первого вызова</td><td>Добавить ключ дедупликации или сначала читать статус операции</td></tr><tr><td>Каждый retry ждёт полный timeout</td><td>Попытки не делят общий deadline</td><td>Посчитать абсолютный deadline всего запроса</td><td>Передавать остаток бюджета, а не запускать новый полный таймер</td></tr><tr><td>В логах только total duration</td><td>Клиент не экспортирует фазы</td><td>Проверить API instrumentation и события pool</td><td>Фиксировать известную границу и не выдавать реконструкцию за факт</td></tr></tbody></table><h2>Повторная попытка не лечит неизвестный результат</h2><p>Timeout не доказывает, что сервер ничего не сделал. Для GET повтор обычно допустим, если ресурс читает актуальное состояние и клиент принимает возможное изменение данных между попытками. Для POST, PUT и DELETE решение зависит от семантики операции. Нужны идемпотентность, ключ дедупликации или endpoint для проверки статуса. Одного сетевого флага retry недостаточно.</p><p>Три попытки по 800 мс дают до 2,4 секунды ожидания без учёта задержек и одновременно утроенную нагрузку на upstream. Общий deadline должен охватывать все попытки. Если остатка мало, новая попытка не должна начинать работу. Для записи безопаснее вернуть состояние «результат неизвестен» и проверить operation-id, чем отправлять запрос повторно наугад.</p><figure><img src=\"/assets/editorial/2027/long-form-interview-2027-claim-evidence-map.svg\" alt=\"Карта HTTP-запроса: общий deadline проходит через DNS, TCP, TLS и чтение; каждая фаза получает только оставшееся время и собственный сигнал.\" loading=\"lazy\" /><figcaption>Общий бюджет проходит через фазы запроса. Диагноз должен ссылаться на сигнал конкретной фазы, а не только на слово timeout.</figcaption></figure><h2>Учебный пример без сети</h2><p>Функция ниже получает общий бюджет и длительности уже измеренных фаз. Она возвращает сумму, остаток и причину. Это учебный пример: он не открывает socket, не моделирует отмену запроса и не заменяет таймеры HTTP-клиента. Его граница — проверка арифметического инварианта отдельно от сети.</p><pre><code>import { allocateTimeoutBudget } from './upgrade-2027-10.mjs';\n\nconst within = allocateTimeoutBudget({\n totalMs: 800,\n dnsMs: 42,\n tlsMs: 88,\n requestMs: 510,\n});\nconst late = allocateTimeoutBudget({\n totalMs: 800,\n dnsMs: 120,\n tlsMs: 210,\n requestMs: 560,\n});\n\nconsole.log(within.ok, within.remainingMs);\nconsole.log(late.ok, late.reason, late.remainingMs);\n// true 160\n// false deadline-exceeded 0</code></pre><p>В первом вызове потрачено 640 мс, поэтому остаётся 160 мс. Во втором сумма равна 890 мс: функция возвращает <code>deadline-exceeded</code> и нулевой остаток. Реальный адаптер должен дополнительно отменять вложенные DNS, socket и чтение, иначе завершение функции не остановит работу сокета.</p><h2>Порядок проверки</h2><ol><li>Запишите абсолютный deadline и момент, когда клиент прекратил ожидание. Не начинайте диагностику с увеличения числа.</li><li>Добавьте DNS, TCP, TLS, write, TTFB и read в одну структурированную запись. Одинаковые имена фаз важнее названий полей конкретной библиотеки.</li><li>Проверьте, передаётся ли остаток бюджета на следующую фазу и между retry. Новый таймер не должен начинаться после истечения deadline.</li><li>Сопоставьте фазу отказа с HTTP-методом. Для записи проверьте идемпотентность, operation-id и способ узнать результат первого вызова.</li><li>Разделите connect, read и общий deadline в конфигурации. У каждого параметра должен быть владелец и тест на граничное значение.</li><li>Сравните cold и warm соединение. Pool может скрыть TLS в одном случае и показать его в другом.</li><li>Повторите проверку через реальный proxy и downstream. Локальная функция не доказывает поведение всей цепочки.</li></ol><h2>Что измерение не доказывает</h2><p>Большой TTFB не доказывает, что сервер медленный: время могло уйти на proxy, очередь или повторный TLS. Малый TTFB не гарантирует быструю загрузку всего тела. Клиентский timeout не равен HTTP-статусу: клиент может закрыть соединение, а сервер продолжить обработчик. Метрики фаз нужно связывать с серверным временем и размером ответа, но не подменять ими друг друга.</p><p>Если библиотека отдаёт только total duration, реконструированные интервалы нельзя называть фактами. Выберите instrumented adapter или события connection pool. До этого фиксируйте только известную границу: «клиент прекратил ждать через N мс». Неполное, но честное измерение лучше точного на вид вымысла.</p><h2>Ограничения и критерий готовности</h2><p>Подход не учитывает jitter часов, системные очереди, HTTP/2 multiplexing и детали конкретного proxy. Учебные числа не являются нормативными значениями для вашей сети. RFC описывает протокол, но не выбирает таймауты, retry policy или параметры клиента. Для боевой системы нужен адаптер с отменой всех вложенных операций и отдельная проверка безопасности повторов.</p><p>Материал готов к применению, если интеграционный тест для выбранного endpoint показывает фазы на cold и warm соединении, общий deadline ограничивает все попытки, а просроченная операция останавливает вложенное чтение. Для записи дополнительно нужен проверяемый operation-id: после timeout система может узнать, был ли первый вызов принят. Если хотя бы одна граница не наблюдается, результат следует считать недоказанным, а не успешным.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110 — HTTP Semantics</a> — официальная спецификация семантики HTTP; не задаёт таймауты конкретной библиотеки.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8446.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3</a> — официальная спецификация TLS 1.3; не определяет retry и application deadline.</li></ul>"}