Производительность, нагрузка и память

🔵 Как Agent OS ведёт себя под нагрузкой: сколько одновременных обращений держит, как ограничивает перегрузку, сколько ест памяти на сессию и как это воспроизвести. Плюс сравнение с другим agent-сервером на том же стенде.

Числа в статье — с одного конкретного стенда (см. §34.2) и служат ориентиром для планирования, а не гарантией на вашем железе.


34.1 Что ограничивает нагрузку

Agent OS — не балансировщик и не делает инференс сама: LLM — это внешний провайдер, а ядро планирует обращения к нему. Поэтому «потолок» задают три вещи: лимиты ядра, поведение при перегрузке и стоимость персистентности.

Рычаг Где По умолчанию Что делает
kernel.concurrency_limit server.yaml 3 Максимум одновременных LLM-стримов к провайдеру (семафор ядра). Лишние ходы встают в очередь.
kernel.max_in_flight_turns server.yaml 0 (выкл) Admission-гейт на HTTP-краю: не больше N ходов «в полёте»; сверх — сразу 429 Too Many Requests + Retry-After. Отклонённый запрос не трогает сессию и не отменяет запущенный ход.
kernel.session_ttl_secs server.yaml 300 Сессия без активности убивается через TTL; чистильщик бежит каждые session_cleanup_interval_secs (60). Ограничивает рост памяти под долгим простоем.

Одна сессия — один активный ход. Если в ту же сессию приходит второй запрос, он отменяет предыдущий (last-wins), а не встаёт в очередь. Это защищает от гонок внутри одной сессии.

Персистентность. Сторы пишут данные хода: JSONL-логгер, EventDb (events.db), message store (messages.db) и access-лог (access.db). Все они вынесены в фоновые потоки-писатели с батчингом (одна транзакция на пачку), поэтому не блокируют async-путь; но на очень высоких ставках их потоки начинают конкурировать между собой (см. §34.3). JSONL — необязательный слой; DuckDB-база (события/транскрипты/доступ) несёт основную функциональность.

Что смотреть в рантайме (GET /metrics, Prometheus):


34.2 Как воспроизвести замеры

Стенд из репозитория (нужны mock_llm + loadgen, сборка release-fast):

cargo build -p agent_web_bot --profile release-fast \
    --bin agent-os --example mock_llm --example loadgen
./scripts/loadtest.ps1                 # свип 1,2,4,8,16,32,64
./scripts/loadtest.ps1 -Sweep 128,256,1000 -DurationSecs 15

Профиль стенда для чисел ниже: mock TTFT=2000ms, 200 tok/s, 8 токенов (ход ≈ 2.04 s), 18 логических CPU, мок + сервер + лоадген на одной машине, 12–15 s на точку. Мерить лучше на чистой машине: посторонние процессы и накопленные БД заметно искажают результат.


34.3 Пропускная способность

Agent OS без персистентности (чистый планировщик + HTTP) — «потолок» ядра:

c rps total p50 TTFT p50 ошибки
1 0.53 2135 ms 2028 ms 0
4 2.17 2126 ms 2027 ms 0
16 8.61 2140 ms 2029 ms 0
64 33.4 2138 ms 2040 ms 0
128 68.3 2151 ms 2045 ms 0
256 128.9 2163 ms 2065 ms 0
1000 459.5 2215 ms 2090 ms 0

Латентность практически плоская: от c=1 до c=1000 TTFT вырос с 2028 до 2090 ms, полное время хода — с 2135 до 2215 ms. Накладные самого ядра ≈ +30 ms к TTFT и +95 ms к ходу и почти не зависят от конкуренции. Пропускная способность растёт линейно, пока хватает CPU и permit'ов.

Agent OS с базой DuckDB (EventDb + message store + access-лог; JSONL выкл):

c rps total p50 TTFT p50 примечание
1 0.53 2142 ms 2025 ms —
4 2.15 2150 ms 2032 ms —
16 8.58 2149 ms 2033 ms —
64 34.3 2146 ms 2045 ms —
128 68.4 2150 ms 2047 ms —
256 136.2 2159 ms 2059 ms —
1000 451.0 2135 ms 2055 ms message_store_queue до 7k; event_db_dropped 0

До ~1000 одновременных ходов база стоит всего ~2% пропускной способности (451 vs 459) и не двигает латентность. Выше ~3000 throughput упирается в конкурирующие писатели DuckDB (события + транскрипты + access), очередь message store переполняется и часть событий отбрасывается под перегрузкой (event_db_dropped) — задумайтесь о retention/шардинге, если держите тысячи одновременных сессий с полной историей.


34.4 Память

Меряем две величины процесса сервера: WorkingSet (резидент, включая разделяемые страницы кода/DLL) и Private (собственная память). На Windows — Get-Process; на Linux — VmRSS и Private_Dirty из /proc/<pid>/smaps_rollup.

Состояние WorkingSet Private
Без сторов, старт (steady) 31 MB 9 MB
С базой DuckDB, старт 108 MB 98 MB
Без сторов, ~1450 сессий 218 MB —
С базой DuckDB, ~1470 сессий 385 MB —

34.5 Поведение при перегрузке (429)

Включите kernel.max_in_flight_turns > 0 (например, 8 при concurrency_limit: 3) — и перегрузка перестаёт копить очереди:

Рекомендация: в проде всегда включайте admission-гейт, а concurrency_limit выставляйте по квоте провайдера (чтобы не ловить внешние 429/таймауты).


34.6 Сравнение с opencode

Тот же стенд, тот же мок (TTFT=2000 ms, 8 токенов), сессия на воркер. opencode (opencode serve v1.18.32) направлен на тот же мок через кастомный провайдер @ai-sdk/openai-compatible; измерялось полное время хода (POST /session/:id/message).

Важно — честное сопоставление. opencode персистит сессии/транскрипты в SQLite, поэтому сравнивать его нужно с Agent OS с базой (EventDb + message store + access; JSONL выкл), а не с чистым ядром. При этом opencode-сервер — это сервер однопользовательского coding-агента (сессии привязаны к проекту, admission-контроля нет), а Agent OS — многотенантный чат/агент-сервер. Сравнение про характер масштабирования, не про продуктовый паритет.

Пропускная способность (Agent OS с базой vs opencode)

c Agent OS rps opencode rps Agent OS p50 opencode p50 opencode ошибки
1 0.53 0.50 2142 ms 2229 ms 0
4 2.15 2.00 2150 ms 2285 ms 0
16 8.58 6.67 2149 ms 2791 ms 0
64 34.3 16.0 2146 ms 4386 ms 0
128 68.4 21.3 2150 ms 6611 ms 0
256 136.2 36.2 2159 ms 11883 ms 142
512 — зависание — >27 s массовые
1000 451.0 — 2135 ms — —

Память

Agent OS (без сторов) Agent OS (база DuckDB) opencode
База (старт, WorkingSet) 31 MB 108 MB 208–336 MB (Node, колеблется из-за GC)
База (private) 9 MB 98 MB —
Прирост на сессию ≈ 0.13 MB ≈ 0.19 MB ≈ 0.09 MB (пустая)
~1500 сессий (WorkingSet) 218 MB 385 MB под нагрузкой 500–680 MB

Итог сравнения

Agent OS заточен под конкурентную многотенантную нагрузку: плоская латентность, линейный рост throughput, 429-защита и TTL-очистка. opencode-сервер в этой роли деградирует: латентность растёт линейно, при перегрузке — ошибки/зависание без защиты. Это ожидаемо: у него другая задача (интерактивный coding-агент на одного пользователя), а не сервер под тысячи одновременных сессий.


34.7 Практические рекомендации