Status

active. Phase 1 (internal logging off, коммит da4ef041) применена 2026-05-19. Phase 1b (memory bump 256Mi/512Mi → 512Mi/1Gi коммит 771e9046 + GOMEMLIMIT=900MiB коммит 103c2b92) применена в тот же день после обнаружения вторичного OOM. Phase 2 — open, пересматривается по триггерам ниже.

Контекст

bifrost — наш LLM gateway. На staging recurrent OOM (BG-12021, алерты с 10 апреля 2026 — три PodOOMKilled подряд: 2:24, 6:29, 10:34) имел два независимых источника, диагностированных последовательно:

Источник #1 — SQLite logs leak (slow grow, часы). По-умолчанию bifrost писал per-call inference data (request body, response, tokens, cost, latency, retries, virtual-key, fallback chain) в локальную SQLite БД ./logs.db внутри pod’а. Файл жил в container’s ephemeral root fs (нет PVC, нет emptyDir). Default retention — 365 дней, SQLite не VACUUM после delete, файл рос монотонно → OOM каждые ~4 часа на 512Mi limit. Pattern: pod рестартал, очищал БД, через 4-8 часов снова.

Источник #2 — concurrent in-flight buffering (burst, минуты). Bifrost под algo-hub fanout обрабатывает ~30+ параллельных POST /v1/chat/completions калов по 5-30 секунд каждый. На пиковую нагрузку in-memory держится: request bodies, accumulating response buffers (LLM payloads 100KB-MB+), retry/fallback state, goroutine stacks, plugin chain clones. Bifrost vendor claim про low memory overhead (~11µs proxy footprint) относится к idle/single-request baseline — peak concurrent buffering выходит далеко за 512Mi. Без bumped limits Phase 1 alone не закрывала OOM: после deploy логирующего фикса pod был OOMKilled через 20 минут (наблюдалось 2026-05-19, Exit Code 137 после deploy da4ef041).

Параллельно existing observability:

  • langfuse (HIPAA region) уже инструментирует analysis-worker, b2c-dashboard, b2b-api, recommendations-portal, patient-portal — trace на бизнес-операцию + generation на каждый LLM-call (модель, prompt-version, tokens, latency, cost, scores). Покрывает все production-сервисы кроме algo-hub (см. llm-observability-stack, секция «Слепые пятна»).
  • Bifrost admin UI на :8080 показывал тот же набор данных через /api/logs — фактически duplicate-источник, плюс уязвим к pod restart (полное обнуление) и наблюдённой count/size-based rotation поверх 365-day retention (записи ~37 минут old недоступны при uptime 157 минут — llm-observability-stack фиксирует это как «retention двухуровневый gotcha»).

Вопрос: какой способ хранить bifrost-side per-call data выбрать, чтобы остановить OOM, не плодя дублирующую инфру.

Рассматривали

Bifrost native flags — disable_content_logging + log_retention_days. Оставить logging on, но без request/response bodies + сократить retention до 7-30 дней. Тела — 95% объёма каждой записи, без них размер БД уменьшается на порядки. Сохраняет dashboard (latency/cost/tokens/model). Минус — SQLite не shrinks без VACUUM, поэтому leak только замедляется, не исчезает. Через недели снова OOM.

Postgres для logs_store. Bifrost нативно поддерживает Postgres вместо SQLite. У нас есть inngest-analysis-db (CloudSQL PG 16) с additional_databases = [] — можно добавить БД bifrost бесплатно через terragrunt. Решает память (данные не в RAM пода) и persistent storage за один шаг. Минус — bifrost делит CPU/IO инстанса с inngest/b2b_edge, plus operational overhead на terraform/secret/CloudSQL Proxy. Реализуем в Phase 2 если решим что gateway-audit нужен.

PVC + SQLite. Mount persistent disk вместо ephemeral fs. Не давит на RAM. Минус — bifrost deployment становится stateful (нельзя scale → 2 реплики без shared-disk-проблем), SQLite файл всё равно растёт без VACUUM, через месяцы PVC fills.

emptyDir на node disk. Дать том на ноде с sizeLimit. Запись на диск, не в RAM → OOM по памяти уходит сразу. Bounded. Минус — эфемерно (теряется при перешедулинге пода), и при заполнении SQLite перестаёт писать → может встать прокси.

Sidecar / CronJob для periodic VACUUM. Раз в час exec в pod, чистить старые записи + VACUUM. Минус — race с writes, VACUUM лочит БД, bifrost image не содержит sqlite3 CLI, нужен sidecar. Costly maintenance.

