company_banner

IoT там, где вы не ждали. Разработка и тестирование (часть 2)

    Продолжение первой части статьи «IoT там, где вы не ждали. Разработка и тестирование (часть 1)» не заставила себя долго ждать. На этот раз я расскажу, какая была архитектура проекта и на какие грабли мы наступили, когда начали тестировать наше решение.

    Disclaimer: ни одна мусорка сильно не пострадала.



    Архитектура проекта


    У нас получился типичный микросервисный проект. Слой микросервисов «нижнего уровня» принимает данные с устройств и датчиков, складирует их в Kafka, после чего уже бизнес-ориентированные микросервисы могут работать с данными полученными из Kafka, чтобы показать текущее состояние устройств, построить аналитику и подстроить свои модели.



    Kafka в IoT-проекте зашла очень круто. По сравнению с другими системами вроде RabbitMQ, Kafka имеет несколько преимуществ:

    • Работа со стримами: сырые данные с датчиков можно обработать в получить стрим. А со стримами вы можете гибко настроить, что вы хотите отфильтровать в них, и легко сделать рестриминг (сформировать новые потоки данных)
    • Логирование: Kafka больше похожа на журнал, и благодаря структуре данных типа лога мы можем хранить данные, например, за полгода. Из-за того, что сервисы из бизнес-части разрабатывались разными командами, новые микросервисы добавлялись в проект постепенно. При внедрении сервиса в инфраструктуру мы могли прогнать на нем все данные, которые были получены в Kafka с начала времен, и построить его модель. Это очень удобно, когда ты первый раз запускаешь приложение, а у тебя уже есть база событий, полученных с реальных устройств за предыдущий месяц.

    Тестируем Backend


    Для начала посмотрим на привычный нам бэкенд, весь бизнес-слой приложения построен на одном стеке Java и Spring. Для тестирования микросервисных приложений в реальном окружении мы используем библиотеку test-containers. Она позволяет легко развернуть внешнюю обвязку (Kafka, PostgreSQL, MongoDB и т.д.) в Docker.

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

    О том, как именно мы это делаем, я подробно рассказывал на Heisenbug 2019 Piter в докладе «Микросервисные войны: JUnit эпизод 5 — TestContainers наносит ответный удар»:


    Давайте посмотрим на небольшом примере, как это выглядело. Сервис нижнего уровня берет данные c девайсов и кидает их в Kafka. А сервис «Heat Map» из бизнес-части забирает данные из kafka и строит тепловую карту.



    Давайте протестируем получение данных сервисом «Heat Map» (через Kafka)

    @KafkaTestContainer
    @SpringBootTest
    class KafkaIntegrationTest {
    
        @Autowired
        private KafkaTemplate kafkaTemplate;
    
        @Test
        void sendTest() {
            kafkaTemplate.send(TOPIC_NAME, MEASSAGE_BODY)
    
            // тут нужно дождаться получения данных
            // и можно проверять что логика их обработки
            // реализована правильно
        }
    }
    

    Мы пишем обычный интеграционный SpringBoot-тест, однако он отличается annotation-based стилем конфигурации тестового окружения. Аннотация @KafkaTestContainer нужна для поднятия Kafka. Для ее использования нужно подключить библиотеку:

    spring-test-kafka

    При старте приложения запускается Spring и стартует Kafka контейнер в Docker. Затем в самом тесте мы используем kafkaTemplate, инжектим его в тестовый кейс и отправляем данные в Kafka, чтобы протестировать логику обработки новых данных из топика.

    Все это происходит на нормальном инстансе Kafka, никаких embedded-вариантов, а только та версия, что крутится в продакшене.



    Сервис Heat Map использует в качестве хранилища MongoDB, и тест для MongoDB выглядит похожим образом:

    @MongoDbDataTest
    class SensorDataRecordServiceTest {
    
        @Autowired
        private SensorDataRecordRepository repository;
    
        @Test
        @MongoDataSet(value ="sensor_data.json")
        void findSingle() {
            var log = repository.findAllByDeviceId("001");
            assertThat(log).hasSize(1);
            ...
        }
    }
    

    Аннотация @MongoDbDataTest запускает MongoDB в Docker аналогично Kafka. После того, как приложение было запущено, мы можем пользоваться репозиторием для работы с MongoDB.

    Для использования в ваших тестах этого функционала, все что нужно — это подключить библиотеку:

    spring-test-mongo

    К слову сказать, там есть много других полезностей, например, можно перед выполнением теста загружать в базу данные, через аннотацию @MongoDataSet как в примере выше, или при помощи аннотации @ExpectedMongoDataSet проверить, что после выполнения тестового кейса в базе появился именно тот набор данных, который мы ожидаем.

    Подробнее о работе с тестовыми данными я расскажу на Heisenbug 2020 Piter, который пройдет 15-18 июня в онлайне.


    Тестируем IoT-специфичные вещи


    Если бизнес-часть — это типичный бэкенд, то работа с данными с девайсов содержала много граблей и специфики, связанной с железом.

    У вас есть девайс, и вам надо с ним сопрягаться. Для этого вам понадобится документация. Хорошо, когда у вас на руках и железка и доки к ней. Однако у нас все начиналось по-другому: была только документация, а девайс был еще в пути. Мы запилили небольшое приложение, которое в теории должно было работать, но как только приехали реальные девайсы, наши ожидания столкнулись с реальностью.

    Мы думали, что на входе будет бинарный формат, а устройство стало кидать в нас каким-то XML-файлом. И в такой жесткой форме родилось первое правило для IoT-проекта:

    Никогда не верь документации!

    В принципе, полученные с устройства данные были более-менее понятные: Time — это временная метка, DevEUI — идентификатор девайса, LrrLAT и LrrLON — координаты.



    А вот что такое payload_hex? Мы видим 8 разрядов, что в них может быть? Это дистанция до мусора, напряжение датчика, уровень сигнала, угол наклона, температура или вообще все вместе взятое? В какой-то момент мы думали, что китайские производители этих девайсов познали какой-то фен-шуй архивирования и смогли в 8 разрядов запаковать все, что только можно. Но если посмотреть выше, то видно, что время записано обычной строкой и содержит в 3 раза больше разрядов, то есть байты явно никто не экономил. В итоге выяснилось, что конкретно в этой прошивке половина датчиков в девайсе просто выключена, и нужно ждать новую прошивку.

    Пока ждали, мы сделали в офисе тестовый стенд, который, по сути, был обычной картонной коробкой. На ее крышку мы прикрепили устройство и закидывали в коробку всякий офисный хлам. Еще нам нужен был тестовый экземпляр машины перевозчика, и ее роль исполняла машина одного из разработчиков в проекте.

    Теперь мы видели на карте, где стоят картонные коробки, и знали, куда ездил разработчик (спойлер: работа-дом, и по вечерам пятницы никто бар не отменял).



    Однако система с тестовыми стендами просуществовала недолго, потому что есть большие отличия от реальных контейнеров. Например, если говорить про акселерометр, то мы крутили коробку из стороны в сторону и получали показания с датчика, и вроде все работало. Но в реальности есть некие ограничения.



    В первых версиях девайса угол измерялся не в абсолютных значениях, а в относительных. И когда коробку наклоняли больше, чем на фиксированную в прошивке дельту, то датчик начинал работать некорректно или даже мог не зафиксировать поворот.



    Конечно, все эти ошибки в процессе исправили, но на старте различия между коробкой и контейнером принесли много проблем. И мы просверлили бак со всех сторон, пока решали, как расположить датчик в контейнере, чтобы при подъеме бака машиной перевозчика мы точно фиксировали, что мусор выгружают.

    Помимо проблемы с углом наклона, мы поначалу не учли, каким будет реальный мусор в контейнере. И если мы кидали в ту коробку пенопласт да подушки, то в реальности люди складывают в контейнер все подряд, даже цемент с песком. И в результате однажды датчик показывал, что контейнер пуст, хотя на самом деле тот был заполнен. Как выяснилось, кто-то во время ремонта выкинул крутой звукопоглощающий материал, который гасил сигналы от датчика.

    В этот момент мы решили договориться с арендодателем бизнес-центра, где находится офис, чтобы установить датчики на его мусорные контейнеры. Мы оборудовали площадку перед офисом, и с этого момента жизнь и будни разработчиков проекта кардинально изменились. Обычно в начале рабочего дня ты хочешь выпить кофе, почитать новости, а тут у тебя вся лента забита мусором, буквально:



    При тестировании температурного датчика, как и в случае с акселерометром, реальность преподнесла новые сценарии. Пороговое значение для температуры довольно сложно подобрать так, чтобы мы вовремя узнали, что датчик горит, и не распрощались с ним. Например, летом контейнеры сильно нагреваются под солнцем, и установка слишком малой пороговой температуры чревата постоянными уведомлениями от датчика. А если девайс реально горит, и кто-то начинает его тушить, то нужно готовиться к тому, что бак зальют доверху водой, потом кто-нибудь его уронит, и тушить будут уже на полу. При таком сценарии датчик, очевидно, не выживет.


    В какой-то момент к нам пришла новая прошивка (вы же помните, что мы ее ждали?). Мы накатили ее на датчики, и протокол связи с датчиком вновь сломался. Прощай, XML, и да здравствует опять бинарный формат. Хочется надеяться в этот момент, что сейчас он соответствует документации, но… нет же!
    Поэтому второе правило: читай первое правило. То есть — никогда не верь документации.
    Что можно сделать? Например, заняться реверс-инжинирингом: сидим с консолью, собираем данные, крутим-вертим датчик, ставим что-то перед ним, пытаемся выявить закономерности. Так можно вычленить дистанцию, статус контейнера и контрольную сумму. Однако некоторые данные было сложно интерпретировать, потому что наши китайские производители девайсов, видимо, любят велосипеды. И чтобы запаковать в бинарный формат число с плавающей точкой для интерпретации угла наклона, они решили взять два байта и разделить на 35.



    И во всей этой истории нам сильно помогло то, что нижний слой сервисов, работающий с девайсами, был изолирован от верхнего, и все данные переливались через кафку, контракты на которую были согласованы и закреплены.

    Это сильно помогало в плане разработки, поскольку если поломался нижний уровень, то мы спокойно пилим бизнес-сервисы, потому что контракт в них жестко зафиксирован. Поэтому настоящее второе правило разработки IoT-проектов — это изолировать сервисы и использовать контракты.
    В докладе было еще много интересного: имитационное моделирование, нагрузочное тестирование, да и в целом я бы посоветовал посмотреть этот доклад.

    В третьей части я расскажу про имитационную модель, stay tuned!
    JUG Ru Group
    Конференции для программистов и сочувствующих. 18+

    Комментарии 2

      0
      Интересный проект… для цивилизованных стран. А по поводу китайского железа, в частности промышленных контроллеров, все хорошо — и цена, и функционал, вот только если нужно баг исправить, то придется общаться с техподдержкой, а хуже китайского английского, может быть только индусский английский ) И еще из опыта касательно документации — пользоваться переводом нельзя, только оригинал руководства.
        0
        да, все именно так!
        с китайской документацией тоже поймали веселья, она даже на английском выглядела странно, чаще приходилось догадываться что это значит и реверс-инженерить…

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

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