На фото представлены устройства, использованные для прототипирования. Как видно, за основу взята процессор x86 (Intel Edison)
Всем привет. В этой статье я хотел бы поделиться опытом решения одной интересной проблемы, связанной с синхронизацией данных между IoT-устройствами и облачным приложением. Сначала я расскажу об основной идее и целях моего проекта, а затем подробно опишу его техническую сторону и реализацию: речь пойдет об ОС Contiki, базах данных, протоколах и подобных аспектах. В заключение я кратко перечислю технологии, использованные при построении системы.
Вкратце о проекте
Для начала давайте поговорим об основной идее проекта. Ниже схематично изображен принцип работы готовой системы:
Есть пользователь, который через облачный сервис или напрямую (по Wi-Fi) подключается к IoT- устройству. Также где-то в Интернете имеется облачный сервер приложения. Облаком может служить что угодно: скажем, инстанс AWS или Azure или выделенный сервер. Для обмена данными между сервером приложения и IoT-устройствами устанавливается соединение по какому-то протоколу. IoT-устройства каким-то образом соединены друг с другом (например, по Ethernet или Wi-Fi). Помимо этого, есть отдельная группа IoT-устройств, генерирующих телеметрические данные (такие как показатели освещенности или температура).
В общей сложности, может набраться больше 100 или даже больше 1000 устройств. Моя основная задача заключалась в том, чтобы обеспечить обмен данными между облаком и этими IoT-устройствами. Прежде чем двигаться дальше, стоит упомянуть, какие требования предъявлялись к системе:
- Она должна синхронизировать данные между IoT-устройствами.
- Она должна собирать данные с IoT-устройств.
- Она должна синхронизировать данные между IoT-устройствами и облаком.
Техническая реализация
Здесь все довольно просто: пользователь подключается к серверу приложения по HTTP(S), WebSocket или подобному протоколу. Небольшая задачка для читателей: как вы думаете, что можно использовать для соединения между сервером приложения и IoT-устройством?
Если вы подумали про MQTT, вы однозначно правы! Равно как и те, кто выбрал HTTP(S). На самом деле подойдет любой протокол — выбирайте на свой вкус! Мой же выбор пал на — барабанная дробь — асинхронную репликацию! Я имею в виду обычную для баз данных репликацию.
Вы можете спросить, зачем мне репликация. Ответ прост: репликация используется для синхронизации данных, поэтому я могу повсюду — включая облако и IoT-устройства — поддерживать одну версию базы данных. Однако репликацию довольно сложно реализовать. Хочешь репликацию — заведи базу данных, которая ее поддерживает, потому что — повторюсь — репликация естественно присуща базам данных.
Здесь я бы хотел сказать пару слов о тех базах данных, которые я рассматривал при работе над проектом: SQLite, Redis, MySQL, PostgreSQL и Tarantool.
Я сравнил их характеристики и попробовал запустить несколько штук — за исключением MySQL и PostgreSQL — прямо на IoT-устройстве. Ниже расскажу, что из этого вышло.
SQLite — однозначно хорошее решение для хранения данных непосредственно на IoT-устройстве, но у нее нет репликации, и она не поддерживает параллельный доступ из разных процессов.
Redis не поддерживает master-master репликацию и поэтому не может решить мою проблему, так как мне необходима двусторонняя репликация.
MySQL и PostgreSQL слишком тяжеловесны для IoT-устройства, так что я даже не пытался их устанавливать. Но если вы все-таки решите это сделать, смело делитесь своим опытом в комментариях.
Последней в моем списке шла база данных Tarantool. Сразу скажу, что я являюсь коммитером в проект Tarantool, поэтому хорошо знаю сам проект и людей, которые его разрабатывают. К тому же, в Tarantool есть master-master репликация. В общем, для меня это был определенно лучший вариант. Вы же можете использовать в своем проекте другую базу данных. Основная идея, которую я пытаюсь донести, в том, что IoT-устройства могут использовать базы данных с master-master репликацией для обмена данными.
До настоящего момента я лишь поверхностно знакомил вас с проектом. Теперь давайте немного погрузимся в его технические аспекты.
Начну с проблем, с которыми я столкнулся при использовании Tarantool. Во-первых, Tarantool не запускалась на архитектуре ARMv7. Во-вторых, Tarantool не запускалась в 32-битном окружении, что только усугубляло ситуацию. В итоге я смог решить эти проблемы. Ниже приведу правила разработки, которые мне в этом помогли.
- Используйте toolchain-файлы для CMake. В противном случае вы, так же как и я, потратите много времени на исправление CMake-файлов.
- Не используйте беззнаковый тип и другие типы, для которых не указан размер. В libc для этого есть специальные типы, такие как uint32_t. Иначе можно получить неопределенное поведение. Это правило применимо только к C/C++.
- Портируйте ваши автотесты.
Ожидается, что ваши автотесты можно запустить на IoT-устройстве. Если это не так, есть риск убить много времени на отладку.
Итак, у меня есть работающая база данных с master-master репликацией. Замечательно! Следующий шаг — соединить устройства, на которых эта база данных установлена, по 6LoWPAN. Напомню, у меня есть сеть из множества IoT-устройств, соединенных друг с другом по 6LoWPAN, с которых мне необходимо собрать все телеметрические данные.
Краткая схема работы готовой системы
Устройства с сенсорами передают телеметрические данные посредством радиоволн. Этот стандарт называется 6LoWPAN (IPv6 поверх маломощных беспроводных персональных сетей). Замечу, что я не использовал в проекте LoRaWAN. Возможно, я найду применение этой технологии в будущем, но в этой статье я сосредоточусь на 6LoWPAN. Итак, для сбора телеметрических данных я буду использовать шлюз, являющийся важной частью системы. Шлюз — это MIPS-устройство (MIPS — это семейство процессоров) с WAN-антенной для сбора данных, передаваемых посредством радиоволн. Кроме этого, на шлюзе установлено приложение 6LBR, конвертирующее полученные данные в IPv6-пакеты.
Приложение 6LBR
Изображение выше иллюстрирует принцип работы 6LBR. Шлюз с установленным на него 6LBR служит конвертером между беспроводной сенсорной сетью и любой другой. На картинке изображена конвертация из беспроводной сенсорной сети в IP-сеть лишь потому, что так 6LBR работает по умолчанию. Немного позже я объясню, как изменить это поведение.
Более подробную информацию можно найти на странице 6LBR на GitHub.
Вы можете спросить, что же мне дает использование 6LBR. Во-первых, я получаю стек IP, так что я могу использовать функционал стеков TCP и UDP в моих приложениях 6LBR. Во-вторых, я могу использовать любое устройство ввода-вывода с 6LBR. Скажем, можно записать сырые данные прямо в bash. =) К сожалению, 6LBR не пишет напрямую в MQTT. MQTT-брокеры ничего не знают о сырых данных, и с этим приходится мириться.
Зачем же мне понадобилась прямая запись в MQTT-брокер? Ответ прост: дело в legacy-коде.
Здесь я бы хотел сказать пару слов о приложениях 6LBR. В общем случае приложение 6LBR — это написанный на С код с API, позволяющим использовать стек IP и делать некоторые другие вещи. Разработка такого приложения сопряжена как минимум с двумя трудностями: сложная модель потоков и сложная модель памяти. Поэтому запаситесь терпением и приготовьтесь к частым аварийным завершениям вашей программы. Ниже приведен небольшой кусок разработанного мной приложения 6LBR (заранее прошу прощения: могу выложить только картинку с нарочно запутанным кодом, потому что исходники закрыты):
Обратите внимание на одну интересную вещь — PROCESS_YIELD(). В 6LBR есть кооперативная многозадачность, а это значит, что приложения 6LBR должны возвращать управление в каждой итерации цикла. Код не должен выполняться слишком долго.
Итак, давайте еще раз посмотрим, на какой стадии находится наш проект. С помощью шлюза и установленного на него приложения 6LBR я создал mesh network для чтения и записи данных внутри нее. Мне также удалось обернуть IP-пакеты в MQTT-сообщения, каждое из которых содержит информацию об устройстве, включая телеметрические данные. Кроме того, у меня появилась возможность манипулировать устройствами ввода-вывода: скажем, я могу записывать MQTT-сообщения на UART. Но затем я столкнулся с новой проблемой: Tarantool не работает с MQTT-брокерами. Ниже расскажу, как мне удалось обойти это ограничение.
Я решил использовать libmosquitto, написанную на чистом С MQTT-библиотеку, потому что она позволяет довольно просто интегрировать MQTT в мое приложение. Ниже приведен пример использования этой библиотеки для работы с MQTT-сообщениями (ссылка):
static
int
mosq_poll_one_ctx(mosq_t *ctx, int revents, size_t timeout, int max_packets)
{
/** XXX
* I'm confused: socket < 0 means MOSQ_ERR_NO_CONN
*/
int rc = MOSQ_ERR_NO_CONN;
int fd = mosquitto_socket(ctx->mosq);
if (fd >= 0) {
/** Wait until event
*/
revents = coio_wait(fd, revents, timeout);
if (revents != 0) {
if (revents & COIO_READ)
rc = mosquitto_loop_read(ctx->mosq, max_packets);
if (revents & COIO_WRITE)
rc = mosquitto_loop_write(ctx->mosq, max_packets);
}
/**
* mosquitto_loop_miss
* This function deals with handling PINGs and checking
* whether messages need to be retried,
* so should be called fairly _frequently_(!).
* */
if (ctx->next_misc_timeout < fiber_time64()) {
rc = mosquitto_loop_misc(ctx->mosq);
ctx->next_misc_timeout = fiber_time64() + 1200;
}
}
return rc;
}
Я могу взять ссылку на дескриптор сокета и использовать собственный событийный цикл для обработки некоторых событий. И это здорово! Хотел бы обратить ваше внимание на то, что в Tarantool, так же как и в 6LBR, есть кооперативная многозадачность. Для возвращения управления Tarantool использует
coio_wait()
.Ах да, забыл упомянуть, что Tarantool — это еще и сервер приложений на языке Lua. Сюрприз! Поэтому я портировал libmosquitto на Lua. Ниже привожу кусок кода, в котором вызывается функция, которую вы уже видели в предыдущем примере:
__poll_forever = function(self)
local mq = self.mqtt
while true do
self.connected, _ = mq:poll_one()
if not self.connected then
if self.auto_reconect then
self:__try_reconnect()
else
log.error(
"mqtt: the client is not currently connected, error %s", emsg)
end
end
fiber.sleep(self.POLL_INTERVAL)
end
end,
Я также портировал все функции из API libmosquitto. Посмотреть на результат можно здесь. По ссылке дан пример использования. Все что нужно сделать для сбора данных со всех устройств внутри mesh network — это вызвать функцию
subscribe()
из определенного места и опубликовать метод get()
! Заключение
Давайте посмотрим на то, что у нас получилось:
Соединение с сервером приложения установлено посредством предоставляемой Tarantool master-master репликации. Из этого вытекают два полезных свойства:
- Если сервер приложения изменяет какие-либо данные, эти обновленные данные доставляются на все IoT-устройства в сети.
- Если IoT-устройство изменяет какие-либо данные, эти обновленные данные доставляются на сервер приложения.
Именно эти свойства и являются решением моих проблем.
Я также могу соединить мои IoT-устройства посредством master-master репликации. Таким образом устройства и облако объединяются в кластер, который можно использовать для синхронизации всех данных. Все IoT-устройства и облако синхронизированы большую часть времени, за исключением случаев, когда между ними пропадает соединение. Как только соединение будет восстановлено, все данные снова синхронизируются. Просто замечательно!
Шлюз с установленным на него приложением 6LBR позволяет обмениваться данными между моими IoT-устройствами и другими IoT-устройствами. Он оборачивает каждое сообщение в MQTT-сообщение и передает его в канал UART.
IoT-устройство #N с установленным на него MQTT-брокером считывает эти сообщения из канала UART. MQTT-брокер перенаправляет сообщения в Tarantool по MQTT-соединению. Tarantool считывает их, затем для каждого сообщения сервер приложений Tarantool выполняет некоторый код.
IoT-устройство #N соединено со всеми остальными устройствами посредством предоставляемой Tarantool master-master репликации. Такая же репликация используется для соединения всех устройств с облаком.
На этом все! Я решил поставленную задачу и очень надеюсь, что мой опыт поможет вам в ваших собственных проектах в будущем. Подытожу: я использовал Tarantool и как основной фронтенд на моих выделенных серверах, и как сервер приложений. Если вас заинтересовала данная тема, рекомендую взглянуть на другую мою статью на английском языке. Оставайтесь на связи и следите на новостями!
Комментарии (14)
crazyblu
01.02.2017 12:08+2«за основу взята платформа Intel Edison, так как она поддерживает многие архитектуры, в том числе MIPS и ARM» звучит неправильно, т.к. Intel Edison == x86 модификация, никаким RISC там не пахнет.
Я бы рекомендовал изменить формулировку, чтобы не вводить в заблуждение.
grossws
01.02.2017 14:04Я также портировал все функции из API libmosquitto. Посмотреть на результат можно здесь. По ссылке дан пример использования. Все что нужно сделать для сбора данных со всех устройств внутри mesh network — это вызвать функцию subscribe() из определенного места и опубликовать метод get()!
Судя по коду вы не портировали libmosquitto, а написали wrapper для lua.
dedokOne
01.02.2017 15:49Именно портировал под tarantool. Попробую рассказать в чем отличие, отличие в том, что у tarantool есть свой собственный i/o loop, если библиотека имеет i/o и работает вне i/o loop tarantool, то такая библиотека будет блокировать работу во время I/O операций.
Другими словами, это не просто wrapper. Посмотрите внимательно на код, он в open-source.dedokOne
01.02.2017 15:51А вот и код интеграции в I/O loop tarantool: https://github.com/tarantool/mqtt/blob/master/mqtt/driver.c#L61 Этот код не совместим с обычной lua.
buran1
07.02.2017 12:11Вопрос про репликацию: какбы понятны доводы в пользу использования её, но а минусов совсем что ли нет? Частая проблема когда репликация «рвётся» приходиться же потом её восстанавливать руками, например в MySQL, да и в других субд, думаю, также или в тарантуле с этим не так?
dedokOne
07.02.2017 12:17Восстанавливать ее не надо. Допустим. У нас прервался коннект — связь потеряна, то механизм репликации _должен_ восстановить работу при возобновлении связи. Если механизм этого не делает, то это очень странно :)
В Tarantool за этим следить не надо, я не знаю MySQL, но думаю, там тоже как-то обрабатывается (по аналогии с PG и т.п.).buran1
07.02.2017 12:19он то есть "механизм", но он может не сработать, если не понятно что стало "мастером", если скажем с момента разрыва мастер-мастер репликации между двумя бд, данные поменялись в обеих бд
dedokOne
07.02.2017 12:25Ааа… понял!
Да такое может случиться — назовем это конфликтом. Конфликт можно обойти добавляя некий UUID, как уникальный PK к каждой записи. Вообще существует много вариантов как это обойти, все не перечислить, да и каждый из вариантов будет заточен под данные.dedokOne
07.02.2017 12:28Чтобы было понятно (про уникальный UUID или отказ от уникальных индексов): при M-M репликации (да и не только) на всех узлах хранится LSN (по сути время последнего изменения), merge LSN — не проблема, проблема — слить данные, которые конфликтуют.
buran1
07.02.2017 12:37Это да, но я не много другом, самом крайнем варианте так сказать: как быть, если изменения были в обеих в БД, в одинаковых записях с одинаковым ID?
Например, допустим, Вы выставляете в "главной" БД (допустим это один из "мастеров" с админкой) отключить устройство, которое сошло с ума и спамит, и связь в этот момент оборвалась. Данные(скажем флаг вкл/выкл) о том, что устройство должно перестать посылать данные не прилетят на это устройство, в добавок оно же само у себя обновит какие-то данные и будет считать, что его данные более актуальны,
будет как минимум "странная" ситуация и как максимум репликация оборвётся, если это было те же записи"/"строки".
Однако, наверное, если грамотно разграничить в архитектуре БД данные, то такого не произойдёт…
dedokOne
08.02.2017 21:21> Однако, наверное, если грамотно разграничить в архитектуре БД данные, то такого не произойдёт…
Верно.
Либо реализовать логику разучивание конфликтов — что == создать репликацию с 0.
BalinTomsk
Она должна синхронизировать данные между IoT-устройствами
а какое расстояние должно быть между девайсами? По идее 2км было бы идеально.
dedokOne
Зависит от сети, т.е. все ограничения имеют физический характер.