Just raise memory limit. Band-aid. Растёт медленнее → OOM реже, но не исчезает.

Disable logs_store completely. Полностью убирает leak. Bifrost остаётся stateless. Langfuse уже покрывает per-call data на application-слое. Минус — теряем gateway-level visibility: governance rejects, VK denials, provider routing decisions — application видит только final HTTP-статус от bifrost.

Forward logs в external sink — OTLP в Langfuse-OTel. Bifrost поддерживает OpenTelemetry export и нативный Langfuse plugin: вместо записи в SQLite gateway отправляет structured traces в Langfuse-OTel collector. Application-traces и gateway-traces оказываются в одном месте — единая корреляция по trace-id, без дублирующей инфры. Минус — нужен research конкретного config (какие именно flags/plugin в bifrost, что Langfuse-OTel ingest принимает, какие fields маппятся), и trace volume в Langfuse удваивается. Это ведущий кандидат на Phase 2 если выяснится что gateway-audit нужен.

stdout-only + Cloud Logging. Отключить logs_store, переключить bifrost на structured stdout-logging — Cloud Logging уже работает на GKE, поиск через Logs Explorer. Минус — bifrost stdout сейчас содержит только startup + HTTP-access события, не inference; не задокументировано поддерживает ли bifrost structured per-call stdout-режим.

Выбрали

Phase 1: client.enable_logging: false + logs_store.enabled: false — закрывает источник #1 (SQLite leak).

Изменение в argocd-applications/common/bifrost-proxy/values.yaml — два булевых флага в configmap.

Почему

  • Langfuse покрывает per-call observability на application-слое. Все production-сервисы кроме algo-hub уже инструментированы. Bifrost dashboard на :8080 был duplicate-источником.
  • SQLite leak устраняется полностью — файл не создаётся вообще, нет места для роста.
  • Zero effort, zero new infra. Один commit в values.yaml. Нет terraform правок, нет нового секрета, нет PVC, нет sidecar.
  • Не блокирует Phase 2. Если выяснится что gateway-audit нужен — OTLP-export в Langfuse или Postgres logs_store включается поверх этого решения, не вместо.

Phase 1b: bumped resources + GOMEMLIMIT env var — закрывает источник #2 (concurrent in-flight buffering). Двусоставная правка, обе части необходимы:

  1. resources.requests.memory: 512Mi, resources.limits.memory: 1Gi (с 256Mi/512Mi) — сохраняет стандартное 1:2 соотношение request:limit, соответствует actual production fan-out от algo-hub (30+ параллельных long-running LLM calls × payload буферы 100KB-MB+ × retry/fallback state).
  2. deployment.env.GOMEMLIMIT: "900MiB" — soft memory limit для Go runtime, ~88% от 1Gi container limit. Без этого env var Go runtime не знает про container limit и может accumulate heap до hard cgroup-cap до того как GC сработает → OOM при ещё-собираемой памяти. GOMEMLIMIT делает GC aggressive при подходе к 900MiB, оставляя 100MiB headroom для off-heap (goroutine stacks, syscall buffers, plugin chain state). Известный Go-in-container gotcha; upstream bifrost docs рекомендуют 90% от container limit2.

Defaults в Helm chart (256Mi/512Mi, без GOMEMLIMIT) изначально рассчитывались на light traffic. Выросший production load и Go runtime неосведомлённость о container limit делали старые defaults under-provisioned под двум осям одновременно.

Почему отдельная Phase 1b, не band-aid

  • Двумерный root cause требует двумерного фикса. Phase 1 убирает growth-over-time, Phase 1b — peak-under-load. Без 1b после Phase 1 OOM сохранялся (наблюдалось 2026-05-19 deploy → 20 минут → OOMKilled).
  • Sizing соответствует actual workload. 30+ concurrent LLM-calls по 5-30 сек × payload 100KB-MB × retry/fallback state не помещаются в 512Mi by basic arithmetic.
  • GOMEMLIMIT orthogonal к sizing. Даже с adequate memory limit, без runtime-side hint GC поведение остаётся реактивным к hard cap’у. Memory bump решает «сколько», GOMEMLIMIT — «когда GC будить».
  • Нет дополнительной инфры. Правки только в values.yaml, никаких новых компонентов.

Следствия

Algo-hub становится абсолютно слепым per-LLM-call. В algo-hub нет Langfuse SDK (нет import, нет env-vars). Раньше bifrost /api/logs хотя бы фиксировал что-через-него-прошло — теперь и этот канал ушёл. Виден только e2e на GCP LB + stdout (HTTP-метаданные без LLM-payload). Это резко повышает приоритет algo-hub Langfuse integration — отдельный open thread в llm-observability-stack.

