Производительность, нагрузка и память
🔵 Как 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):
agentos_in_flight_turns,agentos_admission_limit,agentos_admission_rejected_total— перегрузка и 429;agentos_active_sessions,agentos_active_processes— размер рабочего набора;agentos_event_db_write_wait_ms,agentos_message_store_queue,agentos_access_log_queue,agentos_event_db_dropped_total— заторы писателей;agentos_turn_duration_ms,agentos_tokens_prompt_total/_completion_total— латентность и потребление токенов.
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
- Мок-провайдер (
crates/host/examples/mock_llm.rs) отдаёт OpenAI-совместимый SSE с настраиваемымиMOCK_TTFT_MS,MOCK_TPS,MOCK_TOKENSи инжекцией ошибок; на/statsпоказываетmax_concurrent(доказательство, что ядро не превысило лимит). - Лоадген (
crates/host/examples/loadgen.rs) меряет TTFT и полное время хода по SSE,--sessions == --concurrency, каждый воркер — своя сессия. - Конфиги стенда:
testing/config-scale*(без персистентности / с полной),max_in_flight_turns: 0,concurrency_limit: 1000.
Профиль стенда для чисел ниже: 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 | — |
- Про «70 MB на старте». Собственная память ядра на старте — всего
~9 MB; WorkingSet 30–70 MB — это в основном разделяемые, demand-paged
страницы бинарника (
agent-os.exe~62 MB) иduckdb.dll(~35 MB), которые подгружаются по мере вызова кода и ОС может вернуть. Это фиксированная, не растущая статья, а не утечка. - Настоящая фиксированная стоимость — DuckDB: включение базы поднимает private-память с ~9 MB до ~98 MB (буферный менеджер, даже без нагрузки). Поэтому «70 MB» — неверный ориентир: считайте базу DuckDB (~100 MB) и инкремент на сессию.
- Прирост на сессию ≈ 0.13 MB (без сторов) / 0.19 MB (с базой) и держится линейным: одна сессия = процесс ядра + рабочий контекст.
- Память не освобождается мгновенно: сессии живут до
session_ttl_secs(по умолчанию 300 s). Под долгим простоем чистильщик возвращает RSS к базе. - Транскрипты и события лежат на диске (DuckDB), а не в памяти; рост RSS — это состояние процессов/сессий, а не история.
34.5 Поведение при перегрузке (429)
Включите kernel.max_in_flight_turns > 0 (например, 8 при
concurrency_limit: 3) — и перегрузка перестаёт копить очереди:
- сверх лимита запросы сразу получают
429+Retry-After, дёшево; - принятые запросы дожимаются до конца,
5xxи обрывов SSE нет; - отклонённый запрос не отменяет уже запущенный ход в той же сессии;
- восстановление после спада нагрузки — секунды.
Рекомендация: в проде всегда включайте 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 | — | — |
- Накладные на ход при c=1: Agent OS ≈ 95 ms, opencode ≈ 190 ms.
- Agent OS держит латентность плоской до 1000; opencode при росте c платит латентностью (c=128 → p50 6.6 s, c=256 → 11.9 s), без admission-гейта на 256 появляются ошибки, на 512 — отказ обслуживания.
- При сопоставимой латентности разрыв пропускной способности — порядок (68 vs 21 rps при c=128; 451 vs <40 при высоких c).
Память
| 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 |
- У opencode выше фиксированная база (сам рантайм), инкремент на сессию — сопоставим. Под реальной нагрузкой opencode наблюдался на 550–680 MB при сотнях сессий и не отдаёт память обратно так же охотно, как Agent OS с TTL-чистильщиком.
- Для оценки RAM Agent OS: `~9 MB (ядро) + ~100 MB (база DuckDB, если включена)
- 0.13–0.19 MB × одновременных сессий`.
Итог сравнения
Agent OS заточен под конкурентную многотенантную нагрузку: плоская латентность, линейный рост throughput, 429-защита и TTL-очистка. opencode-сервер в этой роли деградирует: латентность растёт линейно, при перегрузке — ошибки/зависание без защиты. Это ожидаемо: у него другая задача (интерактивный coding-агент на одного пользователя), а не сервер под тысячи одновременных сессий.
34.7 Практические рекомендации
- Провайдер-лимит:
concurrency_limit= сколько одновременных стримов реально выдерживает ваш API-ключ. Это главный рычаг. - Всегда включайте admission:
max_in_flight_turns > 0в проде, чтобы перегрузка давала быстрый429, а не очереди и таймауты. - Персистентность — по потребности: JSONL можно выключать (
jsonl_dir: ""); DuckDB-база нужна для транскриптов/событий, но это ~100 MB фиксированной памяти и основной затор на высоких ставках. - TTL сессий: держите
session_ttl_secsразумным — это возврат памяти под простоем. - Планирование RAM:
~ (100 MB база, если включена) + 0.13–0.19 MB × одновременных сессий, плюс запас на всплески; «стартовые 70 MB» — в основном reclaimable-страницы кода, а не рабочая память. - Мониторьте
/metrics:in_flight_turns,admission_rejected_total, очереди писателей (message_store_queue,access_log_queue),event_db_dropped_totalиturn_duration_ms. - Регрессии: держите baseline-числа и гоняйте
scripts/loadtest.ps1в CI (см. 16-benchmarking); тревожьтесь при росте p95/RSS больше чем на ~20–25%.