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

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

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


Первое узкое место: устройство без хаба должно войти в экосистему само

Комиссионинг — это то, что происходит до того, как устройство вообще появляется в приложении. Получение параметров Wi‑Fi, авторизация, регистрация в облаке, переход в рабочий режим. У устройств с хабом всё это делает, собственно, хаб — он в сети, он доверенный, он знает облако. Без хаба этот путь устройство проходит само, с нуля, в чужой домашней сети. Для нас комиссионинг — не приятное дополнение к системе, а такое же узкое место, как транспорт или рантайм.

Мы сделали ставку на комиссионинг через BLE (Bluetooth Low Energy) — устройство настраивается прямо с телефона. После первого запуска оно входит в режим первичной настройки и получает по BLE предварительно зашифрованный набор данных:

  • параметры Wi‑Fi;

  • код авторизации;

  • данные клиента для обмена кода на токен;

  • служебные данные для регистрации в облаке.

В этом месте легко недооценить проблему. BLE удобен для первичной настройки, но как транспорт он очень тесный. MTU небольшой, поэтому данные приходится передавать чанками. В одной из реализаций под входной payload был фиксированный буфер на 1024 байта, а первый чанк содержал заголовок с длиной всего сообщения. Это позволило избежать бесконтрольных аллокаций и больших временных буферов.

После этого устройство проходит доверенную цепочку:

  • расшифровывает payload, переданный по BLE;

  • подключается к Wi‑Fi;

  • по HTTPS меняет код авторизации на access token;

  • регистрируется в облаке;

  • поднимает постоянное облачное соединение;

  • отправляет описание своих возможностей;

  • перезагружается в рабочий режим.

И всё это должно вести себя предсказуемо на плохих сетях и слабом железе. Поэтому у флоу появились жёстко заданные ретраи и тайм‑ауты: три попытки на подключение к Wi‑Fi, пять попыток на обмен токена, три попытки на регистрацию и общий таймаут настройки в 90 секунд.

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

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

Следующий шаг — перестать плодить соединения

Довольно быстро стало понятно, что главная проблема — в транспорте.

Идея дать каждой задаче свой сетевой канал кажется очень соблазнительной: отдельно телеметрия, отдельно команды, отдельно логи, отдельно OTA, отдельно регистрация описания устройства. На сервере это выглядит опрятно. А на устройстве быстро превращается в набор лишних TLS‑рукопожатий, сокетов, буферов и независимых наборов стейтов — и это ещё до того, как ты написал хоть строчку прикладной логики.

Поэтому почти всё облачное общение устройства, кроме начального OAuth‑обмена и первичной регистрации, мы завели в один мультиплексированный WebSocket/TLS‑канал.

Через него у нас пошли:

  • директивы (команды) от облака;

  • события и состояния устройства;

  • телеметрия;

  • логи;

  • публикация описания возможностей устройства (какие команды принимает, какие состояния отдаёт);

  • OTA.

Стоимость одного соединения на слабом клиенте не нулевая. По нашим оценкам, только одна TLS‑сессия на embedded‑стеке легко съедает около 30–40 КБ RAM. Дальше добавляются буферы, сокет, CPU на криптографию и ещё пара секунд на DNS, TCP и TLS‑рукопожатие в плохой сети. Если таких соединений несколько, мы масштабируем уже не полезную работу, а накладные расходы.

Один канал дал нам сразу несколько эффектов.

  • Во‑первых, сетевой профиль устройства стал почти постоянным. Неважно, сколько у конкретного устройства возможностей — три или тридцать, — базовая стоимость облачного транспорта оставалась примерно одной и той же.

  • Во‑вторых, у нас появилась одна reconnect‑стейт‑машина вместо нескольких. На массовом флоте это критично: reconnection storm всегда проблема и клиента, и сервера.

  • В‑третьих, стало проще планировать серверную часть. Для облачных устройств мы выделили отдельный контур сервисов вокруг WebSocket‑прокси и доставки сообщений, чтобы нагрузку можно было изолировать и проще считать.

Отдельно помогло то, как мы обращались с данными внутри канала. События не улетали по одному сразу после каждого изменения состояния — мы собирали их в короткое окно примерно на 100 мс. В результате стало меньше кадров, накладных расходов на TLS‑record, криптографической работы и шума на сервере. На одном из устройств под это был заранее выделен буфер батча примерно на 1,5 КБ и отдельный стековый буфер запроса около 2 КБ — опять старались избежать динамической аллокации.

А как же локальная работа?

Устройства без обязательного хаба по умолчанию завязаны на облако. Но если у пользователя хаб есть — мы хотели, чтобы устройства умели работать и локально, без интернета.