GET /api/logs?limit=N больше не возвращает данные. Bifrost admin UI на :8080 без per-call dashboard. Endpoint /api/logs/<id> для post-hoc reconstruction app-side rejection (использовалось в BG-1429 verification — bifrost зафиксировал 6 compute-LLM-calls со status=success, app-side отверг 3 из них по schema mismatch) больше не работает.

Governance rejects не expose’ятся. VK denials, provider routing decisions, allowed-models violations — application получает HTTP 403, но следа на gateway-side нет. До Phase 1 они тоже не были в /api/logs (плагин отказывает в request middleware, до logging-хука) — изменение симметричное, но теперь и audit-recovery через SQLite CLI на pod’е невозможен.

SQLite leak устранён, OOM закрыт после Phase 1b. На staging после Phase 1 (da4ef041)3: idle memory держалась на 43-45Mi (~8% от 512Mi), но pod был OOMKilled через 20 минут под concurrent load от algo-hub. Phase 1b пакетом — memory bump (771e9046)4 поднял container limit до 1Gi для peak in-flight, плюс GOMEMLIMIT=900MiB (103c2b92)5 синхронизирует Go GC с container limit. Подтверждение — отсутствие PodOOMKilled алертов в течение observation periода после deploy обоих коммитов.

Открытые вопросы

Phase 2 — нужен ли gateway-level audit log? Решается после observation periода. Триггеры пересмотреть:

  • Если algo-hub Langfuse integration не покрывает все нужные сигналы — например, governance rejects до VK validation (gateway отбил request до того как application узнал что request состоялся).
  • Если придётся дебажить «application получил 403/500 от bifrost, что именно произошло на gateway» без доступа к app-side логам.
  • Если регуляторика (HIPAA audit log) потребует независимый от application-слоя audit trail каждого LLM-call.

Если триггер сработал — OTLP-export в Langfuse-OTel предпочтительный кандидат. Gateway-side данные приземляются в том же месте что app-side, единая корреляция, без новой БД. Postgres logs_store через inngest-analysis-db — fallback если OTLP-маршрут окажется неработоспособным.

Algo-hub Langfuse integration. Не зависит от этого решения, но усугубляется им. Без bifrost backup-канала algo-hub /api/analyze-stream — самый слепой endpoint в системе. См. open thread в llm-observability-stack.

Prometheus plugin для aggregated stats. Bifrost capability есть, не активирован (prometheus plugin not found в startup log). До Phase 1 это давало бы p50/p95/p99 latency + RPS + error rate per provider в pull-режиме поверх per-call SQLite. Теперь — единственный возможный канал для aggregated gateway stats, включение становится более ценным.

Связано

  • bifrost — vendor сам по себе; operational gotcha-секция ссылается сюда как на rationale текущего состояния
  • bifrost-memory-model — техническая карта memory pressure (worker pools / in-flight / GC / FD), куда смотреть при «опять что-то с памятью»
  • llm-observability-stack — visibility matrix отражает Phase 1: gateway-колонка обнуляется
  • langfuse — primary observability канал, покрывает 95% LLM data; potential Phase 2 sink через OTLP
  • llm-proxy-choice — выбор bifrost как gateway, observability tradeoffs скорректированы под Phase 1
  • bifrost-custom-plugin-loading — параллельный bifrost-кастомизационный decision

Источники

Сноски

  1. BG-1202 «Bifrost proxy OOM killed на staging — утечка памяти через SQLite logs», создан 2026-04-10, https://linear.app/realai-plus/issue/BG-1202.

  2. Bifrost Docker performance tuning docs, accessed 2026-05-19, https://docs.getbifrost.ai/deployment-guides/docker-tuning. Цит. по сессии ildar/4e9b6d1a.

  3. Commit da4ef041 (Phase 1 — disable internal logging) на bloodgpt-stage-applications, 2026-05-19, https://github.com/Realai-plus/argocd-applications/commit/da4ef041.

  4. Commit 771e9046 (Phase 1b — memory bump 256Mi/512Mi → 512Mi/1Gi) на bloodgpt-stage-applications, 2026-05-19, https://github.com/Realai-plus/argocd-applications/commit/771e9046.

  5. Commit 103c2b92 (Phase 1b — GOMEMLIMIT=900MiB, Go GC aligned с container limit) на bloodgpt-stage-applications, 2026-05-19, https://github.com/Realai-plus/argocd-applications/commit/103c2b92.