При работе с GUI и терминальными приложениями нередко случается, что пользователь, работая в режиме удаленного доступа (как правило, через Интернет), покинув компьютер минут на 15, по возвращении обнаруживает, что программа зависла. На любое действие она отвечает ошибкой, содержащей примерно такие фразы: «Потеряна связь с сервером», "[WINSOCK] virtual circuit reset by host" и т.п. Наблюдается такое и при выполнении «долгоиграющих» методов (запросов к серверу), в которых не предусмотрен вывод прогресс-бара или какая-либо интерактивность.
Данная проблема характерна не только для GUI и терминальных решений на базе СУБД Caché и Ensemble компании InterSystems, а вообще для любого клиент-серверного взаимодействия по протоколу TCP/IP. Обычно она решается на прикладном уровне путём периодического обмена пустыми сообщениями специального вида, предназначенными лишь для того, чтобы просигнализировать о том, что приложение «живо».
Ниже о том, как можно решать эту проблему без программирования.
Источник проблемы
Источник проблемы лежит в природе протокола TCP/IP. Как правило, источник сеанса TCP/IP и его приемник находятся в различных сетях, и на пути сеанса встречается несколько маршрутизаторов. Хотя бы один из них обычно выполняет NAT-преобразование адресов. Ресурсы маршрутизатора всегда ограничены, поэтому некоторые из них выполняют очистку NAT-таблиц от «мёртвых» сеансов. Сеанс считается «мёртвым», если по нему не передавались пакеты в течение некоторого заданного интервала времени (назовем его интервал очистки). Таким образом, «молчаливый» сеанс может быть принят за «мёртвый» и вычищен из NAT-таблицы. При этом ни источник, ни приемник об этом не уведомляются («не царское дело»), и оба они остаются в уверенности, что сеанс ещё «жив» (в чем легко убедиться командой netstat, выполнив ее на клиенте или на сервере в момент возникновения ошибки, но до нажатия на ОК). Когда пользователь, получивший сообщение об ошибке, нажмет на ОК, о разрыве сеанса узнает клиент; серверный же процесс завершится, когда «умерший» сеанс распознает ОС.
Экспериментально установлено, что интервал очистки у многих маршутизаторов (по крайней мере, с прошитым Linux 2.4/iptables) составляет около 10 минут. Постараемся заставить наш TCP-сеанс автоматически поддерживать себя в активном состоянии, даже когда не передается никаких пакетов с данными.
Предлагаемое решение
На уровне ОС обнаружением разорванных TCP-соединений управляют следующие параметры ядра, управляющие работой механизма tcp_keepalive [1]:
• tcp_keepalive_time — интервал времени с момента отправки последнего пакета с данными; по истечении этого срока соединение помечается как требующее проверки; после начала проверки параметр не используется;
• tcp_keepalive_intvl — интервал между проверочными пакетами (отправка которых начинается по истечении tcp_keepalive_time);
• tcp_keepalive_probes — количество неподтвержденных проверочных пакетов; по исчерпании этого счетчика соединение считается разорванным.
Надо сказать, что механизм tcp_keepalive имеет двойное назначение: он может использоваться как для искусственного поддержания активности соединения, так и для выявления разорванных (так называемых «полуоткрытых») соединений. В данной статье обсуждается в основном первое применение, о втором применении, возможно, речь пойдёт в следующей статье на эту тему.
Для того чтобы механизм tcp_keepalive был задействован для TCP-соединений, необходимы два условия:
• поддержка на уровне ОС; к счастью, и в Windows, и в Linux она имеется;
• на одном из концов соединения сокет должен быть открыт с параметром SO_KEEPALIVE. Как оказалось, сервисы Caché открывают сокеты с этим параметром, а сервис OpenSSH несложно заставить поступать аналогично.
Наибольший интерес для нас представляет первый параметр (tcp_keepalive_time), так как именно от него зависит, насколько часто будет выполняться проверка неактивных (с точки зрения отсутствия трафика) соединений. Его значение по умолчанию — и в Windows, и в Linux — равно двум часам (7200 с). Типичное же время бездействия, после которого наступает разрыв, составляет около 10 минут. Поэтому предлагается установить значение параметра в 5 минут, что позволит искусственно поддерживать активность TCP-сеансов, не перегружая сеть избыточным трафиком (5 минут — это не 5 секунд).
Установка параметров tcp_keepalive на сервере Windows
Вы должны обладать правами Администратора к серверу. В разделе реестра
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesTcpipParameters
создайте параметр DWORD с именем KeepAliveTime и значением 300000 (десятичным). Параметр задаётся в миллисекундах, поэтому предлагаемое значение — это 5 минут. После чего остановите Caché и перезагрузите сервер.
Что касается двух других параметров tcp_keepalive, то их умолчания в Windows таковы:
KeepAliveInterval
Key: TcpipParameters
Value Type: REG_DWORD—time in milliseconds
Valid Range: 0–0xFFFFFFFE
Default: 1000 (1 секунда)
KeepAliveProbes
Такого параметра (устанавливающего количество неподтвержденных проверочных пакетов), в реестре не существует. Согласно [2], в Windows 2000 / XP / 2003 в этом качестве используется значение параметра TcpMaxDataRetransmission (умолчание — 5), а в более поздних версиях [3] — фиксированное значение, равное 10. Поэтому, если менять только значение первого параметра (с 7200 на 300), сохраняя умолчание для второго, сервер Windows 2008 будет узнавать о разрыве TCP-соединения через 1*10 + 300 = 310 секунд.
Установка параметров tcp_keepalive на сервере Linux
Изменить значения параметров можно «на ходу», не перезагружая сервер. Зайдите как root и выполните:
echo 300 > /proc/sys/net/ipv4/tcp_keepalive_time
Чтобы сделать изменение долговечным по отношению к возможным перезагрузкам сервера, проще всего отредактировать файл параметров ядра /etc/sysctl.conf, добавив в него строку (лучше две):
# help to prevent disconnects
net.ipv4.tcp_keepalive_time = 300
Обратите внимание, что в отличие от Windows, значение параметра задается в секундах.
Что касается остальных двух параметров tcp_keepalive, то их умолчания в Linux таковы:
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
Если менять только значение первого параметра (с 7200 на 300), сохраняя умолчания для остальных двух, сервер Linux будет узнавать о разрыве соединения только через 75*9 + 300 = 975 секунд.
Установка параметра TCPKeepAlive в конфигурации СУБД Caché
Начиная с версии 2008.2, в Caché для платформ Windows и Linux появилась возможность задавать tcp_keepalive_time на уровне сокета, что удобно, так как позволяет избежать изменения настроек операционной системы. Однако «в чистом виде» эта возможность, в основном, представляет интерес лишь для независимых разработчиков сокет-серверов. К счастью, она была дополнена параметром конфигурации TCPKeepAlive=n в секции [SQL], доступным для редактирования со страницы Портала управления системой: Конфигурация > Общие Настройки SQL. Значение по умолчанию — 300 секунд (то, что доктор прописал). Действие параметра распространяется не только на SQL, но и, как нетрудно догадаться, на любые соединения с Caché, обслуживаемые сервисом %Service_Bindings. К ним относится, в частности, и объектный доступ через CacheActiveX.Factory, поэтому если ваше приложение может использовать этот протокол в качестве транспорта, не стоит упускать такую возможность.
Установка параметров KeepAlive в конфигурации сервера OpenSSH
Если вы используете SSH [4] (для работы в режиме командной оболочки или как транспорт для вашего GUI-приложения), то… скорее всего, проделанной настройки ядра будет достаточно, поскольку сервис OpenSSH (по крайней мере, в версии 5.x) по-умолчанию открывает сокет с параметром SO_KEEPALIVE.
На всякий случай стоит проверить конфигурационный файл /etc/ssh/sshd_config. Найдите в нем строку
#TCPKeepAlive yes
Если нашли, то делать ничего не надо, так как значения параметров по умолчанию предоставляются в закомментированном виде.
Протокол SSH v.2 имеет альтернативные средства контроля активности сеансов, например, с помощью настройки параметров сервиса OpenSSH ClientAliveInterval и ClientAliveCountMax.
При использовании этих параметров, в отличие от TCPKeepAlive, запросы KeepAlive отправляются через защищённый SSH канал и не могут быть подменены. Приходится признать, что альтернативные средства являются более безопасными, нежели традиционный механизм TCPKeepAlive, для которого существует опасность анализа KeepAlive-пакетов и организации DoS-атак [5].
ClientAliveInterval 0
Устанавливает время ожидания в секундах, по истечении которого, если не поступает информация со стороны клиента, sshd отправляет ему запрос отклика через защищённый канал. По умолчанию используется 0, что означает, что клиенту не будет направлен такой запрос.
ClientAliveCountMax 3
Устанавливает количество запросов клиенту, которые могут быть отправлены sshd без получения на них отклика. Если предел достигнут, sshd разъединяется с клиентом и завершает сеанс. Значение по умолчанию: 3. Если вы установите значение параметра ClientAliveInterval равным 60, оставив ClientAliveCountMax без изменений, то не отвечающие ssh-клиенты будут отключены примерно через 180 секунд. При этом следует отключить механизм TCP KeepAlive, установив
TCPKeepAlive no
Всегда ли это работает?
Существуют категории сетевых проблем, в которых описанный подход может быть малоэффективен.
Одна из них имеет место, когда из-за низкого качества сетевого обслуживания связь может физически пропадать в течение коротких промежутков времени. Если сеанс бездействует, а связь временно пропадает и восстанавливается до того, как клиент или сервер попытаются что-то друг другу послать, то никто из них ничего «не замечает», и TCP-сеанс сохраняется. В случае периодических проверок TCPKeepAlive возрастает вероятность обращения сервера к клиенту в моменты временного исчезновения связи, что может вести к принудительным разрывам TCP-соединения. В такой ситуации можно попробовать увеличить на сервере KeepAliveInterval до 60-75 секунд (вспомнив, что в Windows умолчание — 1 секунда) при максимальном количестве повторов равным 10, в надежде, что за 10 минут любая временная сетевая проблема самоустранится. Правда, если повторные передачи будут длиться слишком долго, и окажется, что
KeepAliveTime + (KeepAliveInterval * кол-во_повторов) > 10 минут
то TCP-сеанс, несмотря на все предпринятые усилия, может быть принят за «мёртвый» и вычищен из NAT-таблицы.
Другая категория проблем связана с недостаточной пропускной способности используемых маршрутизаторов и/или каналов связи, когда при перегрузке могут теряться любые пакеты, в том числе и KeepAlive. В случае маршрутизаторов такие проблемы иногда решаются сменой прошивки (мне, например, это помогло победить Acorp ADSL XXXX), или, в худшем случае, заменой его на более производительную модель. В случае «слишком узких» каналов связи не остаётся ничего другого, кроме как их расширять.
Заключение
Предложенный подход позволяет искусственно поддерживать активность сеансов TCP/IP, по которым в текущий момент не передается никаких данных, исключительно на системном уровне, не внося каких-либо изменений в прикладной код. На сегодня он проверен и успешно используется в Caché for UNIX (Red Hat Enterprise Linux 5 for x86-64) 2010.1.4 (Build 803), Caché for Windows (x86-64) 2010.1.4 (Build 803), а также в более поздних версиях.
Следует признать, что он эффективно работает, если сетевое соединение физически устойчиво, и кроме разрыва неактивных сеансов других сетевых проблем у вас нет.
При развёртывании приложения в агрессивной среде (удалённый доступ, распределённые системы и т.д.), подумайте о реализации KeepAlive не на уровне TCP, а на уровне защищённого протокола более высокого уровня; хорошим кандидатом здесь оказывается SSH.
Литература
[1] Fabio Bussato. TCP Keepalive HOWTO
[2] How to harden the TCP/IP stack against denial of service attacks in Windows Server 2003
[3] TCP/IP Registry Values for Microsoft Windows Vista and Windows Server 2008
[4] Интерактивная система просмотра системных руководств (man-ов)
[5] OpenSSH: установка и конфигурирование sshd
Автор: AlexeyMaslov