Поэтому рядом с облачным WebSocket/TLS‑каналом у нас появился отдельный локальный контур поверх Zenoh — протокола с минимальными накладными расходами, который не требует центрального брокера и в локальной сети умеет работать напрямую, в сценариях peer‑to‑peer и mesh. В одной модели у него живут и pub/sub, и query/reply поверх именованных ресурсов.

Локальное взаимодействие получалось компактным и по протоколу, и по архитектуре: хаб и устройство находили друг друга в LAN, обменивались данными локально и не зависели от внешнего интернета. Для локального контура мы использовали PSK‑based TLS, чтобы не тащить в домашнюю сеть полноценную сертификатную инфраструктуру.

Масштаб на слабом железе начинается не с транспорта, а с модели исполнения

Довольно быстро мы упёрлись ещё в одну вещь: даже хороший транспорт не спасает, если рантайм устройства сам по себе сложный и дорогой.

Многопоточность на сервере — естественный выбор. Однако на слабом железе она очень быстро становится роскошью. Нужно держать отдельные стеки, думать о гонках, мьютексах, порядке колбэков, ловить редкие зависания и разбираться с багами, которые проявляются раз в неделю на конкретной прошивке и конкретной Wi‑Fi‑сети.

Поэтому в нашем SDK мы сознательно выбрали однопоточную модель исполнения: почти вся логика устройства — обработка директив, отправка событий, OTA, комиссионинг, сетевые реакции — проходит через одну очередь задач на FreeRTOS.

Это решение выглядит менее эффектно, чем набор независимых потоков, но для embedded‑платформы оказалось гораздо честнее.

Что оно нам дало:

  • общий state почти перестал требовать мьютексов;

  • поведение стало детерминированнее;

  • ушла большая часть проблем с гонками между сетью, BLE и application‑логикой;

  • стоимость по памяти стала заранее ограниченной.

В одной из конфигураций этот рантайм укладывался в один поток со стеком 10 КБ и отдельный watchdog‑поток на 512 байт. У очереди были фиксированные пулы для немедленных и отложенных задач в конфигурации 32 × 32, а вся инфраструктура диспетчеризации занимала меньше 14 КБ на ARM‑платформе.

Тот же принцип мы протащили через весь SDK: фиксированные ETL‑контейнеры вместо структур, явные лимиты на строки, буферы и контексты задач. Только в этом случае система начинает вести себя предсказуемо на большом флоте слабых устройств.

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

Плохая сеть — это базовый режим работы

IoT‑систему очень удобно проектировать так, будто всегда доступна нормальная сеть. А потом появляются реальные квартиры, дачи, роутеры, провайдеры, captive portal, странный DNS, скачки сигнала, и выясняется, что «нормальная» сеть была слишком смелым допущением с самого начала. Так что мы отдельно занялись вопросом DNS и политикой переподключений.

Как правило, устройство при каждом переподключении проходит полный путь: сначала запрашивает DNS, получает один адрес, а если не может подключиться, всё начинает заново. А теперь представьте то же самое, но на плохой сети: оно будет дольше висеть в офлайне, сжигать CPU и создавать всплески повторных попыток.

Мы сделали в SDK собственный DNS‑кеш с fallback IP для критичных хостов: есть DNS — используем его, нет или барахлит — берём резервные адреса. Сам DNS lookup вынесли в отдельную короткоживущую задачу со стеком 3 КБ и тайм‑аутом в 1 секунду, чтобы случайный вызов системного резолвера не блокировал основную очередь исполнения.

Но одного DNS‑кеша мало, важна и дисциплина переподключений. Для облачного канала мы использовали экспоненциальный backoff с джиттером: базовая задержка — 5 секунд, потолок — 90 секунд, а само ожидание рандомизируется, чтобы после общей аварии множество устройств не начинало ломиться обратно одновременно.

А ещё мы сбрасываем backoff не сразу после первого удачного подключения — только после определённого периода стабильной работы. Иначе система может слишком рано «забыть», что сеть всё ещё может быть нестабильной и в любой момент отвалиться снова.

В масштабе сотен тысяч устройств это критично: один плохой DNS‑сервер или короткая деградация сети не должны разворачиваться в лавину переподключений.

Безопасность без права на тяжёлую криптографию

В IoT есть распространённая ловушка: объявить безопасность абсолютным приоритетом, а потом спроектировать систему, которая на реальном железе просто не помещается. Мы старались выбирать механизмы, которые устройство может себе позволить, и при этом не жертвовать защитой. Отсюда вырос наш подход ECC‑first.

