Масштабирование и надёжность

Обновлено 4 мин чтения

Timeouts и Deadlines: почему сервис не должен ждать бесконечно

Страница перестала ждать, но сервер мог продолжить работу. Разбираемся, как согласовать сроки всей операции, отмену и действия при неизвестном результате.

Сервис расчёта доставки отвечает медленно, и страница оформления заказа продолжает ждать. Сотрудники видят, что сервер работает, ошибок почти нет. Покупатель видит вращающийся индикатор. Тем временем новые запросы занимают соединения и память, а предыдущие ещё не закончились. Формально всё живо, практически — пользоваться уже трудно.

Таймаут ограничивает ожидание. Но одной настройки «ждать не дольше» недостаточно: нужно определить, сколько времени есть у всего сценария, что произойдёт после истечения срока и останется ли на сервере работа, результат которой больше никому не нужен.

Клиент ждёт весь экран, а не каждый сервис отдельно

Чтобы показать варианты доставки, приложение может проверить адрес, запросить тариф и уточнить доступные интервалы. У каждого вызова свой предел ожидания. Если этапы выполняются последовательно и каждый получает полный новый таймаут, общее время растёт. Пользователь не обязан знать, что отдельные команды выполнили свои локальные договорённости.

Deadline — срок завершения конкретной операции. В отличие от нового таймера на каждом шаге, он позволяет учитывать уже потраченное время. Следующей зависимости передают оставшийся бюджет, а не столько же, сколько было у первой. Локальный таймаут при этом может быть ещё короче, если функция необязательная или дальше нужно время на обработку ответа.

Предположим, продукт отвёл на ответ две секунды, а полсекунды уже ушло на предыдущие шаги. Следующий участник не должен начинать ещё одно двухсекундное ожидание. Это условный пример. Рабочие сроки определяются по сценарию и измерениям.

При параллельных вызовах бюджет не обязательно делится поровну между всеми участниками. Они расходуют одно и то же оставшееся время. Важно оставить возможность собрать ответ и вернуть его клиенту, а не завершить последний внутренний вызов ровно в момент, когда внешний запрос уже закрыт.

Что именно измеряет таймер

Настройка с названием timeout может ограничивать установление соединения, ожидание очередного блока данных или весь запрос. Это не одно и то же. Поиск сетевого адреса, защищённое соединение и ожидание свободного подключения могут оказаться за пределами таймера.

Проверьте весь путь запроса: где начинается отсчёт? Особенно после запуска новых экземпляров: повторно используемое соединение и первое подключение к зависимости могут занимать разное время. Короткий тест на прогретой системе этого не покажет.

Само значение тоже нельзя выбирать только по среднему времени ответа. Среднее скрывает редкие долгие операции. Нужны распределение задержек, нормальные пики нагрузки и допустимое число случаев, когда мы прервём ожидание ответа, который ещё мог прийти. Слишком короткий срок создаёт лишние ошибки и повторы; слишком длинный позволяет ожиданию занять общие ресурсы.

Ожидание закончилось. Работа могла продолжиться

Клиент закрыл соединение — это ещё не команда базе данных прекратить расчёт. Обработчик может продолжить обращаться к соседним сервисам, занимать соединение или строить отчёт. При высокой нагрузке такие оставшиеся задачи мешают обслуживать уже новых пользователей.

Отмена должна доходить до тех компонентов, которые способны её исполнить. Код должен замечать сигнал, прекращать ненужную работу и освобождать ресурсы. В документации gRPC отдельно объясняется, что библиотека не может произвольно остановить прикладной обработчик: участие приложения обязательно.

При этом отмена не равна откату. Если операция уже сохранила данные или внешний партнёр принял заявку, закрытие запроса не возвращает систему в прежнее состояние. Сообщение «время ожидания истекло» говорит об отсутствии своевременного ответа, а не доказывает, что действие не произошло.

Для оформления перевозки поэтому нужен идентификатор операции и способ узнать её состояние. Повторить создание новой заявки только потому, что ответ потерялся, — риск заказать перевозку дважды. Здесь таймауты работают вместе с идемпотентностью, а не заменяют её.

Не всякую работу нужно отменять вместе с экраном

Если клиент уже попросил сформировать большой отчёт и система надёжно приняла задачу, закрытие вкладки не обязано её отменять. У такой задачи собственный жизненный цикл: очередь, состояние выполнения, срок актуальности и возможность получить результат позже. Обещание «задача принята» отличается от обещания «всё уже сделано».

Проблема возникает, когда обычный запрос неожиданно становится фоновой задачей, но никто не хранит её состояние. Пользователь не знает, продолжать ждать или запускать заново, а поддержка не может объяснить, что произошло. Решение о переходе в фон нужно принимать явно, с подтверждением надёжного приёма работы.

То же относится к частичному ответу. Варианты доставки без приблизительного времени прибытия иногда можно показать. Подтвердить перевозку без ответа перевозчика — уже другое бизнес-обещание. Допустимую неполноту определяет продукт, а не обработчик исключений.

Проверка, которую стоит провести до пика нагрузки

Замедлите одну зависимость и посмотрите не только на ошибку в браузере. Укладывается ли весь запрос в отведённое время? Освобождаются ли занятые ресурсы? Продолжает ли нижний сервис работать после отмены? Не начинает ли клиент новую попытку, когда общий срок уже истёк?

Отдельно воспроизведите потерю ответа после успешной записи. Интерфейс должен отличать подтверждённый отказ от неизвестного результата и давать путь к проверке состояния. Для руководителя это важнее, чем одинаковое значение timeout во всех настройках.

В наблюдении за системой стоит разделять очередь перед выполнением, время самой зависимости и работу после ухода клиента. Тогда рост таймаутов становится поводом найти конкретное ограничение. Простое увеличение ожидания иногда оправданно, но без этой диагностики оно лишь делает отказ более медленным.