Продвинутый

Оркестрация агентов на S3 с помощью Rivet

Алексей Кузнецов
Алексей Кузнецов
Системный администратор6 ноября 2026 г.14 мин чтения

Rivet — лёгкий оркестратор агентных нагрузок на актерной модели. Состояние агентов хранится в S3, убирая отдельные песочницы и снижая нагрузку.

Оркестрация агентов на S3 с помощью Rivet

Rivet — лёгкий оркестратор агентных нагрузок на актерной модели. Состояние агентов хранится в S3, убирая отдельные песочницы и снижая нагрузку.

Что такое Rivet и зачем он нужен

Что такое Rivet и зачем он нужен

Я уже не раз встречал проекты, где на десяток агентов приходилось десяток Docker-контейнеров. Каждый в своём окружении, с собственными томами, переменными окружения и зависимостями. В начале это выглядит надёжно, но с ростом числа агентов инфраструктура раздувается быстрее, чем нагрузка.

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

По сути Rivet делает для агентов то, что Docker Compose сделал для микросервисов: собирает разрозненные части в одно предсказуемое целое. Только вместо манифестов с портами и томами вы описываете поведение агентов и правила их взаимодействия.

Актерная модель вместо тяжёлых песочниц

В классической схеме каждый агент запускается в своём контейнере. Это надёжно, но дорого: на запуск уходит время, память расходуется на дублирование библиотек, а при росте числа агентов инфраструктура раздувается быстрее, чем нагрузка.

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

Rivet использует это, чтобы убрать отдельные песочницы. Все акторы живут в одном или нескольких процессах оркестратора. Изоляция обеспечивается не границами контейнера, а границей сообщения: актор А не может напрямую изменить состояние актора Б, он может только отправить ему сообщение.

На практике это выглядит так. Вместо того чтобы поднимать для каждого агента отдельный Docker-контейнер:

yaml
# Пример структуры конфигурации Rivet (упрощённо)
agents:
  - name: document-processor
    type: worker
    state_backend: s3
    concurrency: 4

  - name: notification-sender
    type: worker
    state_backend: s3
    concurrency: 2

router:
  type: round-robin
  default_agent: document-processor

вы запускаете один процесс Rivet, который диспетчеризует сообщения между акторами. Состояние каждого актора не лежит в памяти контейнера, а записывается во внешнее хранилище — об этом ниже.

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

Где Rivet хранит состояние агентов

Главное архитектурное решение Rivet — состояние агентов не хранится локально. Оно выносится в S3-совместимое хранилище. Когда актору нужно прочитать или записать данные, он обращается к S3, а не к своему диску или базе на той же ноде.

Это даёт сразу несколько эффектов:

  1. Агент можно перезапустить на любой ноде кластера, и его состояние подтянется из S3 автоматически.
  2. Нет необходимости монтировать сетевые диски или синхронизировать файлы между нодами.
  3. Нагрузка на локальные дисковые подсистемы падает, потому интенсивная работа с состоянием уходит во внешнее хранилище.

В качестве S3-бэкенда может выступать AWS S3, MinIO, Ceph или Cloudflare R2. Последний особенно удобен, если вы уже используете инфраструктуру Cloudflare и не хотите держать собственное хранилище.

Пример подключения хранилища в конфигурации:

yaml
storage:
  type: s3
  endpoint: https://s3.example.com
  bucket: rivet-agent-state
  region: eu-central-1
  # credentials подтягиваются из переменных окружения или IAM-роли

Для быстрых метаданных — например, очередей задач или текущего статуса агентов — Rivet может использовать PostgreSQL или Redis-совместимую базу. Если вам нужна лёгкая альтернатива Redis, присмотритесь к KiviDB: быстрая Redis-совместимая БД на Rust. Для реляционных метаданных подойдёт PostgreSQL, как в примере с локальной системой задач на PostgreSQL.

Если данные в S3 содержат конфиденциальную информацию, стоит подумать о шифровании ещё до загрузки в хранилище. Один из подходов описан в материале про шифрование данных на стороне клиента с ULDA.

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

Как Rivet использует S3 для хранения состояния

Объектное хранилище вместо базы данных

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

