Итак, мой умный дом готов, слушается голоса, управляет климатом, зарядкой аккумулятора на даче (https://habr.com/ru/post/538896/).
Более того, умные устройства стоят теперь как на даче, так и дома, в городе. Причем из-за особенностей совместимости экосистем с Яндексом часть устройств дома (RGB ленты) управляются через сервер на Majordomo (дача).
И вот тут возникает ряд логичных вопросов:
Где должен стоять сервер – дома или на даче?
Потерей управления какими устройствами жертвовать при обрыве связи между домом и дачей?
Как не грузить GSM канал до дачи передачей графиков в HTML верстке сайта?
Легко догадаться, что ответом является резервирование:
Серверы должны быть и там и там
Серверы должны уметь управлять всеми устройствами
Серверы должны иметь полный набор данных
Так как датчики общаются с сервером в основном через протокол MQTT, MQTT брокер так же становится точкой отказа.
Резервирование сервера
Начнем с MQTT брокера. Если не считать таких сообщений, как LWT («последняя воля устройства») и Retain (хранимых на сервере), большинство сообщений передаются одномоментно и только тем, кто в данный момент подключен к брокеру. То есть "отправил - забыл".
К счастью, в последних версиях mosquitto сервера есть режим бриджа – вы просто задаете адрес второго брокера и топики, которые нужно дублировать, направления дублирования. В моем случае вполне пригодился вариант «копировать все в обе стороны». Вот как это делается в raspbian/armbian – добавляем в /etc/mosquito/mosquito.conf:
#connection bridge-01
connection bridge-01
address mqtt.mydomain.ru:1883
topic # out 0
topic # in 0
Вуаля, оба брокера содержат одни и те же данные, локальные устройства могут общаться с локальным брокером в локальной сети (без задержек и прочей чепухи ненадежных каналов).
Дальше, сами серверы Majordomo. Я сделал второй сервер на базе Orange pi one plus (1Gb RAM) – стоит в 2 раза дешевле Raspberry Pi4, для вспомогательного сервера - то, что надо. Но серверы должны уметь делать одно и то же, но не делать этого одновременно (в большинстве случаев это не страшно, ну поступит 2 команды на включение зарядника – не страшно, но некоторые вещи лучше дважды не делать, например, не поворачивать солнечные панели по часам).
Так как для корректной работы с датчиками и исполнительными устройствами я использую MQTT, логично отслеживать работоспособность удаленного сервера через тот же MQTT. Для этого я создал отдельный класс, в котором есть 2 статуса (для отображения) и 2 времени – для локального сервера и для удаленного, а также адрес активного брокера и собственный адрес сервера. Раз в 10 секунд выполняется проверка системного цикла MQTT – время последнего запуска (ThisComputer.cycle_mqttRun). Это время сравнивается с текущим (time()). Если прошло больше 10 секунд – паникуем, то есть понимаем, что локальный сервер не дружит с MQTT брокером и показываем это в интерфейсе. Так же сравниваем время последнего запуска MQTT цикла на удаленном сервере (приходит через MQTT). Если прошло больше 20 секунд, а локальный цикл в порядке – понимаем, что удаленный сервер больше не управляет устройствами. Проверяем еще один параметр, передаваемый через MQTT – имя активного брокера. Если это не локальный, то надо переключать на себя:
$val=getGlobal("ThisComputer.cycle_mqttRun");
$locval=time()-$val;
$this->setProperty("LocValue",$val);
$this->setProperty("LocDeltaT",$locval);
if($locval>10)
$locstate=1;
else
$locstate=0;
$tmp=$this->getProperty("Status");
if(is_null($tmp))
$tmp=10;
if($tmp!=$locstate)
$this->setProperty("Status",$locstate);
$remval=time()-$this->getProperty("RemValue");
$newstate=($remval<20)?0:1;
$this->setProperty("RemStatus",$newstate);
$ot = $this->object_title;
$currBroker=$this->getProperty("MQTT_broker");
$sA=$this->getProperty("selfAddress");
if($sA!=$currBroker)
$this->setProperty("isController",0);
setTimeOut($ot . "_checkCycle",'callMethod("'.$ot.'.checkCycle");',10);
if(
(!$locstate&&($newstate||($this->getProperty("LinkedRoom")=="Energoblok")))&&
($sA!=$currBroker)
)// remote failed local good or local is good and is not local server
{
debMes('Switch to '.$this->getProperty("selfAddress"),0);
$cnt=0;
for($i=40;$i<90;$i++)
{
if(ping('192.168.3.'.number_format($i,0)))
{
getURL('http://192.168.3.'.number_format($i,0).'/cm?cmnd=MqttHost%20'.$this->getProperty("selfAddress"));
debMEs('http://192.168.3.'.number_format($i,0).' is online',0);
$cnt++;
$this->setProperty("LocValue",time());
}
}
if($cnt>10)
{
$this->setProperty("MQTT_broker",$this->getProperty("selfAddress"));
$this->setProperty("isController",1);
}
}
У меня Tasmota устройства (IP в диапазоне c 192.168.3.40 по 192.168.3.90), им можно передать обычным URL запросом новый адрес MQTT сервера. Вот только запросы надо посылать синхронные, а главное – не забывать между ними обновлять MQTT свойство для удаленного сервера. Иначе получится замкнутый цикл – начали переключаться, больше 10 секунд не сообщаем удаленному серверу, что мы живы. Тот начинает переключение на себя и тоже замирает. Не делайте так.
Повышаем надежность самого сервера
Операционная система и БД хранятся на карте памяти. Есть карты класса А1 и даже А2, но через год постоянной нагрузки такая карта с большой вероятностью загнется. Кроме того, штатный код пишет в базу кучу ненужного, а каждое чтение/запись свойства любого объекта – это обращение к БД. У меня было порядка 1200 обращений к БД в секунду.
Карту можно спасти, если базу держать в оперативной памяти. К счастью, разработчики Majordomo сделали прошивку для Raspberry сразу с опцией БД в памяти, а для прочих платформ есть скрипт для переноса БД в память (но памяти должно быть не менее 1Гб, на orange pi zero c 512Мб у меня не взлетело - база весит порядка 300Мб и столько же надо дополнительно для дампов бэкапов). Да, теперь в любой момент просто так перезагружать систему нельзя, нужно выполнить скриптик, иначе данные за последние полчаса потеряются (но база всегда бэкапится в рабочем состоянии!). Зато скорость работы БД и долговечность карты памяти – просто великолепны.
Остался последний штрих – снизить нагрузку на БД, убрав лишние запросы. Решение простое:
Обновиться до последней версии (буквально пару недель назад обновили интерфейс, убрав лишние обращения к БД и переписав на java скрипты)
Обращаться к локальным или глобальным свойствам только один раз и использовать переменные в памяти (смотрите на пример кода цикла проверки – getProperty\setProperty там использованы только по одному разу).
Еще пример оптимизации скриптов – чтобы и обращений лишних не было, и чтобы срабатывала автоматическая зависимость от переменной, например:
if((($temp2Floor=getGlobal("sTemp2Floor.value"))<'21')&&
gg("remote_mqtt_updated.isController")) // if remote failed
{
if ($temp2Floor < '21' && !getGlobal("rConserveSW.status") && timeBetween('2:00', '8:00'))
{
if (!getGlobal("rDieselHome.status"))
{
callMethod("rDieselHome.turnOn");
}
} else if ($temp2Floor > '23')
{
if (getGlobal("rDieselHome.status"))
{
callMethod("rDieselHome.turnOff");
}
}
}
Обратите внимание, что в первом условии есть ветка проверки, этот ли сервер управляет устройствами в настоящий момент (gg("remote_mqtt_updated.isController")). remote_mqtt_updated – это объект контроля работы серверов.
Теперь у меня число обращений к БД порядка 380 в секунду, что гораздо лучше по сравнению с начальным значением в 1200.
Итог
Вот так, добавив сервер за 2500 рублей, получил полное резервирование брокера сообщений, сервера управления устройствами (логики) и можно получать графики с актуальными данными с домашнего сервера, не нагружая сервер, работающий через GSM модем.
Siorinex
Если роутер позволяет установить OpenWrt — туда можно впихнуть нгинкс как сервер-коммутатор для запросов к mqtt: он пошлет топик доступному серверу, а если доступны оба — те сами разберутся между собой.
AndyRoss79 Автор
Ну балансировщик это отлично. Но задача MQTT брокера не только получить сообщение от устройства, но и доставить подписчикам. Как обеспечить сессионность связи с MQTT брокера, да и 2 сервера умного дома должны получать одинаковый набор данных телеметрии и статуса подключения устройств (LWT формируется на основании обрыва сессии от устройства до MQTT брокера).