Embedded HandbookПриложения
Все разделы › X. Проектирование, сети и IoT
STM32F103, STM32F407 · уровень базовый · 12 мин чтения
Открыть в интерактивном справочнике

87. Виртуализация, облака, контейнеры

Прибор — только половина системы. Вторая половина — сервер: брокер, который принимает сообщения, база, которая их хранит, и панели, на которых их смотрят. Сегодня такой сервер почти никогда не ставят «на голое железо»: его запускают в виртуальной машине, в контейнере или арендуют в облаке. Разработчику прибора это важно понимать хотя бы для того, чтобы собрать себе стенд и не зашить в прошивку то, что завтра переедет.

В разделе проделан настоящий опыт: брокер MQTT запущен в контейнере, и ему отправлены пакеты, собранные переносимым кодом на C — тем же, что собирается armclang для Cortex-M.

Как это называется в документации

Оригинал Перевод
virtual machine (VM) виртуальная машина
hypervisor, type 1 / type 2 гипервизор: на железе / поверх системы
container, image, registry контейнер, образ, хранилище образов
volume том — данные вне контейнера
Docker Compose описание набора контейнеров одним файлом
IaaS, PaaS, SaaS инфраструктура, платформа, программа как услуга
public / private / hybrid cloud публичное, частное, гибридное облако
serverless бессерверные вычисления — функции по событиям
SLA соглашение об уровне обслуживания
vendor lock-in привязка к поставщику
grid computing распределённые вычисления на многих машинах

87.1 Виртуальная машина

Виртуальная машина — целый компьютер в программе: со своим процессором, памятью, диском и своей операционной системой. Создаёт и разделяет их гипервизор. Он бывает двух видов:

Что даёт виртуальная машина: изоляцию (сбой или взлом одной не задевает соседей), запуск несовместимого (Linux на Windows), снимок состояния и откат, перенос на другое железо целиком. Цена — каждая машина несёт своё ядро и свою систему: гигабайты на диске, память под систему, загрузку как у настоящего компьютера.


87.2 Контейнер

Контейнер изолирует не компьютер, а процесс: у него своя файловая система, свои сеть и процессы, но ядро — общее с хозяином. Это видно прямо: в проверке запущены два контейнера — брокер MQTT на Alpine Linux и среда сборки на Ubuntu. Оба сообщили одно и то же ядро, 6.6.87.2-microsoft-standard-WSL2, — ядро виртуальной машины, в которой Docker работает под Windows. Разные системы, одно ядро.

Поэтому контейнер лёгкий: образ брокера — порядка десяти мегабайт, запускается за доли секунды, и на одной машине их работают десятки.

Виртуальная машина Контейнер
изолирует компьютер целиком процесс
ядро своё общее с хозяином
размер гигабайты мегабайты — сотни мегабайт
запуск как загрузка компьютера как запуск программы
другая система любая только с тем же ядром (Linux в Linux)
изоляция сильнее слабее: общее ядро

У каждой виртуальной машины своё ядро, у контейнеров — одно общее с хозяином

Три понятия, без которых с контейнерами не работают:


87.3 Облака

Облако — это аренда вычислительных ресурсов как услуги: платят за то, что используют, и получают их по запросу. Модели различаются тем, чем управляет арендатор, а чем — поставщик:

Слой Своё железо IaaS PaaS SaaS
железо, сеть, питание вы поставщик поставщик поставщик
виртуализация вы поставщик поставщик поставщик
система, обновления вы вы поставщик поставщик
среда выполнения, база вы вы поставщик поставщик
приложение вы вы вы поставщик
данные вы вы вы вы — но у поставщика

IaaS — арендованная виртуальная машина; PaaS — площадка, куда кладут только своё приложение; SaaS — готовая программа в браузере, в том числе готовые платформы интернета вещей (раздел 91). Бессерверные вычисления — функции, которые запускаются на каждое событие, например на каждое сообщение от прибора, и оплачиваются за вызовы.

Частное облако компания держит для себя, публичное сдаёт всем, гибридное совмещает оба: чувствительные данные — у себя, пиковые вычисления — в аренде.

Плюсы облака — не нужно своё железо, легко расти, высокая доступность по договору (SLA). Минусы — зависимость от сети и от поставщика, данные лежат у третьей стороны, а переезд к другому поставщику бывает дорог.