Rivet поступает иначе. Состояние каждого актора пишется в S3 как отдельный объект. Ключ объекта — идентификатор агента, тело — JSON с текущим состоянием. Нет отдельных песочниц, нет необходимости держать состояние в памяти агента между вызовами.

На практике это выглядит так. Агент weather-collector завершил сбор данных и записал результат:

json
{
  "agent_id": "weather-collector-01",
  "status": "idle",
  "last_run": "2024-01-15T10:30:00Z",
  "result": {
    "city": "Москва",
    "temperature": -12,
    "collected_at": "2024-01-15T10:29:58Z"
  }
}

Объект лежит в бакете rivet-state по пути agents/weather-collector-01.json. Когда Rivet нужно запустить агента снова, он загружает этот объект, восстанавливает состояние и продолжает с того места, где остановился.

Конфиг Rivet для подключения к S3:

yaml
rivet:
  storage:
    driver: s3
    bucket: rivet-state
    endpoint: https://s3.example.com
    region: ru-central-1
    prefix: agents/
    credentials:
      access_key_id: ${RIVET_S3_ACCESS_KEY}
      secret_access_key: ${RIVET_S3_SECRET_KEY}
  concurrency: 100

Перед запуском стоит убедиться, что бакет существует и доступен:

bash
aws s3api head-bucket --bucket rivet-state

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

Во-первых, состояние агента не привязано к конкретному контейнеру. С практической точки зрения, такой подход даёт предсказуемость: данные всегда лежат в одном месте, не зависят от того, на какой ноде запущен контейнер, и переживают перезапуск любого компонента инфраструктуры.

Преимущества перед Redis и PostgreSQL

Если мы посмотрим на типичный стек для хранения состояния, там обычно Redis для быстрого доступа и PostgreSQL для надёжности. Для агентных нагрузок на актерной модели оба решения работают, но с оговорками.

Redis хранит данные в оперативной памяти. Даже с включённой персистентностью (RDB или AOF) восстановление после сбоя занимает время, а при большом количестве агентов память кончается быстрее, чем дисковое пространство. Если вам нужен Redis-совместимый вариант, посмотрите на KiviDB: быстрая Redis-совместимая БД на Rust, но для состояния агентов она решает не ту проблему.

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

S3 убирает эти нюансы. Нет схемы, нет миграций, нет необходимости следить за размером памяти. Объект записался — состояние сохранено. Объект прочитан — агент продолжил работу. Стоимость хранения в объектном хранилище ниже, чем поддержание кластера PostgreSQL или Redis под такую нагрузку.

Пример проверки состояния агента через AWS CLI:

bash
aws s3api get-object \
  --bucket rivet-state \
  --key agents/weather-collector-01.json \
  /tmp/agent-state.json

Если нужно зашифровать данные на стороне клиента перед отправкой в S3, можно использовать подход из статьи про шифрование данных на стороне клиента с ULDA — это добавит уровень защиты без изменения логики Rivet.

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

Масштабирование и высокая плотность агентов

Когда агентов становится больше десятка, вопрос перестаёт быть «как запустить» и превращается в «как не утонуть в ресурсах». Традиционный подход — каждый агент в своём контейнере, со своими лимитами памяти и CPU — быстро упрётся в потолок одной ноды. Rivet решает это иначе: состояние актера живёт в S3, а сам актер существует только тогда, когда обрабатывает сообщение. Ниже разберём, как это работает на практике и как распределять нагрузку между нодами, чтобы сервер не лежал от одного всплеска активности.

Лёгкие акторы и использование ресурсов

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

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

С практической точки зрения, это значит, что вы можете держать десятки тысяч агентов на скромном железе, потому что в памяти находится только то, что обрабатывается прямо сейчас. Для сравнения: если бы каждый агент жил в своём Docker-контейнере с Alpine и Node.js, вы бы потратили гигабайты ОЗУ только на бездействие.

Вот пример минимального конфига Rivet, где состояние актеров направляется в S3-совместимое хранилище:

yaml
# rivet.yaml
actors:
  storage:
    type: s3
    endpoint: https://s3.example.com
    bucket: rivet-agent-state
    region: ru-central1
    access_key_id: ${S3_ACCESS_KEY}
    secret_access_key: ${S3_SECRET_KEY}

  runtime:
    max_concurrent_actors: 1000
    idle_ttl_seconds: 300
    message_timeout_seconds: 30

