У нас завис запрос, а вместе с ним завис поток, который его сделал. Я сказал то, что говорил и раньше: клиент без таймаута ждёт вечно, поэтому таймаут ставь. Совет этот я и сейчас дам. Просто я ни разу не проверил, верна ли его первая половина. Она неверна.
У ядра есть своё мнение о том, сколько ждать. Это мнение достаётся бесплатно.
Стенд это 2 машины и 1 адрес, который никуда не ведёт. Диапазон 192.0.2.0/24 зарезервирован под документацию, поэтому пакеты туда где-то по дороге выбрасываются. Ответ не приходит никогда. Значит, соединение с 192.0.2.1:80 делает ровно то, что я хотел засечь: ждёт ответа, которого не будет.
На сервере, где tcp_syn_retries стоит по умолчанию равным 6, оно ждало 135.3 секунды и упало с ETIMEDOUT. Это число не выбирала ни одна строка моего кода.
Удвоение это вся механика. Первый пакет уходит. Если никто не ответил, следующий идёт через 1 секунду, потом через 2, потом через 4 и так далее, поэтому при 6 повторах сумма пауз даёт 127 секунд. Модель стоит ровно столько, сколько стоят её предсказания, поэтому я поменял настройку и засёк заново. При 2 повторах ядро сдалось за 7.2 секунды против предсказанных 7. При 3 повторах вышло 19.4 секунды против предсказанных 15.
То есть на коротком конце модель верна, а на длинном промахивается сначала на 4.4, потом на 8.3 секунды. Первая догадка была про память ядра: Linux кеширует оценку времени оборота по адресу назначения. Испорченная оценка растянула бы каждый следующий прогон. Дело не в этом. Записи по этому адресу нет вообще, а после сброса кеша выходит те же 19.4 секунды, до десятых. Откуда берутся лишние секунды, я не знаю.
Половина, которая действительно вечность
Таймаут на соединение это та половина, которую тебе дают. Вторую не дают.
Я поднял локальный сервер, который принимает соединение, читает запрос и не отвечает никогда. Клиент, у которого ничего своего не выставлено, всё ещё ждал, когда я остановил его на 30 секундах. Повторять тут нечего и обнаруживать ядру нечего, потому что соединение здорово, а собеседник просто молчит. Ни у чего ниже библиотеки нет причины вмешаться. Тот же клиент с выставленными 2000 миллисекундами умер на 2.0 секундах.
Вот форма ловушки. Отказ, о котором думают все, это упавшая машина. Он уже обработан: плохо, но ограниченно. Отказ, под который никто не заводит будильник, это собеседник, который принял твой запрос, ничего не сказал и теперь держит твой поток до самой смерти процесса.
Где я споткнулся
Первый прогон занял 0.0 секунды и отчитался об успехе. Я запускал его дома. Моя домашняя сеть отвечает на 192.0.2.1 по 80 порту, на адрес, который по стандарту не принадлежит никому. На 10.255.255.1 она отвечает так же. В сети, где всегда кто-то снимает трубку, чёрной дыры для замера нет, поэтому замер переехал на машину с честным маршрутом. Честная версия моего первого результата звучит так: я померил свой роутер, а не ядро.
Чего я не проверял
Откуда берутся лишние 8.3 секунды. Те же ли числа на Windows при честном маршруте: дома проверить это не на чем. Оставляет ли библиотечный таймаут, сработавший посреди запроса, сокет в состоянии, которое пул потом переиспользует. Это и есть тот отказ, за который мне тревожно в проде. Он требует другого стенда. И повторы: любой знакомый мне клиент кладёт ретрай поверх таймаута, поэтому вызывающий ждёт произведение 2 настроек, а не любую из них.
Узкое утверждение такое. После того как соединение поднялось, время не считает никто. До того как оно поднялось, ядро считает примерно до 2 минут числом, которого никто в коде ни разу не видел.