Написал небольшую библиотечку для синхронизации данных между программами. Есть очередь сообщений. Есть сервер, цепляющийся к очереди, открывающий порт и отвечающий на запросы типа "дай все сообщения", "дай сообщения с такого-то времени", "дай сообщения за последние n мс" и так далее. Есть клиент, цепляющийся к аналогичной очереди, и периодически опрашивающий сервера с указанными адресами/портами, занося новые сообщения в очередь. Подключается, опрашивает и отключается. Написал несколько программ, обменивающихся через такие очереди. Под Windows всё работает в любых версиях от W7 до W10, проверял c версиями Qt 4.7.1, 4.8.6, 5.5.1. Под Linux (Ubuntu 14.04, Qt 5.5.1) тоже работает практически всё. Проблема в слове "практически".
Итак, есть программы A, B, C. B опрашивает несколько источников (A) и формирует выходную очередь, которую опрашивают несколько клиентов (С). Доставка от A к B или от A к C работает всегда. А вот от B к C иногда, если С запущена позже, чем B, связь не налаживается - идут timeout'ы и порча пакетов. При этом, если B и C находятся на одном узле, всё работает. Пробовал для отладки поднимать на ноутбуке виртуальные машины Virtualbox и запускать B и C в разных виртуалках или одну в виртуалке, другую на хосте - всё работает, даже если сетевые задержки сильно увеличивать. Проблемы появляются только при работе между разными физическими компами. Попробовал пересобирать B под Qt 4.8.6 - проблема появляется реже, но всё равно иногда остаётся. Если запускать B в виндовой версии, через Wine - всё тоже работает. Сетевые экраны отключены. Порты (9200-9300), используемые для сервера, никем не заняты, после запуска программ-серверов (A, B) сканер показывает, что они (порты) открыты.
Сейчас собираю на работе тестовый стенд (несколько узлов с Linux) для того, чтобы запустить B под отладчиком. Но в целом ситуация вызывает у меня глубочайшее недоумение, так как:
один и тот же код работает между A и B, между A и C, но не между B и C
один и тот же код работает между разными виртуальными машинами, но не между разными физическими
... в сборке для `Windows`, но не в сборке для `Linux`.
... если C запущена раньше, чем B, но не наоборот.
Код проверен Valgrind'ом и cppcheck'ом. Натыкался ли кто-нибудь на похожие "грабли", можете ли предположить хоть одну причину такого странного поведения?
Update: Всё страньше и страньше. Собрал тестовый стенд - и не могу воспроизвести проблему. Те же самые A, B и C цепляются друг к другу во всех возможных комбинациях. Чувствую, придётся запасной switch на объект везти.
Update2: Удалось воспроизвести проблему на стенде. Цепляю к одному B несколько C, через некоторое время часть клиентов (C) перестаёт цепляться. Что удивительно: когда клиент цепляться перестал, я пробую с его машины пинговать машину, на которой развёрнут сервер (B) - и она не пингуется. И начинает пинговаться только после того, как машина B в свою очередь начнёт пинговать клиентскую (С). У меня появилось подозрение, что Ubuntu воспринимает эту кучу запросов, приходящую с клиента, как DOS-атаку, и блокирует хост клиента. Осталось найти, где в настройках системы это прописано, и отключить.
Update3: Ситуация действительно очень похожа на защиту от DDOS-атаки. И такая версия объясняет, почему связь между A и B работает, а между B и C - нет. К одному источнику данных (A) цепляется максимум один промежуточный обработчик (B), а вот к нему может цепляться несколько клиентов C. И именно обращения на один порт с разных узлов, видимо, интерпретируются как DDOS-атака. При этом первый подключившийся клиент остаётся работоспособным, а вот последующие обрубаются так, что даже ping'и от них не проходят.
Update4: найден workaround. Я запустил на машине, на которой запущен B, команду ping до хостов, на которых запущены C. После этого отрубаться эти хосты перестали. Вместе с тем, понять, что же вызывает такую реакцию системы, пока не получается. ufw отключён.
Update5: проблема решена. Сервер не должен рвать соединение, надо дождаться, пока соединение разорвёт клиент (см. Update к комментарию).
Ответ
Причины возникновения проблемы нашлись. Их было две. Одна - в тестовой системе, там серьёзные неполадки с сетью, и даже pingи теряются процентов на 30. Вторая и главная причина - в моём коде, сервер неправильно закрывал соединение. Обработав входящее соединение и сделав socket->write(), он висел на socket->waitForBytesWritten() (таким образом, до завершения отправки поток сервера был заблокирован), а после этого выполнял socket->close(). Видимо, такое принудительное закрытие сокета и не нравилось ОС. Когда я убрал waitForBytesWritten() и вместо close() сделал disconnectFromHost(), всё заработало.
Update: Заработало не всё. На сервере проблема осталась, workaround из Update4 помогает, пока конфигурация остаётся стационарной. Когда мы добавляем мобильный клиент, подключающийся из разных мест, с разных IP, добавить его в пингующий скрипт уже не получается, и он довольно скоро отваливается. На клиентах, даже стационарных, иногда появляются сообщения об обрыве и восстановлении связи. Решение проблемы нашёл только недавно.
Итак: когда сервер стал закрывать сокет более осторожно, через disconnectFromHost, проблема стала менее острой. Однако правильное решение оказалось таким: сервер вообще не должен закрывать сокет самостоятельно! Он должен реагировать на сигналы disconnected и error, по которым вызывает deleteLater()
Когда сервер сам решает закрыть сокет, завершив отправку данных, возникает гонка сигналов. Сигнал об отключении с сервера может прилететь раньше, чем ответ сервера клиенту, и тогда мы на клиенте получаем ситуацию "сервер отключился - сервер подключился". А когда до сервера долетает запрос на отключение от клиента, а сокета уже нет, видимо, возникает аварийная ситуация, и накопление таких ситуаций приводит к блокировке IP клиента (под Linux. Windows такие запросы, похоже, просто игнорирует).
Итог: из сервера просто удалил disconnectFromHost(), и старый клиент работает (завершается неделя тестирования 2 клиента на 1 сервер) без единой потери связи. Timeoutы уменьшил с 400-600 мс до 10мс, и всё равно разрывов нет. Главный вывод: сервер не должен закрывать соединение сам, он должен реагировать на сигналы disconnected и error, возникающие вследствие действий клиента и среды передачи данных.
Комментариев нет:
Отправить комментарий