resources:
  cpu_limit: "2"
  memory_limit: "4Gi"

Параметр idle_ttl_seconds здесь критичен: через пять минут бездействия актер выгружается из памяти, а его состояние остаётся в S3. Если через час пришло новое сообщение — Rivet развернёт актера на любой ноде, где есть место, и подгрузит состояние. Это и даёт высокую плотность: вы платите ресурсами только за активных агентов, а не за зарегистрированных.

Метаданные о маршрутизации и планировании могут храниться в PostgreSQL, а горячий кэш часто используемых состояний — в Redis или его Rust-аналоге, например KiviDB: быстрая Redis-совместимая БД на Rust. Локальные отладочные данные удобно смотреть через SQLite, если нужно быстро заглянуть в журнал событий без подключения к кластеру — подробнее про работу с SQLite в браузере можно прочитать в этой статье.

Команда для проверки текущей загрузки актеров:

bash
rivet actors list --namespace production --format table

Вывод покажет, сколько агентов активно в данный момент, а сколько ожидают в S3. Если видите, что активных актеров мало, а памяти потребляется много — значит, где-то утекает состояние или не работает выгрузка по TTL.

Главное правило здесь — предсказуемость конфигурации. Я всегда пишу конфиги руками, даже если есть GUI, потому что только так видно, какой лимит памяти установлен и зачем. GUI для тех, кто не знает, где лежит конфиг.

Распределение нагрузки между нодами

Когда агентов десятки тысяч, а нод несколько, возникает вопрос: кто решает, на каком сервере развернуть актер? Rivet использует планировщик, который смотрит на загрузку нод, доступность S3 и локальность данных.

Если актер уже работал на ноде А и его состояние кэшировано там, планировщик попытается вернуть его на ту же ноду. Если нода А перегружена — актер уйдёт на ноду Б, загрузит состояние из S3 и продолжит работу.

Это отличается от классического подхода, где каждый сервис привязан к конкретному контейнеру на конкретном хосте. Здесь актер мигрирует, как файл между папками, потому что состояние не привязано к диску ноды.

Пример конфига кластера из трёх нод:

yaml
# rivet-cluster.yaml
nodes:
  - id: node-vinni
    address: 10.0.0.10:8080
    role: worker
    weight: 1
  - id: node-piglet
    address: 10.0.0.11:8080
    role: worker
    weight: 1
  - id: node-owl
    address: 10.0.0.12:8080
    role: worker
    weight: 1

scheduler:
  strategy: least_loaded
  rebalance_interval_seconds: 60
  drain_timeout_seconds: 300

state:
  backend: s3
  bucket: rivet-agent-state
  consistency: strong

Параметр weight позволяет отдавать предпочтение более мощному железу. Если одна нода — старый Dell PowerEdge, а другая — свежий сервер, вы можете дать второй вес 2, и планировщик будет чаще размещать там новых актеров.

Команда для проверки состояния нод:

bash
rivet nodes status --cluster production

Пример вывода:

NODE         STATUS   ACTOR_COUNT   CPU_LOAD   MEM_LOAD
node-vinni   ready    1420          45%        62%
node-piglet  ready    1380          42%        58%
node-owl     ready    1450          48%        65%

Если одна нода выходит из строя, актеры на ней не теряются: их состояние в S3, и при следующем сообщении Rivet развернёт их на живой ноде. Время восстановления зависит от скорости чтения из S3 и размера состояния, но обычно это секунды, а не минуты.

Для метаданных о распределении и истории перемещений актеров удобно использовать PostgreSQL — например, если вы уже разворачивали локальную систему задач на PostgreSQL, как в этом руководстве, то схему можно использовать аналогично. Если же нужно шифровать данные перед отправкой в S3, посмотрите в сторону клиентского шифрования — в статье про ULDA разобрано, как это сделать без потери производительности.

