Устройство программной платформы
Почему быстрый сервер — это прежде всего ограниченные очереди?
Качество ядра определяется тем, можно ли объяснить каждую команду и ограничить последствия каждого входа.
В асинхронной системе легко создать впечатление бесконечного параллелизма. На самом деле процессор, память и накопитель конечны. Если телеметрия, голос и команды занимают одну неограниченную очередь, задержка появится именно в момент, когда система больше всего нужна.
Разберём, какие данные хранятся в памяти, кто ими владеет, где нужна последовательность и какие вычисления выносятся в отдельный процесс. Быстрый путь команды будет коротким, но не будет обходить проверку прав и состояния.
ABI — двоичный договор между скомпилированными компонентами · IPC — обмен между процессами
В центре Koobique находится один процесс с несколькими явными подсистемами: реестр, наблюдения, оценки состояния, политики, исполнитель команд, адаптеры и хранилище. Внешний API и собственный протокол не должны обходить общий путь проверки прав и ограничений. Голос, приложение и локальная автоматизация создают разные источники намерений, но не три независимых механизма включения реле.
Предлагаемые crates#
koobique-domain содержит идентификаторы, величины, состояния команд и проверки инвариантов. koobique-wire — кодирование, лимиты и тестовые векторы протокола. Оба по возможности не зависят от Linux и могут иметь no_std-профиль для устройств.
koobique-core реализует реестр, очереди и исполнитель. koobique-storage — транзакции и запросы. koobique-api — мобильный и диагностический интерфейс. koobique-adapter- * — конкретные способы связи. koobique-sim — виртуальные датчики и устройства. Отдельные executables отвечают за privileged-helper, speech-worker и CLI.
Структура не должна порождать десятки пустых библиотек: модуль выделяется, когда у него есть собственный контракт или зависимость. Главное — направление зависимостей. Физическая модель не должна знать о конкретной SQL-таблице или библиотеке BLE.
Владение состоянием#
Изменяемое состояние исполнительного канала имеет одного логического владельца. Команды этому каналу упорядочиваются. Параллельные датчики могут поступать независимо, но изменение конфигурации имеет номер версии. Операция с устаревшим ожидаемым номером либо отклоняется, либо проходит явно описанную процедуру согласования.
Глобальный Mutex<HashMap<...>>, удерживаемый при записи в БД или сетевом ожидании, создаёт скрытую сериализацию всей системы. Tokio отдельно описывает риски удержания блокировок и блокирующих операций в асинхронном исполнении. Это основание для коротких критических секций и явных очередей, а не для запрета всех mutex. [45][82]
Ограниченные очереди#
Каждая очередь имеет ёмкость, класс важности и реакцию на переполнение. Для телеметрии допустимо объединить несколько ещё не обработанных значений одного канала в последнее, если история явно помечается как прореженная. Для команды с обещанным надёжным принятием молчаливая потеря недопустима.
Предлагаемые классы: управление и safety-события; актуальное состояние; обычная история; диагностика и большие передачи. Нельзя позволить экспорту графиков занять все буферы и задержать выключение. Однако чрезмерный приоритет одной очереди может голодать остальные, поэтому нужен ограниченный квант обработки и метрика возраста события.
Детерминированная логика#
Чистая функция решения получает snapshot состояния, версию правил и время из внедрённого источника, затем возвращает намерения и объяснение. Она не читает часы и сеть из произвольных мест. Это позволяет воспроизводить ситуацию по журналу и сравнивать новую версию алгоритма без отправки реальных команд.
Нельзя обещать полный детерминизм всех физических данных: сеть и измерения приходят с неопределённостью. Но можно сделать детерминированным решение на зафиксированном наборе входов. При воспроизведении история не должна запускать реальные устройства, если режим явно не разрешён.
Наблюдаемость и диагностика#
У каждой команды есть trace ID и источник: приложение, правило, кнопка, голос. Журнал связывает её с использованными наблюдениями, версией политики и ответом устройства. Метрики включают возраст данных, глубину очереди, время ожидания записи, число переподключений и неизвестные результаты.
Логи не являются единственным источником истины. Состояние команды хранится структурированно; текстовый лог помогает человеку. Не следует реконструировать текущую конфигурацию из случайных строк info! после перезапуска.
Где Rust не обязан быть единственным языком#
SQLite, драйверы Linux и зрелые inference-библиотеки могут быть реализованы на других языках. Это не нарушает цель собственного Rust-ядра. Через FFI нужно ограничить время жизни буферов, потоки, отмену и обработку ошибок. Более рискованный ML-runtime лучше изолировать процессом, чем пытаться доказать его безопасность одной обёрткой unsafe.
Rustls предоставляет готовую TLS-реализацию и выбор криптографических провайдеров. Axum удобен для HTTP-API и диагностического интерфейса; использование их не превращает Koobique в готовую чужую платформу. [46][47]
Первый benchmark ядра#
Симулятор подаёт события с управляемой частотой, задержками и сбоями. Одновременно выполняются команды и экспорт истории. Измеряются p50/p95/p99, максимальный возраст события и память. Сначала исправляются неограниченные очереди и блокировки, а уже потом обсуждаются lock-free и низкоуровневые оптимизации.
Управление должно оставаться предсказуемым, когда модель загружает GPU, запись истории тормозит, а устройство присылает слишком много данных.
Что находится в одном ядре#
Предлагаем модульный Rust-процесс с явными границами: реестр объектов и возможностей; приём и проверка наблюдений; актуальное состояние; оценщики; планировщик процессов; арбитр полномочий; исполнитель; журнал и API. Это логические модули, не обязательные сетевые микросервисы.
Общий код контрактов отделён от Linux, Tokio и базы. В нём находятся типы, версии, единицы, пределы сообщений и преобразования. На устройстве используются только подходящие по памяти части. Семантически значимые поля не передаются как неограниченные строки без схемы. Внутри Rust можно использовать размерностно-типизированные величины, например подход uom. [107] Это снижает риск смешения единиц в коде, но не исправляет неверный внешний дескриптор: данные проверяются на границе.
Очереди и давление нагрузки#
Для каждого источника ограничиваем размер сообщения, частоту и память очереди. Команды и подтверждения не стоят за многомегабайтным аудиофрагментом. Потеря телеметрии фиксируется; потеря критического управляющего события не маскируется таким же механизмом сброса.
Полезно хранить актуальное значение как компактный снимок, а историю — отдельным потоком. Подписчик медленного графика может получить последнее состояние и информацию о пропуске. Исполнитель не обязан ждать, пока мобильный клиент просмотрит все промежуточные точки.
Одно устройство имеет последовательный поток исполнения команд. Разные независимые ресурсы могут обрабатываться параллельно. Не нужен глобальный mutex вокруг всего «мира», но и повсеместный lock-free не является автоматическим выигрышем. Длительное удержание блокировки и блокирующий код внутри асинхронной задачи способны задерживать другие задачи; это прямо разбирается в документации Tokio. [82]
Два разных срока#
Deadline вычисления определяет, когда результат модели ещё полезен. Срок команды — до какого момента разрешено физическое действие. Завершившийся позже deadline прогноз отправляется в диагностику, не в исполнение. Актуальность проверяется по монотонному времени станции и текущей эпохе управления.
Отмена Rust future не гарантирует остановку уже запущенного синхронного native-инференса. Поэтому тяжёлые модели выполняются в отдельном worker-процессе с ограниченным IPC. При зависании процесс можно завершить; состояние ядра и локальное управление остаются целыми.
Отмена ожидания не останавливает физический мир#
У тайм-аута есть точный предел действия. Tokio может прекратить ожидание future, но длительное вычисление, которое не возвращает управление планировщику, способно превысить заданное время. Уже начавшую работу функцию в spawn_blocking нельзя остановить вызовом abort. Поэтому этот механизм не является аварийным выключателем инференса. [142][143]
В нашей архитектуре запрос к вычислительному помощнику получает ID, снимок состояния и дедлайн. Поздний ответ помечается как непригодный к исполнению. При зависании отдельный процесс можно перезапустить; само право на действие при этом не передаётся ни этому процессу, ни его ответу. Устройство отдельно ограничивает свой выход.
Пример: модель начала распознавать команду, человек нажал OFF, а распознавание закончилось позже. Отмена запроса полезна для ресурсов, но правильность обеспечивает другое: старая эпоха и версия состояния не проходят проверку исполнителя. Даже если поздний ответ дошёл, он не отменяет кнопку.
Выбор ML-runtime#
| Кандидат | Полезная роль | Что проверить |
|---|---|---|
| Candle | Rust-инференс и собственные модели | Операторы, checkpoint, backend, память [73] |
| Burn | Исследование и обучение на Rust | Зрелость конкретного backend и перенос артефакта [134] |
| ort + ONNX Runtime | Использование экспортируемых моделей | Native-зависимость, execution provider, версии операторов [135][137] |
| tract | Компактный CPU-инференс на Rust | Поддерживаемое подмножество модели и скорость [136] |
| Отдельный исследовательский worker | Быстрая проверка чужой модели без переноса | Не превращать временный Python-стенд в скрытую обязательную зависимость |
Наш собственный производственный код остаётся на Rust. Это не означает, что Linux, драйвер GPU или все математические ядра также написаны на Rust. Безопасность FFI, совместимость ABI и происхождение native-библиотек проверяются отдельно.
Изоляция ресурсов#
Linux cgroups v2 позволяет ограничивать и учитывать ресурсы процессов, включая CPU, память и I/O. [132] Это полезный механизм отделения worker от ядра, но не доказательство жёсткого реального времени и не универсальный контроль каждой GPU-операции.
При нехватке памяти первой ограничивается история/инференс по заданной политике, а не исчезает способность обработать кнопку. Для критических физических реакций сохраняется локальный путь устройства. Резерв вычислителя проверяем совместной нагрузкой, а не суммой красивых паспортных цифр.
Начальная конструкция: один Rust-core, отдельный AI-worker, отдельный узкий сервис привилегированных операций и существующие системные службы Linux. Дробление на большее число процессов оправдывается измерением или анализом отказов.
Границы процессов и устройств#
| Процесс | Права и IPC | Сбой |
|---|---|---|
| koobique-core | Непривилегированный; журнал, KQIP/API | Перезапуск; pending-намерения не оживают |
| koobique-device-agent | Только нужные BLE/USB; Unix socket с Core | Переподключение адаптера без выдачи root Core |
| koobique-admin-helper | Узкие сетевые/установочные операции; проверяемый IPC | Запрос отклоняется; нет shell из пользовательского текста |
| koobique-speech-worker | Аудио/модель по профилю; без GPIO и секретов | Тайм-аут не останавливает кнопку и Core |
| koobique-model-worker | Лимиты CPU/RAM; снимок → предложение | Кандидат отбрасывается, работает базовый автомат |
Внутри Core остаются протокольные адаптеры без привилегированного доступа. Работа с USB/BT вынесена в device-agent; так устраняется двойное описание одной границы. Если конкретный E0 не использует BLE на Hub, этот процесс просто не запускается.
Критические задачи Rust регистрируются у супервизора: результат JoinHandle нельзя игнорировать. Для E0 предлагается fail-stop Core при потере владельца команд и перезапуск systemd; выбор panic=abort либо контролируемого unwind фиксируется сборкой и тестом. Синхронная SQLite работает на выделенном потоке; длительное вычисление не попадает в сетевой executor. [162]