Для прошивки из этого следует одно правило: прибор не должен знать, где живёт сервер. В прошивке — имя (раздел 86.4) и порт, причём так, чтобы их можно было сменить без перепрошивки: через настройки или обновление по воздуху (раздел 48). Переезд сервера в другое облако тогда не превращается в выезд к каждому прибору.


87.4 Стек для приборов в контейнерах

Типовой набор для сбора и показа данных с приборов:

Контейнер Роль
брокер MQTT (Mosquitto) принимает сообщения приборов (раздел 37.2)
сборщик (Telegraf) подписывается на топики и пишет значения в базу
база временных рядов (InfluxDB) хранит измерения с отметкой времени
панели (Grafana) графики и тревоги в браузере
потоки (Node-RED) по желанию: правила «если — то» мышкой

Весь стек описывают одним файлом Compose:

services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports: ["1883:1883"]
    volumes: ["./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro",
              "./passwd:/mosquitto/config/passwd:ro"]
    restart: unless-stopped
  influxdb:
    image: influxdb:2.7
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME: admin
      DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD:?задайте в .env}
      DOCKER_INFLUXDB_INIT_ORG: lab
      DOCKER_INFLUXDB_INIT_BUCKET: sensors
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN:?задайте в .env}
    volumes: ["influx-data:/var/lib/influxdb2"]
    restart: unless-stopped
  telegraf:
    image: telegraf:1.30
    environment:
      INFLUX_TOKEN: ${INFLUX_TOKEN:?задайте в .env}
    volumes: ["./telegraf.conf:/etc/telegraf/telegraf.conf:ro"]
    depends_on: [mosquitto, influxdb]
    restart: unless-stopped
  grafana:
    image: grafana/grafana:10.4.0
    ports: ["3000:3000"]
    volumes: ["grafana-data:/var/lib/grafana"]
    depends_on: [influxdb]
    restart: unless-stopped
volumes:
  influx-data:
  grafana-data:

Три привычки, заложенные в этот файл:

Файлы настроек лежат рядом с Compose. Без mosquitto.conf Docker создаст на его месте пустую папку, и брокер не запустится: «Are you trying to mount a directory onto a file». С пустым файлом он запустится, но будет слушать только внутри своего контейнера — в журнале «Starting in local only mode», — и до него не достучатся ни приборы, ни Telegraf, хотя порт опубликован. Минимум — три строки:

listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd

Файл паролей создаёт программа из того же образа — она спросит пароль:

docker run --rm -it -v "$PWD:/w" eclipse-mosquitto:2 \
  sh -c "mosquitto_passwd -c /w/passwd sensor1 && chown 1883:1883 /w/passwd"

Файл создаётся с доступом только для владельца, а брокер работает от пользователя 1883; без chown он файл не прочтёт и не запустится: «Unable to open pwfile». Прибор без пароля брокер не пустит: «not authorised». В telegraf.conf — вход [[inputs.mqtt_consumer]] с адресом tcp://mosquitto:1883, темами, именем и паролем и выход [[outputs.influxdb_v2]] с адресом http://influxdb:8086, токеном ${INFLUX_TOKEN}, организацией lab и корзиной sensors.

Запись 1883:1883 открывает порт на всех сетевых интерфейсах машины: docker port показывает 0.0.0.0:1883 и [::]:1883. Правила для опубликованных портов Docker вписывает в iptables сам, мимо ufw, — порт, «закрытый» брандмауэром, окажется открыт. Чтобы брокер видели только приборы своей сети, перед портом пишут адрес её интерфейса: 192.168.1.10:1883:1883; только эта машина — 127.0.0.1:1883:1883. В интернет брокер выставляют лишь с паролями и TLS (раздел 37.4), обычно на порту 8883.


87.5 Опыт: прибор говорит с настоящим брокером

Пакеты MQTT прибор собирает сам — это несколько десятков строк на C, которые не зависят ни от сети, ни от операционной системы:

/* Длина остатка: от 1 до 4 байт по 7 бит, старший бит — «дальше ещё» */
size_t mqtt_put_len(uint8_t *p, uint32_t n)
{
    size_t k = 0;
    do {
        uint8_t b = (uint8_t)(n % 128);
        n /= 128;
        p[k++] = n ? (uint8_t)(b | 0x80) : b;
    } while (n);
    return k;
}