Вывод: высокая плотность агентов в Rivet достигается за счёт разделения вычислений и состояния. Актеры лёгкие, потому что живут в памяти только во время обработки сообщения, а состояние лежит в S3. Распределение между нодами становится тривиальным, потому что любой сервер может взять любого актера — достаточно доступа к общему хранилищу. Это даёт предсказуемость при росте: добавили ноду, кластер сам перебалансировал нагрузку, и вам не пришлось переписывать конфиги каждого агента вручную.

Ограничения и сценарии применения

Rivet оправдан, когда у вас много лёгких агентов, состояние которых можно размазать по S3, а взаимодействие строится на асинхронных сообщениях. Но это не универсальный движок для любого агентного workflow. Ниже разберём, когда от него стоит отказаться и чем заменить.

Когда Rivet не подходит

Во-первых, Rivet плохо подходит, если нужна строгая консистентность состояния между агентами. S3 по умолчанию обеспечивает eventual consistency — запись не видна сразу всем чтителям. Если агент А обновил ключ, а агент Б должен прочитать его через миллисекунды и принять решение, вы получите гонку данных. Для таких сценариев лучше использовать базу данных с транзакциями, например PostgreSQL, и писать состояние туда, а не в объектное хранилище.

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

В-третьих, отладка распределённых агентов, состояние которых размазано по объектам S3, сложнее, чем отладка монолитного приложения. Вы не можете просто зайти в контейнер и посмотреть логи — нужно собирать их из разных мест, сопоставлять по идентификаторам актеров. Если команда не готова к этому, Rivet станет источником инцидентов.

Наконец, если у вас уже работает кластер Kubernetes и вы привыкли управлять нагрузкой через Deployment, HPA и Service, вводить отдельный оркестратор ради нескольких агентов — избыточно. Проще запустить агентов как Pod’ы и обмениваться сообщениями через Redis или RabbitMQ.

Пример: проверка, что порт 443 не занят перед запуском агента:

bash
ss -tlnp | grep ':443'

Если порт занят, агент не стартует. В Rivet такую проверку придётся встроить в саму логику актера, а не вынести в инфраструктуру.

Альтернативы для сложных агентных workflow

Если Rivet не подходит, смотрите в сторону инструментов, которые лучше справляются со сложными зависимостями и состоянием.

Temporal — рабочие процессы с кодом на Go, TypeScript, Java и других языках. Состояние хранится в базе данных (PostgreSQL, MySQL, Cassandra), есть встроенные ретраи, таймауты и сигналы. Подходит, когда агент должен выполнить последовательность шагов с откатом при ошибке. Temporal

AWS Step Functions — если инфраструктура уже в облаке, можно описать агентное workflow как машину состояний. Интеграция с Lambda, S3 и DynamoDB из коробки. Минус — привязка к экосистеме AWS и стоимость вызовов переходов. AWS Step Functions

Celery + Redis — классическая очередь задач для Python. Агенты — это воркеры, состояние можно держать в Redis или PostgreSQL. Проще отлаживать, чем распределённых актеров, но нет встроенной модели актеров — придётся реализовывать саму логику взаимодействия. Celery

Собственное решение на PostgreSQL — если нагрузка небольшая, состояние агентов можно хранить в обычной таблице с полем status и использовать SELECT ... FOR UPDATE для блокировки. Это убирает необходимость в отдельном оркестраторе. Пример схемы:

sql
CREATE TABLE agents (
    id UUID PRIMARY KEY,
    name TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'idle',
    payload JSONB,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Для быстрого кэширования состояния между агентами можно использовать KiviDB: быстрая Redis-совместимая БД на Rust — она работает поверх локального хранилища и не требует отдельного кластера Redis.

Если нужно хранить зашифрованные данные на стороне клиента перед отправкой в S3, посмотрите на Шифрование данных на стороне клиента с ULDA.

Для метаданных агентов подойдёт SQLite, а тяжёлые бинарные состояния можно выносить в S3 или Cloudflare R2. Подробнее о работе с SQLite в браузере — в этой статье.

В общем, Rivet оправдан, когда у вас много независимых агентов, состояние которых не требует строгой консистентности, а взаимодействие строится на асинхронных сообщениях. Во всех остальных случаях проще взять инструмент с явным управлением состоянием — будь то Temporal, PostgreSQL или Celery.

Поделиться:TelegramX / TwitterVK

Читайте также