На ESP32-C3 генерация ECC‑ключа P-256 занимает примерно 50–150 мс. Для RSA-2048 это уже примерно 800 мс — 3 секунды. На ещё более слабых ARM‑платформах разница становится совсем неприятной: RSA‑ключи могут генерироваться секунды и даже десятки секунд. Для заводского контура это означает очень простую вещь: линия начинает простаивать.

Дальше разница становится ещё заметней:

  • приватный ECC‑ключ — 32 байта против ~1232 байт;

  • подпись — 64 байта против 256 байт;

  • base64-представление подписи в HTTP‑заголовке — 88 символов против 344 символов.

Для встроенного устройства это не абстрактная математика, а конкретные байты во Flash, RAM, BLE‑пакетах и HTTP‑заголовках, которых либо хватает, либо нет.

Поэтому доверенную цепочку мы строили так:

  • при комиссионинге данные по BLE шифруются гибридной схемой на ECC и AES-128;

  • устройство получает OAuth‑токен по HTTPS;

  • регистрацию в облаке подписывает собственной ECDSA‑подписью (алгоритм на основе ECC);

  • приватный ключ генерируется на устройстве и не покидает его.

Безопасность транспорта мы тоже ужимали до разумного embedded‑формата. PEM‑наборы — это текст, а значит, лишние байты, поэтому мы перешли к более лёгким бинарным DER‑сертификатам. Основной bundle — 21 корневой сертификат, суммарно это около 20 КБ. Размер TLS‑буферов подбирали вручную: 5 КБ на вход, 4 КБ на выход. Ну и дополнительно уменьшили размер фрагментов TLS — так мы перестали переплачивать памятью на каждом соединении.

Мы пошли дальше и начали сокращать сами наборы корневых сертификатов под реальные хосты. В наших экспериментах вместо системного bundle примерно на 223 КБ получался минимальный набор из 13 сертификатов на 18 КБ, а в упакованном DER‑архиве — около 10 КБ.

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

И ещё кое‑что

Есть ещё одна вещь, которую легко недооценить в начале проекта, — производство.

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

  • генерацию ключей на устройстве во время производства;

  • выгрузку наружу только публичной части;

  • проверку связки между устройством, его ключом и облачной регистрацией;

  • сбор метаданных о произведённых устройствах;

  • self‑тестирование и валидированный выход из factory mode только устройств, прошедших все тесты.

Принцип здесь тот же, что и в рантайме: не плодить ручную неоднозначность. ECC‑ключ генерируется прямо на устройстве, публичный уходит наружу, приватный — нет. Устройство выходит из factory mode только после обязательных проверок, а отладочные возможности в production‑контуре мы жёстко режем.

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

Что в итоге у нас получилось

Наше новое облако уже применяется в ИК‑пульте и эмби‑лампе Яндекса, а также в системе для защиты от протечек NEPTUN.

Мы не пошли по пути одного большого бэкенда, который держит всё на себе. Мы убирали лишнюю сложность из каждого слоя, и теперь у нас:

  • в подключении — BLE‑комиссионинг с контролируемой доверенной цепочкой;

  • в транспорте — один мультиплексированный WebSocket/TLS‑канал вместо зоопарка соединений;

  • в рантайме — однопоточная модель с фиксированной стоимостью по памяти;

  • в сетевой устойчивости — DNS‑кеш, fallback IP и аккуратный backoff;

  • в безопасности — подход ECC‑first и уменьшенный сертификатный след;

  • в производстве — стандартизация и автоматизация заводского контура.

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

В этом и состоит суть настоящей масштабируемости для IoT: не держать миллион соединений любой ценой, а сделать так, чтобы цена каждого соединения, каждого байта и каждого нового устройства оставалась контролируемой.

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


  1. fedotovartuom76
    28.07.2026 08:08

    Один вопрос. Как решить проблему с автономностью без хаба? Мы все знаем, что Wi-Fi устройства живут на батарейке существенно меньше, нежели ZigBee, которому нужен хаб.


  1. nbkgroup
    28.07.2026 08:08

    На ESP32-C3 генерация ECC‑ключа P-256 занимает примерно 50–150 мс. Для RSA-2048 это уже примерно 800 мс — 3 секунды. На ещё более слабых ARM‑платформах разница становится совсем неприятной: RSA‑ключи могут генерироваться секунды и даже десятки секунд. Для заводского контура это означает очень простую вещь: линия начинает простаивать.

    https://esp32.com/viewtopic.php?t=23830#p85332

    Right now I have variable base implementations (i.e. shared secret calculations) for Curve25519 and P-256 written that runs in 7 ms and 8 ms respectively on the ESP32-S2 and fixed base implementations (i.e. keygen) that are ~60% faster. I am now about to write implementations for the original ESP32 as well, which I expect to be ~50% faster after making some timing experiments.