TL;DR
Раньше я уже рассказывал на Хабре об Inquir Compute, который тогда был платформой для запуска serverless-функций. С тех пор проект вырос в PaaS с поддержкой контейнерных приложений, баз данных, фоновых задач и собственной системой управления вычислительными ресурсами.
Вместо Kubernetes или Nomad я решил развивать собственный оркестратор с allocation plane, планировщиком и вычислительными раннерами на containerd. Главная причина не в желании переписать существующие инструменты, а в возможности объединить долгоживущие приложения и serverless-функции в одну ресурсную модель, напрямую связанную с жизненным циклом сервисов и тарификацией платформы.
Расскажу, как развивалась архитектура Inquir Compute, почему я считаю собственный оркестратор правильным выбором для этого проекта, какие задачи удалось решить и над чем ещё предстоит работать.
Как Inquir Compute вырос из FaaS
Некоторое время назад я уже писал на Хабре об Inquir Compute. Тогда проект представлял собой FaaS (Function as a Service) платформу, которая позволяла запускать serverless-функции в изолированных контейнерах, вызывать их через HTTP и выполнять фоновые задачи.
Изначально идея была довольно простой: разработчик загружает код, платформа подготавливает окружение, запускает функцию и возвращает результат. При этом не нужно самостоятельно арендовать сервер, настраивать Docker или постоянно поддерживать работающий процесс.
Для обработки вебхуков, небольших API и периодических задач этого было вполне достаточно.
Но по мере развития проекта стало понятно, что одними функциями ограничиваться не хочется.
Представим обычный небольшой сервис:
-
Frontend на React.
-
API на Node.js.
-
PostgreSQL.
-
Redis.
-
Telegram-бот или фоновый обработчик.
На одном VPS такой стек можно развернуть через Docker Compose. Понадобится настроить reverse proxy, сертификаты, переменные окружения, тома и резервное копирование.
Если проект один, особых сложностей нет.
Но затем появляется второе приложение, ещё несколько сервисов, экспериментальный проект, автоматизация или инструмент, написанный за выходные.
И возникает необходимость управлять уже не отдельными контейнерами, а всей инфраструктурой.
Как разделять ресурсы между приложениями? Как обновлять их без простоя? Как быстро откатывать неудачные релизы? Как размещать приложения на нескольких серверах? Что делать, если один контейнер начинает потреблять всю память?
Конечно, для всего этого существуют готовые инструменты. Можно собрать инфраструктуру из Docker, reverse proxy, систем мониторинга, очередей и средств автоматизации развёртывания.
Или воспользоваться готовыми PaaS, например Railway или Render.
Но мне хотелось развить Inquir Compute в платформу, где разработчик мог бы запускать и обычные приложения, и функции, и фоновые процессы, используя общий пул вычислительных ресурсов.
При этом управление должно оставаться максимально простым: создать приложение, указать ресурсы, развернуть код и получить работающий сервис.
Так Inquir Compute начал превращаться из FaaS в PaaS.
А вместе с этим пришлось существенно пересмотреть архитектуру.
Почему я решил писать собственный оркестратор
Когда проекту потребовалось управлять большим количеством контейнеров на нескольких вычислительных узлах, первым очевидным вариантом был Kubernetes.
У него уже есть планировщик, контроллеры, декларативная модель состояния, Service Discovery, управление ресурсами и механизмы восстановления после сбоев.
Есть и более компактные решения, например HashiCorp Nomad.
Nomad тоже умеет распределять нагрузки между узлами, управлять жизненным циклом заданий, выполнять canary-развёртывания, проверки здоровья и переключение версий.
Поэтому вопрос вполне закономерный: зачем писать собственный оркестратор, когда уже существуют зрелые системы?
Сначала я рассматривал оркестрацию просто как инфраструктурную задачу. Но постепенно стало понятно, что для Inquir Compute это одна из основных технологий платформы.
И здесь есть несколько причин.
Одна ресурсная модель для разных типов вычислений
В обычном контейнерном хостинге относительно простая модель: есть приложение, которому нужно выделить ресурсы, запустить контейнер и поддерживать заданное количество реплик.
Но Inquir Compute изначально разрабатывался как FaaS.
Функция может запускаться на несколько миллисекунд, затем простаивать, масштабироваться до нуля и снова активироваться при следующем запросе.
При этом для уменьшения задержки желательно сохранять готовые экземпляры, пока это оправдано.
Долгоживущее приложение работает иначе.
Ему могут требоваться постоянные соединения, подключённые тома, несколько реплик и определённая стратегия обновления.
Но оба типа нагрузок используют одни и те же CPU, RAM и вычислительные узлы.
Представим сервер, на котором работают несколько API. Большую часть времени они не используют весь доступный процессорный ресурс.
Часть свободной вычислительной ёмкости можно использовать для выполнения функций.
Если функция активно вызывается, платформа может увеличить количество реплик в пределах установленных лимитов. Если вызовы прекратились, реплики можно постепенно освободить.
Но при этом нельзя допустить, чтобы активированные функции вытеснили постоянно работающие приложения или исчерпали память сервера.
То есть мне нужен был не просто инструмент запуска контейнеров, а единый механизм распределения ресурсов между нагрузками с разными жизненными циклами.
Именно вокруг этой задачи начал формироваться собственный allocation plane.
Я не хотел создавать два независимых уровня оркестрации
Kubernetes или Nomad вполне могли бы стать нижним уровнем такой системы.
Но поверх них всё равно потребовалось бы реализовать продуктовую модель Inquir Compute.
В ней есть приложения, релизы, preview-окружения, функции, задания, рабочие пространства, тарифы и ограничения пользователей.
Например, при вызове serverless-функции платформа должна определить, есть ли готовая реплика, можно ли создать новую, какие ресурсы доступны и сколько времени допустимо ждать её активации.
После этого решение пришлось бы преобразовывать в операции другого оркестратора.
А затем согласовывать его фактическое состояние с состоянием Inquir Compute.
Аналогичная ситуация с развёртываниями. Платформа управляет production-релизом, preview-версиями, проверками готовности и переключением маршрутов.
В результате собственная продуктовая логика управляла бы другим полноценным оркестратором.
В таком подходе нет ничего неправильного. Многие облачные платформы именно так и устроены.
Но я решил сделать приложения, функции и их реплики непосредственными объектами собственного слоя управления.
Так появились allocations, планировщик и вычислительные раннеры.
И это решение в дальнейшем оказалось полезным при объединении FaaS и PaaS.
Оркестрация напрямую связана с экономикой платформы
Есть ещё одна причина, по которой я решил развивать собственный планировщик.
Для PaaS важно не просто запускать приложения, а эффективно использовать серверные мощности.
Допустим, у нас есть сервер с 16 ГБ RAM.
На нём можно разместить несколько крупных сервисов с относительно постоянной нагрузкой. А можно множество небольших приложений, которые большую часть времени ожидают запросы и почти не используют CPU.
Во втором случае потенциально можно получить значительно более высокую плотность размещения.
Но просто ориентироваться на текущее потребление ресурсов нельзя.
Если несколько приложений одновременно увеличат нагрузку, сервер может оказаться перегружен.
Поэтому планировщику необходимо учитывать лимиты, резервирование, фактическое потребление и доступную ёмкость.
В Inquir Compute уже есть binpack-размещение и резервирование ресурсов. Память allocation резервируется полностью, а CPU может резервироваться в объёме ниже установленного лимита.
Эта модель связана и с тарификацией.
Пользователь получает определённый объём вычислительных ресурсов, а платформа должна учитывать его при размещении приложений, создании реплик и выполнении функций.
Мне важно, чтобы ресурсная модель и модель тарификации развивались как части одной системы.
Это позволяет в дальнейшем реализовывать более гибкие политики размещения и распределять инфраструктурные затраты между разными типами нагрузок.
Конечно, Kubernetes и Nomad тоже умеют работать с квотами, резервированием и планированием ресурсов.
Преимущество собственного решения здесь не в том, что эти механизмы пришлось изобрести заново, а в том, что их можно непосредственно связывать с продуктовой и экономической моделью Inquir Compute.
Мне не нужен универсальный оркестратор
Kubernetes предоставляет огромный набор возможностей: произвольные ресурсы, контроллеры, операторы, разные типы storage и сетевых интеграций.
Но Inquir Compute не предоставляет пользователям Kubernetes API.
Я заранее определяю поддерживаемые типы нагрузок, их жизненный цикл, ограничения и доступные операции.
Поэтому нет необходимости реализовывать всю функциональность универсального оркестратора.
Например, при изменении механизма масштабирования функций я могу менять политику активации реплик, сроки простоя и правила выделения ресурсов непосредственно в собственном управляющем слое.
В этом смысле собственный оркестратор позволяет сосредоточиться на ограниченном наборе задач, которые действительно нужны платформе.
При этом я не считаю, что его разработка автоматически обходится дешевле Kubernetes.
Планировщик, контроллеры, раннеры и сетевую инфраструктуру тоже нужно писать, тестировать и обслуживать.
Но в моём случае эта работа одновременно развивает основные возможности продукта.
Именно поэтому собственный оркестратор для Inquir Compute не стал ещё одним инфраструктурным велосипедом. Он превратился в основу вычислительной модели платформы.
Как теперь устроен Inquir Compute
Первые версии Compute использовали Docker и относительно простой механизм управления контейнерами.
Для FaaS этого было достаточно: создать контейнер, запустить runtime, выполнить обработчик и при необходимости оставить контейнер для повторного использования.
Но с появлением контейнерных приложений, реплик и нескольких вычислительных узлов архитектуру пришлось менять.
Так появился allocation plane, собственный слой размещения вычислительных нагрузок, с отдельными раннерами и поддержкой containerd.
При этом Dockerfile и OCI-образы остались частью пользовательского процесса развёртывания.
Архитектурно систему можно разделить на несколько компонентов.
Control Plane отвечает за управление приложениями, конфигурациями, релизами, рабочими пространствами и заданиями. Серверная часть написана на Node.js и Fastify, метаданные хранятся в PostgreSQL. Авторизация и подписки интегрированы с отдельным сервисом Inquir ID.
App / Function Controllers управляют жизненным циклом приложений и функций, формируя желаемое состояние вычислительных нагрузок.
Allocation Plane отвечает за размещение нагрузок на доступных узлах с учётом ресурсных ограничений.
Compute Runners запускают контейнеры, управляют их выполнением и сообщают управляющему слою о фактическом состоянии.
Gateway обрабатывает пользовательские запросы и направляет их к соответствующим приложениям и функциям.
Упрощённо путь размещения выглядит так:
Web UI / CLI / API | v Control Plane | v App / Function Controllers | v Allocations | v Compute Scheduler | v Runners on Compute Nodes | v Containerd / OCI Runtime | +-----+-----+ | | v v Container Function Apps Replicas
Это логическая схема управления вычислениями. Пользовательский HTTP-трафик проходит через Gateway и не изображён на этой схеме.
Важный принцип заключается в том, что пользователь работает с приложением как с логической сущностью, а не с конкретным контейнером на определённом сервере.
Контейнер может быть пересоздан, версия обновлена, количество реплик изменено.
При сохранении настроенного домена пользователь продолжает обращаться к тому же адресу приложения.
Именно это разделение позволяет развивать механизмы размещения независимо от пользовательской модели сервисов.
Самый показательный результат: функции стали обычными allocations
Одним из результатов новой архитектуры стал переход функций на общий вычислительный слой.
Раньше serverless и долгоживущие приложения использовали разные механизмы управления контейнерами.
Это означало, что для функций приходилось отдельно решать задачи размещения, запуска, проверки готовности и восстановления.
После появления allocation plane я смог представить функцию как специальное внутреннее приложение, или shadow application.
По сути, функция получила возможность использовать ту же инфраструктуру размещения и управления релизами, что и обычное контейнерное приложение.
При этом у неё сохранилась собственная модель выполнения.
Обычное приложение может работать непрерывно, принимать HTTP-запросы и поддерживать долгоживущие соединения.
Функция активируется по запросу, выполняет обработчик и может масштабироваться до нуля после простоя.
Но на уровне оркестратора оба типа нагрузки управляются через общий механизм allocations.
Это даёт несколько преимуществ.
Во-первых, не нужно поддерживать два независимых механизма распределённого запуска контейнеров.
Во-вторых, можно использовать общую модель проверки готовности, размещения и восстановления после сбоев.
В-третьих, функции и приложения учитываются в одной ресурсной модели.
Например, при размещении новой функции планировщик видит не только другие функции, но и уже работающие контейнерные приложения.
Для меня это стало одним из главных подтверждений правильности выбранной архитектуры.
Вместо добавления ещё одной независимой подсистемы получилось переиспользовать уже существующие механизмы оркестрации.
Задача №1. Обновлять приложения без остановки работающей версии
Одна из первых возможностей, которую я хотел получить для контейнерных приложений, это управление релизами.
Допустим, у нас работает API версии v1, а разработчик отправляет новую версию v2.
Самый простой алгоритм выглядел бы так:
-
Остановить предыдущий контейнер.
-
Собрать или скачать новый образ.
-
Запустить новую версию.
-
Направить на неё запросы.
Но если новая версия не запустилась, приложение станет недоступным.
Поэтому в Inquir Compute приложение и релиз существуют отдельно.
Приложение хранит общую конфигурацию сервиса, а релиз содержит ссылку на конкретный образ, параметры запуска и снимок переменных окружения.
Для публичного HTTP-приложения с подходящей стратегией rolling новая версия может запускаться параллельно с предыдущей.
Сначала платформа собирает или получает Docker-образ и запускает новый экземпляр.
Затем выполняется health check.
Если приложение не готово, новый релиз не становится production-версией.
Если запуск успешен, при настроенном preview-домене релиз получает отдельный URL, по которому его можно проверить перед публикацией.
После этого пользователь может перевести новую версию в production.
Deploy v2 | v Build / Pull | v Start Container | v Health Check / \ Failed Healthy | | v v Keep v1 Preview v2 | v Promote | v Production: v2 | Optional rollback | v Production: v1
При этом сам факт выдачи preview-адреса не означает, что приложение уже готово. Необходимо дождаться успешного запуска и проверки работоспособности.
Есть и ограничения.
Для приложений с постоянными томами или закреплённым TCP-портом стратегия по умолчанию предполагает recreate: предыдущий экземпляр останавливается перед запуском нового.
Это может приводить к перерыву в работе.
Откат также зависит от того, сохранился ли предыдущий production-релиз.
Если его контейнеры уже остановлены, возврат потребует повторного запуска.
И, конечно, откат приложения не означает откат данных.
Если новая версия выполнила несовместимую миграцию PostgreSQL, возвращение предыдущего контейнера не восстановит старую схему базы данных.
Поэтому обратная совместимость миграций остаётся задачей разработчика.
В результате обычное обновление приложения превращается в отдельную систему управления версиями, состояниями и маршрутизацией.
Задача №2. Serverless и холодные старты
В FaaS одна из основных проблем связана с холодным стартом.
Если для каждого вызова создавать контейнер заново, выполнение включает не только работу пользовательского кода, но и подготовку окружения.
Например, для Node.js-функции процесс может выглядеть так:
HTTP Request | vAllocate Resources | vCreate Container | vStart Runtime | vLoad Handler / Dependencies | vExecute Handler | vReturn Response
При этом сама функция может выполняться всего несколько миллисекунд.
Но подготовка контейнера, запуск runtime и загрузка модулей способны занимать значительно больше времени.
Чтобы снизить эту задержку, ещё в ранних версиях Compute я реализовал механизм тёплых контейнеров.
В текущей архитектуре основного пути выполнения он работает через тёплые реплики.
Как работают тёплые реплики
После завершения вызова реплика функции может оставаться запущенной.
Если приходит новый запрос и есть готовая реплика со свободной ёмкостью, платформа использует её повторно.
Если готовых экземпляров нет, система пытается активировать необходимую нагрузку.
Request | v Ready replica available? / \ Yes No | | v v Reuse it Activate / Wait | | +------+-------+ | v Run Handler
В основном пути выполнения функции используются следующие значения по умолчанию:
-
15 минут простоя до масштабирования к нулю.
-
Один одновременный вызов на реплику.
-
Максимум четыре реплики.
-
До 60 секунд ожидания активации.
Эти параметры могут изменяться конфигурацией платформы.
Масштабирование происходит асинхронно, поэтому 15 минут не означают гарантированную остановку контейнера ровно в этот момент.
Также предусмотрена отдельная политика keep-warm, позволяющая дольше сохранять определённые функции запущенными.
Почему нельзя держать все функции прогретыми
Представим 1000 функций, каждая из которых использует по 128 МиБ памяти.
Если постоянно держать по одному готовому экземпляру каждой функции, потребуется около 125 ГиБ RAM.
При этом значительная часть функций может вызываться всего несколько раз в день.
С одной стороны, чем больше готовых реплик, тем меньше задержка при следующем вызове.
С другой, каждая такая реплика занимает ресурсы, которые могли бы использовать обычные приложения или другие функции.
Поэтому управление тёплыми репликами становится задачей распределения вычислительной ёмкости.
И здесь снова оказывается полезен общий allocation plane.
Он позволяет учитывать serverless-нагрузку в той же модели размещения, что и постоянно работающие приложения.
При этом дополнительные реплики не решают проблемы внешних зависимостей. Если функция упирается в перегруженную базу данных или сторонний API, увеличение числа контейнеров может только усилить конкуренцию за этот ресурс.
Поэтому эффективный serverless требует не только быстрого запуска контейнеров, но и правильного управления их количеством.
Задача №3. Как размещать больше приложений на одних серверах
Для небольших приложений выделение отдельной виртуальной машины часто бывает избыточным.
Например, десять небольших API могут большую часть времени почти не нагружать процессор.
Их можно размещать на общих вычислительных узлах, ограничивая доступные каждому ресурсы.
Но совместное размещение создаёт проблему конкуренции за CPU и память.
Если один пользователь запустит процесс, который потребляет все доступные ресурсы, это может повлиять на соседние сервисы.
Поэтому планировщик должен учитывать несколько характеристик:
-
Лимит: верхняя граница потребления ресурса.
-
Резервирование: объём ресурсов, который учитывается при размещении.
-
Фактическое потребление: измеренная нагрузка.
-
Доступная ёмкость: ресурсы, которые можно выделить новым нагрузкам.
Например, есть сервер с 8 vCPU и 16 ГБ RAM.
На нём размещены три приложения:
Compute Node: 8 vCPU / 16 GB RAMApplication A CPU limit: 2 vCPU RAM limit: 2 GBApplication B CPU limit: 1 vCPU RAM limit: 1 GBApplication C CPU limit: 2 vCPU RAM limit: 4 GB
Если ориентироваться только на текущее потребление CPU, можно разместить слишком много приложений.
Пока нагрузка небольшая, всё будет работать нормально.
Но когда несколько сервисов одновременно начнут активно использовать процессор, они станут конкурировать друг с другом.
С памятью ситуация ещё сложнее. Её нехватка может приводить к завершению процессов.
В Inquir Compute память allocation резервируется полностью, а для CPU предусмотрена модель, в которой резерв может быть ниже лимита.
При этом резервирование CPU само по себе не гарантирует соответствующую долю процессорного времени при конкуренции на хосте. Для этого необходима отдельная политика распределения CPU.
Binpack-размещение
В Compute уже реализован механизм binpack.
Его задача заключается в размещении новых нагрузок с учётом доступной ёмкости вычислительных узлов.
Например, если несколько небольших приложений распределены по множеству почти пустых серверов, инфраструктура может использоваться неэффективно.
Более плотное размещение позволяет уменьшать фрагментацию ресурсов.
Но здесь важно не просто разместить максимальное количество контейнеров на одном сервере.
Необходимо сохранять достаточно ресурсов для новых релизов, восстановления после сбоев и обработки пиков нагрузки.
Поэтому binpack должен работать вместе с резервированием ресурсов и ограничениями контейнеров.
Именно в этом направлении я планирую дальше развивать Compute: повышать эффективность использования доступных мощностей, сохраняя предсказуемое поведение сервисов.
Конкретный процент экономии невозможно определить только по наличию binpack. Для этого нужны измерения на репрезентативных нагрузках.
Задача №4. Согласование желаемого и фактического состояния
Когда начинаешь управлять контейнерами на нескольких серверах, быстро сталкиваешься с проблемой согласования состояния.
Допустим, в PostgreSQL записано, что приложение должно иметь две работающие реплики.
Но один из вычислительных узлов перезагрузился, и фактически осталась только одна.
Или контейнер был создан, но управляющий слой не получил ответ об успешном запуске.
Получается, что желаемое состояние системы не совпадает с фактическим.
Desired State: app: api replicas: 2Actual State: node-1: api-1 running node-2: api-2 missing
Просто отправлять команды запуска и остановки в такой системе недостаточно.
Нужен механизм, который будет обнаруживать расхождения и приводить инфраструктуру к требуемому состоянию.
В Inquir Compute для этого используется reconciliation.
Control Plane хранит желаемое состояние, а вычислительные раннеры сообщают фактическое.
Контроллеры периодически сравнивают эти данные и инициируют необходимые действия.
События и адресные сигналы позволяют ускорять реакцию отдельных компонентов.
Desired State | v Reconciler <---- Actual State | v Calculate Diff | v Apply Actions | v Compute Nodes | v New Actual State
Это общий подход, который используется и в контроллерах Kubernetes.
Разница заключается в том, что в Compute reconciliation работает с собственными сущностями платформы.
Например, с приложениями, релизами, allocations и состоянием их выполнения.
Здесь возникает множество интересных инженерных вопросов.
Что делать, если узел временно недоступен?
Как отличить сетевую проблему от фактического завершения контейнера?
Как не создать две реплики вместо одной при повторной обработке операции?
Что делать с контейнером, который существует на сервере, но отсутствует в метаданных платформы?
Как восстановить приложение после перезапуска управляющего слоя?
Именно такие вопросы постепенно превращают простой менеджер Docker-контейнеров в полноценный оркестратор.
Задача №5. Сеть и изоляция пользователей
Когда приложения начинают взаимодействовать между собой, появляется ещё одна задача.
Представим типичный стек:
Internet | v HTTPS / API | v Backend / \ v v PostgreSQL Redis
Пользователю нужен публичный доступ к API, но совершенно необязательно выставлять PostgreSQL или Redis в интернет.
Поэтому в Inquir Compute сервисы могут взаимодействовать через приватную сеть рабочего пространства.
При настроенной и готовой приватной DNS-инфраструктуре приложение получает внутреннее имя, по которому к нему могут обращаться другие сервисы.
Публичный HTTP-доступ настраивается отдельно.
В текущей production-конфигурации приватные TCP-соединения поддерживаются, а произвольные публичные TCP-порты пока недоступны.
Для них потребуется дальнейшее развитие сетевого слоя и L4-маршрутизации.
Недоверенный код
Ещё один важный момент: в PaaS пользователь запускает на инфраструктуре платформы собственный код.
И такой код нельзя считать доверенным по умолчанию.
Необходимо ограничивать доступ к файловой системе, сети, секретам, системным вызовам и вычислительным ресурсам.
Для serverless-функций исходящий сетевой доступ по умолчанию запрещён, если он не был явно разрешён конфигурацией.
В реализации на containerd используются механизмы изоляции Linux, ограничения capabilities и no-new-privileges. Набор ограничений зависит от профиля нагрузки.
Однако обычные Linux-контейнеры разделяют ядро хостовой системы.
Поэтому контейнерная изоляция не полностью эквивалентна отдельной виртуальной машине.
В дальнейшем можно рассматривать более строгие варианты изоляции, например gVisor или Firecracker.
Это разные подходы, каждый со своими компромиссами по совместимости, производительности и потреблению ресурсов.
Пока они остаются возможными направлениями развития, а не заявленными компонентами текущей production-среды.
Задача №6. Длительные процессы и фоновые задания
Не все операции удобно выполнять внутри обычного HTTP-запроса.
Например, приложению необходимо обработать большой документ, обратиться к нескольким внешним API, сохранить результаты и отправить уведомление.
Такая операция может занимать минуты.
Держать HTTP-соединение открытым всё это время неудобно.
Поэтому в Compute есть асинхронные задания и пайплайны.
Пользователь запускает выполнение и получает идентификатор задания.
Позже по этому идентификатору можно проверить состояние и получить результат.
Для более сложных процессов используются пайплайны с несколькими шагами, ветвлениями и политиками повторов.
Периодические запуски также организованы через пайплайны.
Что происходит при сбое worker
Допустим, фоновый обработчик начал выполнять задачу, но процесс неожиданно завершился.
Если информация о выполнении хранилась только в памяти, состояние будет потеряно.
Поэтому задание сохраняется в PostgreSQL до передачи обработчику.
Для пайплайна также создаётся запись выполнения до начала исполнения графа.
Это позволяет восстановить управление выполнением после сбоя.
Но важно понимать, что сохранение состояния не означает автоматическое продолжение JavaScript-процесса с прерванной инструкции.
Восстановление зависит от сохранённых данных и политики повторных попыток.
Например, для обычного задания maxAttempts по умолчанию равен 1.
То есть наличие механизма повторов не означает, что любое выполнение автоматически будет перезапущено после ошибки.
Идемпотентность
Здесь появляется ещё одна проблема.
Предположим, функция отправляет запрос в платёжную систему.
Платёж выполняется успешно, но обработчик завершается до сохранения результата.
Если повторить выполнение, платёж может быть отправлен ещё раз.
Поэтому для внешних операций важна идемпотентность.
Например, можно использовать уникальный идентификатор операции, по которому внешняя система определяет, что действие уже выполнялось.
Дедупликация задания внутри платформы сама по себе не гарантирует однократное выполнение всех внешних действий.
Это отдельная задача, которую необходимо учитывать при проектировании надёжных фоновых процессов.
Как теперь выглядит развёртывание приложения
При всей внутренней сложности пользовательский процесс я стараюсь сохранять максимально простым.
Допустим, у нас есть Node.js-приложение с обычным Dockerfile:
FROM node:22-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .EXPOSE 3000CMD ["node", "server.js"]
Предполагается, что в проекте есть package.json, согласованный package-lock.json и server.js, запускающий HTTP-сервер на 0.0.0.0:3000.
Также стоит добавить .dockerignore, чтобы не отправлять ненужные локальные файлы в контекст сборки:
node_modules.git.env.env.*!.env.examplenpm-debug.log*
Для конкретного проекта список нужно дополнить путями к секретам. Сам .env.example должен содержать только примеры значений.
Устанавливаем CLI Inquir Compute. Для него требуется Node.js версии 22 или новее.
npm install -g @inquir/compute-cliinquir login
Создаём приложение с публичным HTTP-доступом и отдельным маршрутом проверки готовности:
inquir apps create web \ --port 3000 \ --ingress public \ --health-path /health
Маршрут /health должен быть реализован самим приложением.
Разворачиваем:
inquir apps deploy web
Платформа самостоятельно собирает Docker-образ из текущего каталога, создаёт релиз и запускает приложение.
Для HTTP-приложения с настроенным preview-доменом у нового релиза появляется отдельный адрес, по которому его можно проверить.
После успешного запуска переводим релиз в production:
inquir apps promote <releaseId>
Получаем логи:
inquir apps logs web --tail 100
Или откатываемся на предыдущий сохранённый production-релиз:
inquir apps rollback web
При этом не нужно устанавливать Docker Engine на компьютер разработчика или настраивать собственный Kubernetes-кластер.
Развёртывание, управление релизами, маршрутизация и запуск контейнеров выполняются внутри платформы.
Из нюансов: если CLI перестал ожидать завершения из-за таймаута, это не обязательно означает отмену удалённой операции. Сначала необходимо проверить её состояние.
AI-агенты и управление инфраструктурой
Ещё одно направление, которое я начал развивать, связано с AI-агентами.
Сегодня агент может написать приложение, подготовить Dockerfile, установить зависимости и помочь с исправлением ошибок.
Но результат всё равно необходимо собрать, развернуть и проверить.
Поэтому я добавил поддержку MCP в CLI Inquir Compute.
CLI может работать как MCP-сервер по stdio, позволяя совместимым агентам взаимодействовать с платформой.
Например, агент может развернуть функцию, вызвать её, получить результат и использовать диагностические данные для исправления ошибок.
В MCP-интеграции есть инструменты для разных сценариев.
deploy работает с функциями и web-mode, а compose_up предназначен для многокомпонентных стеков Container Apps.
При этом MCP не является автоматическим отображением всех команд обычного CLI. Каждый инструмент имеет свою область применения.
Отдельно необходимо учитывать безопасность.
AI-агент, который получил возможность управлять инфраструктурой, не должен автоматически получать неограниченные права.
Серверный API различает разрешения на чтение, изменение приложений, перевод релизов в production и выполнение команд внутри контейнеров.
MCP использует полномочия выданного токена и не заменяет механизмы авторизации.
В дальнейшем я планирую развивать это направление в сторону AIOps.
Например, автоматического анализа ошибок развёртывания, диагностики состояния сервисов и выполнения разрешённых эксплуатационных действий.
И здесь собственный оркестратор тоже оказывается полезен: агенты взаимодействуют с единой моделью приложений, ресурсов и состояний, а не с набором разрозненных инфраструктурных инструментов.
Что оказалось сложнее всего
Главный вывод, который я сделал за время разработки: PaaS гораздо сложнее, чем API для запуска контейнеров.
Контейнер можно создать одной командой.
Но для полноценной платформы нужно обеспечить управление его состоянием, восстановление после сбоев, распределение ресурсов, работу сети, хранение данных, обновления и диагностику.
Особенно непростыми оказались задачи, связанные с распределённым состоянием.
Control Plane хранит желаемое состояние системы в PostgreSQL, а вычислительные раннеры сообщают фактическое состояние процессов.
При сетевых сбоях, перезапусках и частично выполненных операциях эти состояния могут расходиться.
Необходимо не только обнаружить проблему, но и безопасно восстановить систему, не создав дублирующие контейнеры или конфликтующие операции.
Отдельной частью платформы стала наблюдаемость.
Раннеры собирают логи и метрики контейнеров, а сетевой слой формирует информацию о запросах и выполнениях.
Эти данные необходимы как пользователям, так и для диагностики самого оркестратора.
Наличие мониторинга, конечно, не гарантирует сохранение каждой записи при сбое хранилища или системы телеметрии. Поэтому надёжность сбора диагностических данных тоже требует отдельного внимания.
Отказоустойчивость Control Plane
Ещё одна важная задача, отказоустойчивость самого управляющего слоя.
В проекте уже есть разделение ролей Control Plane, механизмы выбора лидера контроллеров и документация по использованию Patroni для PostgreSQL.
Но наличие кода и конфигураций не означает, что все сценарии отказоустойчивости включены и проверены в production.
Поэтому я не буду утверждать, что платформа уже выдерживает любой отказ узла или управляющего компонента.
Такие возможности необходимо отдельно проверять в реальных условиях, включая потерю связи, перезапуски и восстановление базы данных.
Думаю, об этой части архитектуры можно будет написать отдельную статью.
Почему я считаю собственный оркестратор правильным выбором
Возвращаясь к вопросу из начала статьи: действительно ли стоило разрабатывать собственный оркестратор?
На мой взгляд, для Inquir Compute это решение оказалось оправданным.
И не потому, что Kubernetes или Nomad не могут решить аналогичные задачи.
Могут.
Но для Compute оркестрация стала частью самой продуктовой архитектуры.
Мне важно одновременно управлять несколькими моделями вычислений: постоянно работающими приложениями, serverless-функциями и фоновыми заданиями.
При этом они должны использовать общую инфраструктуру, единые правила размещения, ограничения ресурсов и механизмы восстановления.
Я хочу развивать собственную ресурсную модель, напрямую связанную с тарификацией платформы.
Хочу управлять жизненным циклом функций, их активацией и тёплыми репликами без второго независимого слоя планирования.
Хочу развивать алгоритмы размещения небольших приложений, чтобы эффективнее использовать доступные вычислительные мощности.
Именно эти задачи определяют дальнейшее развитие оркестратора.
Самым показательным результатом стал переход от отдельных механизмов выполнения функций к общей архитектуре allocation plane.
Теперь serverless-функции могут использовать те же механизмы размещения, проверки готовности и управления релизами, что и обычные приложения.
Это позволило объединить ранее независимые подсистемы, не добавляя второй полноценный оркестратор.
При этом я понимаю стоимость такого подхода.
Собственный планировщик и контроллеры требуют разработки, тестирования, сопровождения и постоянной работы над отказоустойчивостью.
Готовые решения имеют огромное преимущество в зрелости и количестве проверенных сценариев эксплуатации.
Для компании, которой просто нужно запускать приложения, написание собственного оркестратора почти наверняка окажется неоправданной тратой ресурсов.
Но для Inquir Compute управление вычислениями является одной из основных технологий продукта.
Поэтому развитие собственного оркестратора одновременно означает развитие возможностей платформы.
Я не стремлюсь создать универсальный аналог Kubernetes.
Моя цель, построить специализированную вычислительную платформу, в которой управление приложениями, serverless и распределение ресурсов работают как единая система.
И в этом контексте собственный оркестратор оказался не лишним компонентом, а основой выбранной архитектуры.
Что получилось и куда дальше
Сегодня Inquir Compute позволяет запускать веб-приложения, API, Telegram-ботов, базы данных, фоновые обработчики и serverless-функции.
Для контейнерных приложений доступны релизы, проверки работоспособности, preview-окружения, откаты, настройки ресурсов, приватная сеть и подключение собственных доменов.
Для функций есть несколько runtime, HTTP Gateway, тёплые реплики, асинхронные задания и пайплайны.
При этом платформа пока не пытается заменить весь набор возможностей крупных облачных провайдеров.
Например, PostgreSQL и Redis можно разворачивать из шаблонов как контейнерные сервисы с постоянным хранилищем, но это не эквивалент полноценного управляемого отказоустойчивого кластера СУБД.
Есть ограничения по ресурсам, стратегиям развёртывания и поддерживаемым сетевым сценариям.
Следующие направления развития связаны прежде всего с эффективностью размещения вычислительных нагрузок и автоматизацией эксплуатации.
Я планирую продолжать работу над ресурсной моделью, плотностью размещения приложений, механизмами восстановления после сбоев и интеграцией AI-агентов с платформой.
Ещё одно направление, которое мне интересно, это возможность развёртывания Inquir Compute на собственной инфраструктуре заказчика.
В таком сценарии пользователь получает готовый слой управления приложениями и функциями, но вычисления и данные остаются на его серверах.
Заключение
Когда я впервые рассказывал об Inquir Compute, это была платформа для выполнения serverless-функций.
С тех пор проект существенно изменился.
Появились полноценные контейнерные приложения, собственная модель релизов, вычислительные раннеры, allocation plane, планировщик и поддержка containerd.
Но главное изменение даже не в количестве новых возможностей.
По мере развития платформы я всё глубже погружался в задачи, которые обычно скрыты внутри готовых PaaS.
Когда пользуешься облачной платформой, развёртывание приложения, переключение релиза, масштабирование или восстановление контейнера выглядят как несколько действий в интерфейсе.
Когда начинаешь разрабатывать такую платформу самостоятельно, каждое из этих действий превращается в отдельную инженерную задачу.
Именно эта часть разработки оказалась для меня наиболее интересной.
Я продолжаю развивать Inquir Compute и буду рад, если кто-то захочет попробовать платформу на своих проектах.
Актуальные условия доступа доступны на сайте, а технические возможности и инструкции описаны в документации.
Особенно интересна обратная связь от разработчиков, которые используют Kubernetes, Nomad, Docker или самостоятельно управляют инфраструктурой.
А кто-нибудь из вас тоже приходил к решению написать собственный оркестратор? В каком случае этот выбор оказался оправданным, а когда было бы разумнее использовать готовую систему?
ссылка на оригинал статьи https://habr.com/ru/articles/1101392/