company_banner

Master-master репликация и масштабирование приложений между всеми IoT-устройствами и облаком


    На фото представлены устройства, использованные для прототипирования. Как видно, за основу взята процессор 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-битном окружении, что только усугубляло ситуацию. В итоге я смог решить эти проблемы. Ниже приведу правила разработки, которые мне в этом помогли.

    1. Используйте toolchain-файлы для CMake. В противном случае вы, так же как и я, потратите много времени на исправление CMake-файлов.
    2. Не используйте беззнаковый тип и другие типы, для которых не указан размер. В libc для этого есть специальные типы, такие как uint32_t. Иначе можно получить неопределенное поведение. Это правило применимо только к C/C++.
    3. Портируйте ваши автотесты.

    Ожидается, что ваши автотесты можно запустить на 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 репликации. Из этого вытекают два полезных свойства:

    1. Если сервер приложения изменяет какие-либо данные, эти обновленные данные доставляются на все IoT-устройства в сети.
    2. Если 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 и как основной фронтенд на моих выделенных серверах, и как сервер приложений. Если вас заинтересовала данная тема, рекомендую взглянуть на другую мою статью на английском языке. Оставайтесь на связи и следите на новостями!
    Mail.Ru Group 801,81
    Строим Интернет
    Поделиться публикацией
    Похожие публикации
    Комментарии 16
    • +1
      Она должна синхронизировать данные между IoT-устройствами

      а какое расстояние должно быть между девайсами? По идее 2км было бы идеально.
      • +1
        Зависит от сети, т.е. все ограничения имеют физический характер.
      • +2
        «за основу взята платформа Intel Edison, так как она поддерживает многие архитектуры, в том числе MIPS и ARM» звучит неправильно, т.к. Intel Edison == x86 модификация, никаким RISC там не пахнет.
        Я бы рекомендовал изменить формулировку, чтобы не вводить в заблуждение.
      • 0
        Я также портировал все функции из API libmosquitto. Посмотреть на результат можно здесь. По ссылке дан пример использования. Все что нужно сделать для сбора данных со всех устройств внутри mesh network — это вызвать функцию subscribe() из определенного места и опубликовать метод get()!

        Судя по коду вы не портировали libmosquitto, а написали wrapper для lua.

        • 0
          Именно портировал под tarantool. Попробую рассказать в чем отличие, отличие в том, что у tarantool есть свой собственный i/o loop, если библиотека имеет i/o и работает вне i/o loop tarantool, то такая библиотека будет блокировать работу во время I/O операций.

          Другими словами, это не просто wrapper. Посмотрите внимательно на код, он в open-source.
          • 0
            А вот и код интеграции в I/O loop tarantool: https://github.com/tarantool/mqtt/blob/master/mqtt/driver.c#L61 Этот код не совместим с обычной lua.
        • 0

          Вопрос про репликацию: какбы понятны доводы в пользу использования её, но а минусов совсем что ли нет? Частая проблема когда репликация «рвётся» приходиться же потом её восстанавливать руками, например в MySQL, да и в других субд, думаю, также или в тарантуле с этим не так?

          • 0
            Восстанавливать ее не надо. Допустим. У нас прервался коннект — связь потеряна, то механизм репликации _должен_ восстановить работу при возобновлении связи. Если механизм этого не делает, то это очень странно :)

            В Tarantool за этим следить не надо, я не знаю MySQL, но думаю, там тоже как-то обрабатывается (по аналогии с PG и т.п.).
            • 0

              он то есть "механизм", но он может не сработать, если не понятно что стало "мастером", если скажем с момента разрыва мастер-мастер репликации между двумя бд, данные поменялись в обеих бд

              • 0
                Ааа… понял!

                Да такое может случиться — назовем это конфликтом. Конфликт можно обойти добавляя некий UUID, как уникальный PK к каждой записи. Вообще существует много вариантов как это обойти, все не перечислить, да и каждый из вариантов будет заточен под данные.
                • 0
                  Чтобы было понятно (про уникальный UUID или отказ от уникальных индексов): при M-M репликации (да и не только) на всех узлах хранится LSN (по сути время последнего изменения), merge LSN — не проблема, проблема — слить данные, которые конфликтуют.
                  • 0

                    Это да, но я не много другом, самом крайнем варианте так сказать: как быть, если изменения были в обеих в БД, в одинаковых записях с одинаковым ID?


                    Например, допустим, Вы выставляете в "главной" БД (допустим это один из "мастеров" с админкой) отключить устройство, которое сошло с ума и спамит, и связь в этот момент оборвалась. Данные(скажем флаг вкл/выкл) о том, что устройство должно перестать посылать данные не прилетят на это устройство, в добавок оно же само у себя обновит какие-то данные и будет считать, что его данные более актуальны,
                    будет как минимум "странная" ситуация и как максимум репликация оборвётся, если это было те же записи"/"строки".


                    Однако, наверное, если грамотно разграничить в архитектуре БД данные, то такого не произойдёт…

                    • 0
                      > Однако, наверное, если грамотно разграничить в архитектуре БД данные, то такого не произойдёт…
                      Верно.

                      Либо реализовать логику разучивание конфликтов — что == создать репликацию с 0.
            • 0
              Подскажите, пожалуйста, ссылку на подробную инструкцию по установке из исходников Tarantool на ARM
              • 0
                Готовой сборки пакетов пока нет (собрать можно ручками — это просто).
                PS написал в личку свои контакты.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое