Обзор Akka.NET: как проектировать IoT-системы с помощью этой библиотеки
Всем привет! Меня зовут Влад Медведовский, и я уже
Вообще IoT и .NET — это слова, которые редко стояли рядом до недавних пор. Но с развитием Azure-сервисов среди них появились и такие, как, например, IoT gateway. И теперь есть возможность строить решения для IoT на платформе .NET. К Azure есть ряд вопросов, а именно:
- Итоговая стоимость обслуживания решения. На начальных этапах она не высока, но с ростом нагрузки и количества устройств стоимость будет расти. Не всегда у заказчика есть на это бюджет.
- На предприятиях уже есть сетевая инфраструктура, и не всегда целесообразно (и возможно) переносить ее в облако.
- Иногда данные с устройств нельзя хранить в публичном облаке, если они представляют собой конфиденциальную информацию — привет, GDPR :)
- Некоторые устройства не поддерживают популярные протоколы типа MQTT, зато предоставляют свой уникальный протокол. Еще лучше, если устройство не поддерживает TCP-транспорт и push-модель, а, например, пишет какие-то файлы на FTP-сервер. В таком случае приходится периодически опрашивать FTP на предмет наличия новых данных.
Если вы только собираетесь проектировать IoT-решение или столкнулись с какой-то из проблем, обозначенных выше, а может, вас не устраивают готовые Azure-сервисы — добро пожаловать.
Традиционные подходы для решения IoT-задач
Из каких основных частей состоит решение для IoT? Обычно, если опустить cross-cutting concerns вроде авторизации, мониторинга и логирования, то это:
- IoT-устройство;
- gateway;
- business logic API.
Никакое IoT-решение не существует без устройств, которые взаимодействуют с физическим миром. Обычно IoT-устройства проектируют автономными, а это накладывает ограничения на мощность передатчика. Поэтому для группы устройств часто добавляют стационарный gateway, в которого таких ограничений нет.
Gateway — шлюз, который обрабатывает запросы от конечных устройств. Он не только выполняет функцию ретрансляции, но и разбирает протокол, понятный API/бэкенду и по которому общается оконечное устройство, на команды. Бэкенд, в свою очередь, содержит код, что обычно реализует все бизнес-правила системы.
Для наиболее распространенных сценариев в Azure есть набор готовых сервисов. Из них можно собрать архитектуру, которая представлена на рисунке 1.
Рис. 1. Эталонная архитектура IoT-решения, по мнению Microsoft Docs
Ряд основных недостатков такой архитектуры:
- сложности, связанные с интеграцией и развертыванием каждой подсистемы;
- необходимость контроля версий различных подсистем и их совместимости между собой;
- необходимость иметь raytracing для отладки;
- необходимость иметь развитую систему мониторинга для наблюдения за отдельными компонентами системы и системой целиком;
- «зоопарк» используемых сервисов и API в проекте:
- повышает порог входа для новичков;
- требует ведения документации, чтобы знания об архитектуре не потерялись.
Устранение хотя бы некоторых из вышеперечисленных недостатков требует определенного уровня навыков и дисциплины в команде. Мой опыт говорит, что это присутствует не всегда и не везде. Иногда вмешиваются дедлайны, иногда нужно инвестировать время и деньги в обучение разработчиков. Эти ресурсы (время и деньги) тоже доступны не всегда.
Акторы
Какие же существуют альтернативы подходам, построенным вокруг сервисов и традиционного слоеного подхода к проектированию IoT-систем? Попробуем представить каждое устройство или вообще каждую сущность системы как отдельный объект, который будем называть актором.
Более того, так как речь идет об устройствах, мы можем добавить ряд естественных ограничений на взаимодействие между акторами.
Акторы не могут напрямую вызывать методы друг друга. В реальности устройства используют асинхронную сеть для различного рода коммуникаций (P2P, Client-server). Вместо этого коммуникация осуществляется посредством обмена сообщениями.
IoT-устройства довольно ограничены в вычислительных мощностях. Поэтому мы можем рассматривать каждый актор как однопоточный. Это означает, что он может обрабатывать только одно сообщение в каждый момент времени.
У каждого актора должен быть уникальный адрес для коммуникации. Без него невозможно доставить сообщение тому адресату, которому оно предназначалось.
Акторы должны иметь возможность создавать другие акторы. Например, для сценария обнаружения новых устройств путем broadcast-сообщений внутри сети. Тогда устройство, которое осуществляет broadcasting, будет получать acknowledge-пакеты на свой адрес, а значит, обладать информацией про найденные устройства. В этом случае выглядит естественным, если программная сущность будет создавать дочерние самостоятельно.
Mailbox. Поскольку акторы однопоточные по своей природе, есть смысл ввести для каждого из них некую очередь, в которую будут поступать все входящие сообщения. Такая очередь в терминологии акторов называется mailbox. По умолчанию сообщения из mailbox обрабатываются по принципу FIFO — first in, first out. Однако на практике необходимо некоторые сообщения обрабатывать с повышенным или пониженным приоритетом. Это сообщения тревоги, сообщения об ошибках или о статусе устройства, которые требуют срочной реакции. Например, есть устройства, которые умеют сигнализировать о том, что их пытаются взломать (можно поискать по anti-tampering protection). Очевидно, получение такого сообщения требует быстрой реакции оператора.
Akka.NET
Все вышеперечисленное предоставляет библиотека под названием Akka.NET. Это, по сути, безальтернативная реализация модели акторов в среде .NET. Есть API как для C#, так и для F#.
Краткий список возможностей библиотеки:
- базовые типы для создания акторов;
- большое количество готовых типов для обычных задач вроде маршрутизации сообщений, балансировки нагрузки между акторами и так далее;
- управление mailbox и разные варианты реализации для него;
- «среда выполнения» для акторов — Actor System;
- управление именами акторов (впрочем, всегда можно написать что-то свое);
- удаленный обмен сообщениями с другими акторами по сети;
- кластеризация системы акторов — возможность запустить приложение из коробки на нескольких машинах;
- встроенная очередь для dead letter;
- поддержка DI;
- поддержка персистентных акторов.
Сразу перечислю некоторые недостатки:
- Сложная и неочевидная конфигурация библиотеки:
- не все задокументированные настройки работают;
- некоторые не работают так, как задумано;
- некоторые вообще не работают;
- Непривычные подходы к именованию (dead letters, например) и концепции некоторых сущностей в библиотеке. Не все сразу понятно, документация тоже не всегда помогает. Благо, Akka.NET — это библиотека с открытым исходным кодом и всегда можно взять исходники, собрать из них библиотеку и посмотреть, что там и как внутри происходит.
- Крутая кривая обучения. Гайд по библиотеке ровно один, плюс есть несколько примеров (которые не всегда рабочие). Документация не всегда исчерпывающая.
Routers and routing
В Akka.NET есть ряд встроенных типов, которые можно использовать для поочередной отправки сообщений группам акторов. Эти типы называют Routers. В библиотеке есть несколько реализаций для самых частых сценариев:
- Round-robin — последовательная отправка сообщений акторам.
- Hash key — адресация по ключу в сообщении.
- Random — адресат выбирается случайным образом.
- Weighted round robin — то же, что и Round-robin, но с помощью весовых коэффициентов можно настраивать частоту отправки сообщений конкретным адресатам.
- Broadcast — сообщение отправляется всем акторам из группы.
Akka.NET use case
Перейдем теперь к реальному примеру использования Akka.NET. Есть некоторая система, которая обрабатывает информацию с датчиков. Как уже было сказано, датчики не подключены напрямую к серверу системы из-за довольно скромных возможностей для автономной связи на дальние расстояния и особенностей топологии их размещения.
Информация с оконечных устройств поступает в систему через стационарные шлюзы.
Представим каждый шлюз в виде актора. Тогда актор шлюза может создавать акторы оконечных устройств, которые будут к нему подключены. Таким образом у нас получилась небольшая иерархия устройств.
Добавим в эту иерархию актор, который будет обрабатывать ошибки оконечных устройств — между шлюзом и датчиком. Он будет периодически опрашивать датчик на предмет его состояния и отправлять команду перезагрузки, если что-то с оконечным устройством не так. Добавим также актор над шлюзом, который возьмет на себя управление нестабильным TCP-соединением и будет заниматься буферизацией, повторной отправкой сообщений от шлюза на сервер при необходимости.
На минутку перестанем добавлять акторы, остановимся и осмотримся. Как видно, основной способ расширения системы — это добавление в нее новых акторов. Они могут брать на себя произвольные функции — как расширение поведения существующих акторов, так и полностью новые сценарии в системе. Это позволяет расширять систему с минимальными затратами на внедрение нового функционала.
Вернемся к нашему примеру со шлюзом и оконечными устройствами, которые к нему подключены. Усложняем задачу: у нас есть 6 устройств, которые измеряют наличие или отсутствие группы химических соединений в жидкости в двух параллельных трубопроводах.
Устройства производят измерения по команде — погружают измерительный стержень, а затем каким-то образом читают показания. Сами устройства дублируют друг друга (группами по 3) и разделены на две группы. Нет смысла постоянно запрашивать измерения со всех шести датчиков — это нецелесообразно, потому что реактивы не дешевые. Вместо этого мы можем по очереди опрашивать каждую группу и отправлять команду на измерение с помощью Round-robin router.
Рис. 2. Получившаяся иерархия акторов
Это был пример базового сценария использования Akka.NET. Перейдем теперь к более сложному. Естественный вопрос, который хочется задать следующим — как мне масштабировать систему? Как сделать её более отказоустойчивой?
Akka Remoting
Прежде чем затрагивать вопросы про масштабируемость, нужно понять, как акторы на разных физических машинах обмениваются сообщениями. За счет ссылочной прозрачности и асинхронной природы обмена сообщениями в модели акторов легко сделать переход от in-process коммуникации до сетевой. Это принципиально отличает модель акторов от, например, DCOM или RPC (в виде WCF к примеру), где за синхронными вызовами методов стояли асинхронные вызовы по сети.
В Akka.NET уже есть расширение для обмена сообщениями — Akka.Remoting. Что умеет Akka.Remoting:
- отправлять сообщения на удаленные акторы;
- развертывать акторы удаленно (на принимающей стороне);
- есть возможность расширения для поддержки других сетевых протоколов, не только TCP и UDP.
Важные замечания при использовании Akka.Remoting
При использовании Akka для сетевой коммуникации важно ДО того, как проект начинает разрастаться, продумать, как будут сериализоваться сообщения.
Сериализатор практически невозможно поменять после того, как ваш проект достигнет определенных размеров. Вам нужно будет поддерживать две параллельные копии системы с разными сериализаторами для апгрейда — Akka.NET (впрочем, как и любая другая распределенная система) не сможет десериализовать сообщения, если они были упакованы другим способом. Никакой метаинформации про тип транспортного протокола с сообщениями не передается. Впрочем, всегда можно написать свой сериализатор, который будет абстракцией над механизмом сериализации.
Akka Cluster
Если мы хотим объединить множество акторов, размещенных на разных машинах, в один логический кластер, то для этого существует специальное расширение — Akka.Cluster. Оно реализует логику работы кластера акторов поверх Akka.Remoting. Поговорим о том, как работает кластер в Akka.NET. Для работы кластера акторов необходимо, чтобы выполнялись следующие ограничения.
На каждой машине система акторов (ActorSystem) должна иметь одинаковое название. Не обязательно, чтобы в каждой системе был один и тот же набор акторов, важно именно название. Название системы принимает участие в построении адреса к актору внутри кластера.
В кластере обязательныо должны присутствовать seed nodes. Akka.Cluster реализован поверх протокола Gossip (рис. 3). Назначение протокола — в per-to-peer обмене информацией между узлами сети или, в нашем случае — информацией о машинах в кластере. Сам по себе протокол никак не регламентирует добавление новых участников сети в кластер. Чтобы обойти это, в Akka.Cluster есть seed nodes — это точки входа в кластер. Они указываются как доменные имена/адреса узлов в соответствующей секции конфигурационного файла приложения. Если в кластере нет seed node или она вышла из строя, то новые машины не смогут присоединиться к кластеру. Поэтому имеет смысл держать 2 и более seed nodes для обеспечения отказоустойчивости кластера.
Рис. 3. Схема работы Gossip протокола (рисунок из документации)
Akka.NET use case — cluster
Вернемся к нашему примеру. В реальности датчики довольно старые. Каждая группа датчиков была подключена к х86-устройству с Windows для считывания показаний через COM-порт. До внедрения решения поверх акторов сотрудниками предприятия вручную совершался обход и собирались файлы с информацией от датчиков. Немного расширяем схему с рисунка 2 и получаем:
Рис. 4. Кластер из измерительных устройств
Единственное отличие от предыдущего примера — в схеме появились at-least-once delivery акторы, которые отвечают за гарантированную доставку сообщений на центральный сервер. Для их реализации были взяты персистентные акторы из Akka.NET, немного модифицированы для использования оперативной памяти в качестве хранилища отправленных/доставленных сообщений.
В итоге благодаря Akka.NET получилось построить простое и понятное IoT-решение для Legacy-устройств с поддержкой масштабирования, если вдруг это понадобится в будущем. От базового варианта, который работал на одном сервере, без особых усилий получилось перейти к распределенному решению, которое уже можно разворачивать в кластере. При этом акторы, которые отвечают за бизнес-логику, остались неизменны — добавление обмена сообщениями между ними по сети никак не повлияло на первоначальный пример.
Выводы
Akka.NET — это неплохая альтернатива конвенциональным подходам для построения распределенных систем. Из коробки вы получаете большое количество расширяемой и модифицируемой функциональности. Модули в библиотеке достаточно независимы друг от друга. Используя предоставляемую функциональность как фундамент, можно ускорить разработку IoT-решения и упростить его поддержку. К тому же базовые концепции библиотеки довольно простые, и при правильном подходе к разделению ответственности между акторами можно построить легко расширяемую систему.
Полезные ресурсы:
- документация Akka.NET с примерами использования;
- простой Service Discovery для Akka.Cluster;
- сериализатор сообщений по умолчанию в Аkka.NET;
- стоит обратить внимание на набор поддерживаемых адаптеров для actor persistence (Redis, Cassandra etc.).