dimhold.by
← Статьи

Таймаут, который ты не поставил, равен 135 секундам

У нас завис запрос, а вместе с ним завис поток, который его сделал. Я сказал то, что говорил и раньше: клиент без таймаута ждёт вечно, поэтому таймаут ставь. Совет этот я и сейчас дам. Просто я ни разу не проверил, верна ли его первая половина. Она неверна.

У ядра есть своё мнение о том, сколько ждать. Это мнение достаётся бесплатно.

Стенд это 2 машины и 1 адрес, который никуда не ведёт. Диапазон 192.0.2.0/24 зарезервирован под документацию, поэтому пакеты туда где-то по дороге выбрасываются. Ответ не приходит никогда. Значит, соединение с 192.0.2.1:80 делает ровно то, что я хотел засечь: ждёт ответа, которого не будет.

На сервере, где tcp_syn_retries стоит по умолчанию равным 6, оно ждало 135.3 секунды и упало с ETIMEDOUT. Это число не выбирала ни одна строка моего кода.

SYN уходит снова, каждая пауза вдвое длиннее 0 7 15 31 63 секунд с первого пакета модель: 127 замер: 135.3, дальше ETIMEDOUT
Каждая пауза вдвое длиннее предыдущей, поэтому последние 2 интервала и составляют почти весь итог. Удвоение ставит конец на 127 секунд. Машина отработала 135.3.

Удвоение это вся механика. Первый пакет уходит. Если никто не ответил, следующий идёт через 1 секунду, потом через 2, потом через 4 и так далее, поэтому при 6 повторах сумма пауз даёт 127 секунд. Модель стоит ровно столько, сколько стоят её предсказания, поэтому я поменял настройку и засёк заново. При 2 повторах ядро сдалось за 7.2 секунды против предсказанных 7. При 3 повторах вышло 19.4 секунды против предсказанных 15.

То есть на коротком конце модель верна, а на длинном промахивается сначала на 4.4, потом на 8.3 секунды. Первая догадка была про память ядра: Linux кеширует оценку времени оборота по адресу назначения. Испорченная оценка растянула бы каждый следующий прогон. Дело не в этом. Записи по этому адресу нет вообще, а после сброса кеша выходит те же 19.4 секунды, до десятых. Откуда берутся лишние секунды, я не знаю.

Половина, которая действительно вечность

Таймаут на соединение это та половина, которую тебе дают. Вторую не дают.

Я поднял локальный сервер, который принимает соединение, читает запрос и не отвечает никогда. Клиент, у которого ничего своего не выставлено, всё ещё ждал, когда я остановил его на 30 секундах. Повторять тут нечего и обнаруживать ядру нечего, потому что соединение здорово, а собеседник просто молчит. Ни у чего ниже библиотеки нет причины вмешаться. Тот же клиент с выставленными 2000 миллисекундами умер на 2.0 секундах.

соединение считает ядро ожидание ответа не считает никто сдалось на 135.3 с всё ещё ждал на 30 с и никто об этом не просил и ждал бы дальше 2.0 с при выставленных 2000 мс
2 фазы и 2 разных владельца часов. Фаза, которой ты не управляешь, ограничена. Фаза, которой управляешь, не ограничена ничем.

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

Где я споткнулся

Первый прогон занял 0.0 секунды и отчитался об успехе. Я запускал его дома. Моя домашняя сеть отвечает на 192.0.2.1 по 80 порту, на адрес, который по стандарту не принадлежит никому. На 10.255.255.1 она отвечает так же. В сети, где всегда кто-то снимает трубку, чёрной дыры для замера нет, поэтому замер переехал на машину с честным маршрутом. Честная версия моего первого результата звучит так: я померил свой роутер, а не ядро.

Чего я не проверял

Откуда берутся лишние 8.3 секунды. Те же ли числа на Windows при честном маршруте: дома проверить это не на чем. Оставляет ли библиотечный таймаут, сработавший посреди запроса, сокет в состоянии, которое пул потом переиспользует. Это и есть тот отказ, за который мне тревожно в проде. Он требует другого стенда. И повторы: любой знакомый мне клиент кладёт ретрай поверх таймаута, поэтому вызывающий ждёт произведение 2 настроек, а не любую из них.

Узкое утверждение такое. После того как соединение поднялось, время не считает никто. До того как оно поднялось, ядро считает примерно до 2 минут числом, которого никто в коде ни разу не видел.