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) |
| изоляция | сильнее | слабее: общее ядро |
Три понятия, без которых с контейнерами не работают:
- образ — неизменяемый шаблон: система, программа, настройки; контейнер — запущенный экземпляр образа. Образы берут из хранилища с номером версии;
- том — каталог вне контейнера. Контейнер одноразовый: обновили образ — старый контейнер удалили, новый создали. Всё, что должно пережить это, — база, настройки, журналы — хранится в томах;
- сеть контейнеров — внутри неё контейнеры находят друг друга по
именам. В опыте ниже публикатор подключается к брокеру просто по имени
mq.
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:
Три привычки, заложенные в этот файл:
- номера версий у образов — не «последняя»: иначе однажды стек поднимется с новой версией и сломается без вашего участия;
- данные в томах — база и панели переживают обновление контейнеров;
- секретов в файле нет: пароль и токен приходят из файла
.envрядом, который не кладут в git. Запись${INFLUX_PASSWORD:?…}делает их обязательными — проверено: без них Compose отказывается собирать стек и прямо пишет, чего не хватает.
Файлы настроек лежат рядом с 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), — этому коду безразлично.
Опыт. В сети контейнеров запущены три участника:
- брокер Mosquitto — под именем
mq; - подписчик — клиент
mosquitto_subна топикstm/test; - «прибор» — тот же
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 пакет быть не может.
⚠️ Брокер в опыте запущен без авторизации — только внутри закрытой сети контейнеров. Наружу брокер выставляют с паролями и шифрованием (раздел 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 Итоги
- Виртуальная машина — целый компьютер со своим ядром; контейнер — изолированный процесс на общем ядре: у Alpine и Ubuntu в проверке ядро одно.
- Образ — шаблон, контейнер — экземпляр, том — данные, которые переживают контейнер; внутри сети контейнеров соседи находятся по именам.
- IaaS, PaaS, SaaS отличаются тем, что остаётся на вас; облако снимает заботу о железе, но добавляет зависимость от сети и поставщика.
- Прибор знает имя сервера, а не его место: имя и порт меняются без перепрошивки.
- Стек для приборов — брокер, сборщик, база, панели — описывается одним
файлом Compose: версии закреплены, данные в томах, секреты в
.env. - Пакеты MQTT из переносимого кода на C приняты настоящим брокером: CONNECT в 22 байта, код 0, сообщение дошло до подписчика.
Дальше: раздел 88 «Распределённые системы и API».