/* CONNECT: протокол «MQTT» уровня 4 (3.1.1), чистая сессия, keep-alive в секундах */
size_t mqtt_connect(uint8_t *buf, const char *client_id, uint16_t keepalive)
{
    uint32_t rem = 10u + 2u + (uint32_t)strlen(client_id);
    size_t k = 0;
    buf[k++] = 0x10;
    k += mqtt_put_len(buf + k, rem);
    k += put_str(buf + k, "MQTT");
    buf[k++] = 4;                           /* уровень протокола */
    buf[k++] = 0x02;                        /* флаги: чистая сессия */
    buf[k++] = (uint8_t)(keepalive >> 8);
    buf[k++] = (uint8_t)keepalive;
    k += put_str(buf + k, client_id);
    return k;
}

/* PUBLISH с QoS 0: заголовок 0x30, топик и данные — без номера пакета */
size_t mqtt_publish(uint8_t *buf, const char *topic, const void *data, size_t len)
{
    uint32_t rem = 2u + (uint32_t)strlen(topic) + (uint32_t)len;
    size_t k = 0;
    buf[k++] = 0x30;
    k += mqtt_put_len(buf + k, rem);
    k += put_str(buf + k, topic);
    memcpy(buf + k, data, len);
    return k + len;
}

Пакеты пишутся прямо в буфер передачи, без промежуточных копий. Какой транспорт их отправит — сокет стека TCP/IP, микросхема со стеком или модуль с AT-командами (раздел 37.1), — этому коду безразлично.

Опыт. В сети контейнеров запущены три участника:

  1. брокер Mosquitto — под именем mq;
  2. подписчик — клиент mosquitto_sub на топик stm/test;
  3. «прибор» — тот же mqtt87.c, собранный для компьютера, который подключается к mq по TCP и публикует t=23.5.

Что получилось:

Шаг Результат
CONNECT от прибора 22 байта: 10 14 00 04 MQTT 04 02 00 3C 00 08 stm32-87
ответ брокера CONNACK из 4 байт, код 0 — принят
в журнале брокера клиент stm32-87: протокол 4, чистая сессия, keep-alive 60
PUBLISH 18 байт
подписчик получил stm/test t=23.5

Журнал брокера повторил ровно те флаги, что стоят в байтах CONNECT, а байты совпали с кодировщиком, заново написанным на Python. Длина остатка занимает один байт до 127, два — с 128, три — с 16 384, четыре — с 2 097 152; больше 268 435 455 пакет быть не может.

Длина остатка MQTT: 7 бит на байт — до 127 хватает одного, больше 268 435 455 не бывает

⚠️ Брокер в опыте запущен без авторизации — только внутри закрытой сети контейнеров. Наружу брокер выставляют с паролями и шифрованием (раздел 37.4).


87.6 Распределённые вычисления

Когда задача слишком велика для одной машины, её режут на куски и раздают многим — в кластере или прямо через интернет. Так считают погоду, обучают большие модели и ищут лекарства. Приборам это нужно редко, но сама идея — раздать работу многим узлам и собрать ответ — продолжается в разделе 88.


87.7 Грабли

После переезда сервера все приборы молчат. В прошивку вписан адрес, а не имя, или имя нельзя сменить без перепрошивки.

После обновления контейнера пропали все данные. База хранилась внутри контейнера, а не в томе.

Стек однажды не поднялся сам. Образы без номера версии: подтянулась новая, несовместимая.

Пароль от базы оказался в репозитории. Секреты вписаны прямо в файл Compose, а не в .env.

Брокер открыт всему интернету. Порт 1883 опубликован без адреса, паролей и шифрования; ufw такой порт не закрывает.

Приборы не подключаются, хотя порт опубликован. Брокер без своего mosquitto.conf слушает только внутри контейнера: «local only mode».

Контейнер не находит соседа по имени. Контейнеры в разных сетях или обращение идёт по адресу машины, а не по имени в сети контейнеров.

Под Windows путь внутри контейнера превратился в C:/Program Files/…. Командная оболочка Git Bash переписывает аргументы, похожие на пути. Для команд Docker её отключают: MSYS_NO_PATHCONV=1. На этом споткнулся и опыт из 87.5 — брокер не нашёл свой файл настроек.

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


87.8 Итоги

Дальше: раздел 88 «Распределённые системы и API».

Открыть в интерактивном справочнике