Прекратите принимать новые TCP-соединения, не отбрасывая существующие

У меня есть два сервера, которые прослушивают порт TCP за балансировкой нагрузки. Балансировщик нагрузки может определить, была ли попытка подключения TCP к клиенту неудачной и повторить попытку на второй сервер, не отбрасывая это соединение. Я хочу, чтобы можно было отключить любой из этих двух серверов для обслуживания, не отбрасывая одну коллекцию клиентов.

Мои серверы используют этот код для обработки клиентских запросов:

ServerSocketFactory ssf = ...
ServerSocket serverSocket = ssf.createServerSocket(60000);
try {
    while (true) {
        Socket socket = serverSocket.accept();
        ...// Do the processing
    }
} catch (IOException e) {
    ...
}
...

Моя первоначальная мысль заключалась в том, чтобы добавить логическое значение, которое будет установлено при завершении работы приложения и предотвратить новые вызовы serverSocket.accept(), ожидая, пока все существующее соединение будет обработано и закрыто. Однако новое соединение устанавливается еще до вызова serverSocket.accept(). Вот что я вижу в Wireshark, если я поставил точку останова перед этим вызовом. введите описание изображения здесь Проблема в этом пункте, как только я вызываю serverSocket.close(), все такие клиентские соединения удаляются. То, что я хочу достичь, - это один из способов сказать ServerSocket прекратить принимать все новые подключения (т.е. Отправлять только RST для новых подключений или отпускать их тайм-аут), поэтому балансировщик нагрузки может перенаправить их на другой сервер, но в то же время не отбрасывать любые уже установленные соединения.

Изменить: Я ищу какое-то автоматическое решение, которое не требовало бы меня менять настройки балансировки нагрузки или настройки ОС каждый раз, когда я хочу обновить приложение.

Ответ 1

На сервере можно добавить правило брандмауэра, которое заблокирует новые, но будет поддерживать старые подключения. Я полагаю, что сервер основан на Linux? Если это так, вы можете попробовать:

iptables -A INPUT -p tcp --syn --destination-port <port> -j REJECT --reject-with icmp-host-prohibited

После этого вы можете проверить с помощью netstat активное подключение и принести приложение один раз, когда его нет:

netstat -ant|grep <port>|grep EST

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

iptables -L -n

И удалите его:

iptables -D INPUT <rule number>

Ответ 2

В любой момент, когда ServerSocket.accept() блокирует, или ServerSocketChannel.accept() возвращает значение null, очередь отставания пуста. В этот момент прекратите принимать и закрывать гнездо для прослушивания. Подождите, пока все существующие принятые сокеты не закончат работу, и пусть приложение выйдет в этой точке.

Ответ 3

Самый простой способ решить вашу проблему - добавить дополнительный балансировщик нагрузки прямо перед сервером приложений.

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

Изящное выключение для nginx:

nginx -s quit

Изящное выключение для HAproxy:

haproxy -sf $(cat /var/run/haproxy.pid)

Ответ 4

Я пришел к выводу, что то, чего я пытаюсь достичь, невозможно в Linux. Проблема в том, что ОС завершает первоначальное рукопожатие с клиентами, отправив пакет SYN, ACK и ACK без какого-либо контроля над этим процессом приложением. После установления связи соединение устанавливается, и ОС помещает его в очередь на отставание. Как только соединение установлено, балансировщик нагрузки, который я использую (F5 BigIP), не передает его другому серверу ни при каких обстоятельствах, независимо от того, какие проверки здоровья у меня есть. Когда я закрываю сокет, уже установленные, но еще не принятые соединения из очереди журнала не были удалены.

Однако в Windows можно добиться с помощью опции сокета SO_CONDITIONAL_ACCEPT и WSAAccept для Windows Sockets С++ API. Этот параметр позволяет приложению контролировать начальное рукопожатие. Хорошее объяснение можно найти в этом ответе:

При вызове функции listen() на порту ОС начинает принимать соединения на этом порту. Это означает, что он начинает отвечать на SYN, ACK-пакеты на соединения, независимо от того, что код C еще вызвал accept().... Однако в окнах вызов SO_CONDITIONAL_ACCEPT позволяет приложение для управления очередью отставания. Это означает, что сервер ничего не ответит на пакет SYN до тех пор, пока приложение делает что-то с соединением. Это означает, что отклонение соединения на этом уровне могут фактически отправлять RST-пакеты в сеть без создания состояния.

Похоже, что Linux не имеет подобной функции, как описано в этот ответ:

Трехстороннее рукопожатие является частью основной структуры tcp/ip, поэтому он вложен в стек (то есть уровень ядра). Все неядерные код, который вы получаете, работает ПОСЛЕ рукопожатия.