Files
EvoBGP/.cursor/plans/технический_аудит_evobgp_60e04155.plan.md
Denozordec fad2bd3353
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 2m11s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m27s
feat(db): implement PostgreSQL monitoring and maintenance features
Added PostgreSQL monitoring and maintenance capabilities to the API, including new endpoints for instance-level metrics, maintenance operations, and job scheduling. Updated the HTTP API to support PostgreSQL monitoring routes and integrated a background scheduler for metrics collection. Enhanced the CLI with database commands for maintenance tasks. Updated documentation to reflect these changes.
2026-06-01 13:43:33 +07:00

22 KiB
Raw Permalink Blame History

name, overview, todos, isProject
name overview todos isProject
Технический аудит EvoBGP Полный технический аудит EvoBGP для production-сценария (10+ клиентов, нестабильная сеть). Архитектура — hybrid control plane; сильные стороны: stale fallback, CDN/RIPEstat resilience, Ed25519 bundles. Критичные риски: in-process jobs, DoH без retry, misconfiguration demo-seed, отсутствие HA API.
id content status
quick-ops-checklist Применить production-checklist (SEED_DEMO=0, BUNDLE_SEED_HEX, DB/JOB/CONCURRENCY tuning, TLS) pending
id content status
fix-doh-retry Добавить DoWithRetry для DoH в internal/pipeline/refresh.go pending
id content status
fix-job-meta-race Исправить чтение j.Meta в worker.go через Snapshot() или locked accessor pending
id content status
cdn-preview-resilience Перевести CDN preview на upstreamHTTPDo в routes_crud.go pending
id content status
partial-cdn-failure Partial CDN source failure: skip/degrade вместо fail всего модуля pending
id content status
graceful-shutdown Cancel/drain jobs при SIGTERM в cmd/evobgp-api и evobgp-all pending
id content status
ha-job-queue Roadmap: распределённая очередь jobs (PG claim или NATS) для HA API pending
false

Технический аудит EvoBGP

Executive summary

EvoBGP — hybrid control plane: один процесс evobgp-all (monolith) или reference Compose с разделёнными воркерами (docs/architecture.md). Data plane (BIRD + agent) отделён от control plane (API + PostgreSQL + jobs).

Сильные стороны для нестабильной сети:

Главные риски для 10+ клиентов:

  1. jobs.Registry — in-memory, только в процессе API (ARCH-04)
  2. DoH — без retry/breaker (критично при блокировках провайдеров)
  3. Один failed CDN source без stale cache валит весь модуль
  4. Production misconfiguration: Bearer dev, HTTP API, ephemeral bundle key
  5. Data race на Job.Meta и alias pointers в store.Memory

1. Архитектура

Стиль

flowchart TB
  subgraph hybrid [Hybrid deployment]
    All[evobgp_all monolith]
    Split[evobgp_api + workers]
  end
  subgraph cp [Control plane]
    API[HTTP API]
    Jobs[jobs.Registry in-process]
    PG[(PostgreSQL)]
  end
  subgraph dp [Data plane per speaker]
    Agent[evobgp_agent]
    BIRD[BIRD2]
    NodeCLI[evobgp_node]
  end
  All --> API
  Split --> API
  API --> Jobs
  API --> PG
  NodeCLI --> API
  Agent --> API
  Agent --> BIRD
Профиль Стиль Когда
microvps / evobgp-all Monolith 1 VPS, shared Registry
reference Compose Microservices-lite API + scheduler/ingest/render/deploy
Remote speakers Edge agents Panel→Node dispatch

Узкие места (bottlenecks)

# Bottleneck Где Impact
B1 In-process job queue internal/jobs/job.go:175-177 HA API невозможен без потери/дублирования jobs; scheduler без EVOBGP_CONTROL_PLANE_URL создаёт отдельный Registry — cmd/evobgp-scheduler/main.go:58-60
B2 Module refresh = sync upstream fan-out internal/pipeline/collect_parallel.go До EVOBGP_COLLECT_CONCURRENCY (8 default, max 32) параллельных HTTP; worst case ~45s × retries на источник
B3 Default job concurrency = 8 internal/jobs/job.go:264-268 При burst refresh 10+ tenants — очередь растёт, goroutine блокируются на sem
B4 PostgreSQL pool default internal/db/open.go:28-38 pgx default ~4 conns; при JOB_MAX=16 + HTTP — contention без EVOBGP_DB_MAX_CONNS=25
B5 Live endpoints fan-out internal/httpapi/peers_live.go N goroutines × N speakers, 12s timeout каждый
B6 Broker — заглушка internal/broker NATS URL логируется, очередь не распределена

Масштабируемость

  • Вертикальная: хорошо до ~10–20 tenants при evobgp-all + tuning (docs/production-checklist.md)
  • Горизонтальная API: не поддерживается — два evobgp-api = два независимых Registry; job_audit в PG — audit only, не очередь исполнения
  • Workers (ingest/render/deploy): координируются через общую БД, не через jobs — OK для prefetch/drift

Отказоустойчивость

Сценарий Поведение Оценка
CDN/RIPEstat недоступен Stale snapshot + circuit breaker Хорошо (если был prior snapshot)
DoH недоступен Fail модуля или stale domain snapshot Средне (нет HTTP retry)
API restart mid-job Job теряется из Registry; audit может быть inconsistent Плохо
PG недоступен API /v1/ready → 503 OK
Agent unreachable Deploy job succeed, drift в evobgp-deploy Частичный fail (by design)

Рекомендация: для 10+ клиентов — evobgp-all на каждом CP или один CP + tuning; HA API требует распределённой очереди (NATS/Redis + worker pool) — задокументировано как future work.


2. Анализ кода

Антипаттерны

ID Проблема Файл Критичность
A1 Concurrent map read/write — worker читает j.Meta без lock, handler пишет через mergeMeta/Snapshot worker.go:108,263,379, job.go:103-111 high
A2 Escape internal pointers из Memory store store/memory.go:416-475 high (tests/dev); low (prod PG)
A3 Fire-and-forget goroutine на каждый auth auth.go:79-81 medium
A4 Silent error swallow в prefetch internal/ingest/run.go, prefetch.go medium
A5 Bypass resilience layer — CDN preview прямой Do routes_crud.go:267 medium
A6 EVOBGP_DEV_INSECURE — dead code compose + server.go low (misleading ops)
A7 Unused Registry в ingest/render/deploy binaries cmd/evobgp-ingest/main.go low (resource waste)

Maintainability

Плюсы: чёткое разделение слоёв (ARCH-01..10), store.Backend, OpenAPI как контракт, engineering rules, table-driven tests в birdfmt/pipeline.

Минусы:

  • Дублирование retry-логики (httpclient vs nodedispatch inline loop)
  • Env-tuning разбросан (EVOBGP_* в 15+ местах без central config struct для pipeline)
  • Job comment «персистенция в БД пока не подключена» устарел — hooks есть в bootstrap.go:67-92

Потенциальные баги и race conditions

  1. j.Meta data race — -race на TestParallelModuleRefresh_* + concurrent GET /v1/jobs/{id} polling
  2. Memory store alias — deploy.Run читает LastAppliedRevisionID пока worker пишет
  3. peerLiveCache возвращает slice без копии — peers_live.go:82-84
  4. TOCTOU idempotency — terminal job удаляется из byIdempo, повторный POST создаст новый job (by design, но клиент должен знать)

Error handling

Хорошо:

  • Префиксы ошибок (httpclient:, birdfmt:)
  • HTTP 5xx через writeProblem, без raw err.Error() (ERR-01)
  • context.Context в pipeline workers

Пробелы:

  • runRollback без workContext — не отменяется — worker.go:500+
  • Prefetch/ingest: ошибки не логируются
  • mergeBirdPostApplyMeta — context.Background() 8s, игнорирует job cancel

3. Производительность

Блокирующие операции

Участок Блокировка Риск
POST .../cdn-sources/preview Sync CDN fetch до 45s в HTTP handler UI timeout, worker starvation
GET /v1/peers/live N × agent HTTP, wg.Wait Slow при многих speakers
Module refresh job Sequential: ingest → render revision → optional deploy Long job chain
bird -p / birdc configure Subprocess в deploy Disk I/O на ноде

Неэффективные алгоритмы / лишние запросы

  • Tenant refresh: aggregateTenantPrefixRowsAll — parallel по модулям, но каждый модуль может refetch все CDN/ASN/DoH — aggregate.go:28+. Snapshot skip есть через module_hash — проверять hit rate в meta.
  • ASN resolve: PolitePause() 150ms между AS — asnresolve/ripestat.go — при 50 AS = +7.5s minimum.
  • GetModulePrefixSnapshot вызывается многократно в одном refresh (cdn_snapshot, collect_parallel) — potential duplicate DB reads.
  • Auth TouchAPIKeyLastUsed: UPDATE на каждый request (async) — load на PG при high RPS.

Кэширование

Кэш TTL Gap
ASN prefix cache 1800s (EVOBGP_ASN_CACHE_TTL_SEC) OK
CDN ETag in DB Until 304/change OK
Module prefix snapshot Content-hash based skip OK
peerLiveCache In-memory, per-process Не shared между API replicas; нет defensive copy
Circuit breaker state Per-process Не shared

Конкретные улучшения

// 1. CDN preview — использовать upstreamHTTPDo вместо прямого Do
resp, err := pipeline.UpstreamHTTPDo(r.Context(), s.cdnHTTP, req) // extract upstreamHTTPDo

// 2. Job.Meta — читать под lock или через Snapshot()
st := j.Snapshot()
mid, _ := st["meta"].(map[string]any)["module_id"].(string)

// 3. Memory store — возвращать копии (как Postgres)
modCopy := *mod
return &modCopy, nil

4. Сетевое взаимодействие (критично)

Текущее состояние

flowchart LR
  subgraph resilient [Resilient path]
    CDN[CDN fetch]
    RIPE[RIPEstat]
    CDN --> Breaker[Circuit breaker]
    RIPE --> Breaker
    Breaker --> Retry[DoWithRetry 3x linear 2s]
  end
  subgraph fragile [Fragile path]
    DoH[DoH resolve]
    Preview[CDN preview API]
    AgentHealth[Agent health/bird]
    DoH --> SingleDo[Single hc.Do]
    Preview --> SingleDo
    AgentHealth --> SingleDo
  end
  subgraph fallback [App-level fallback]
    Stale[Stale snapshot]
    SysDNS[System DNS]
    DoH --> SysDNS
    CDN --> Stale
    RIPE --> Stale
  end
Upstream Timeout Retry Breaker Stale fallback
CDN ingest 45s 3× linear per-host yes
RIPEstat 45s 3× per-host yes + cache
DoH 10s/profile no no domain snapshot
CDN preview 45s no no N/A
Scheduler→API 45s 3× no N/A
Node dispatch 30s inline 3× no N/A

Пробелы для блокировок провайдеров

  1. DoH без retry — transient timeout = fail; failover между profiles есть, но каждый profile — single shot
  2. 429/408 не ретраятся — только >= 500
  3. Нет jitter — thundering herd при mass tenant refresh
  4. DNS rebinding TOCTOU — SSRF check до fetch, HTTP dial без pinned IP — cdn_url.go:75-115
  5. Circuit breaker без half-open — после 30s cooldown сразу full traffic — circuit.go:29-33
  6. Breaker per-process — ingest container ≠ API container

Рекомендации для нестабильной сети

# Изменение Effort Effect
N1 DoH через DoWithRetry + optional breaker Low High для DOMAINS modules
N2 Retry 429/503 с Retry-After + exponential backoff + jitter Medium High при rate limits
N3 Partial CDN failure — continue с stale per-source, не fail whole module Medium High
N4 Multiple DoH profiles + failover policy (already exists) — документировать ops playbook Low High (config, not code)
N5 Pinned dialer / custom Transport.DialContext после SSRF resolve Medium Medium (SSRF hardening)
N6 Proxy support (HTTP_PROXY / EVOBGP_HTTP_PROXY) для CDN/DoH Medium High в censored networks
N7 Unify CDN preview на upstreamHTTPDo Low Medium

5. Устойчивость и надёжность

Graceful degradation

Работает:

Не работает / частично:

Сценарии отказов

Событие Что произойдёт
Потеря CP↔PG Ready=false; running jobs fail; no new jobs persist audit reliably
Потеря CP↔CDN Stale prefixes если были; иначе job fail; breaker opens 30s
Потеря CP↔agent Deploy meta dispatch_failed; BIRD на старой ревизии; drift logs
RIPEstat rate limit 429 → no retry → stale or fail
Рост нагрузки Job queue; goroutine pile-up; PG pool exhaustion; /metrics shows queue depth
API restart In-flight jobs lost; clients poll 404 or stale terminal state

6. Безопасность

ID Finding Severity Fix
S1 Bearer dev → operator при demo-seed high (misconfig) EVOBGP_SEED_DEMO=0 — auth.go:66-92
S2 API plain HTTP high (ops) TLS на edge (Traefik/nginx)
S3 Ephemeral bundle key без EVOBGP_BUNDLE_SEED_HEX high (ops) Stable seed + pubkey на нодах
S4 Compose defaults: weak PG password, sslmode=disable high (ops) Secrets manager, sslmode=require
S5 /metrics без auth medium Network policy / mTLS
S6 No rate limiting on auth medium Middleware limiter (e.g. per-IP)
S7 CDN SSRF DNS rebinding medium Pinned dialer after resolve
S8 EVOBGP_CDN_ALLOW_PRIVATE=1 medium Never in prod
S9 EVOBGP_NODE_DISPATCH_INSECURE_TLS=1 medium Valid TLS to agent
S10 Plaintext EVOBGP_API_KEYS in env medium DB keys via API
S11 editor can cancel jobs low Restrict to operator
S12 agent_secret == compare low subtle.ConstantTimeCompare

SQL injection: не обнаружено — параметризованные запросы в repository/.

Bundle crypto: Ed25519 корректно; path traversal blocked в tar extract.


7. Конкретные рекомендации (prioritized backlog)

High

# Описание Как исправить
H1 DoH без retry Обернуть hc.Do в DoWithRetry(ctx, hc, req, 3) в refresh.go:288,360
H2 Data race Job.Meta Читать через Snapshot() или добавить MetaLocked() accessor
H3 CDN source partial failure В collectCDNPrefixRows: при err без stale — log warning + skip source вместо return nil, r.err (config flag EVOBGP_CDN_PARTIAL_OK=1)
H4 Production checklist enforcement CI/deploy validation: reject SEED_DEMO=1, require BUNDLE_SEED_HEX
H5 Job queue HA roadmap Persist queued jobs in PG + worker claim (SELECT FOR UPDATE SKIP LOCKED) или NATS — ARCH-04

Medium

# Описание Как исправить
M1 CDN preview bypass routes_crud.go:267 → upstreamHTTPDo
M2 Retry 429/503 Extend DoWithRetry status check + parse Retry-After
M3 Graceful shutdown On SIGTERM: Registry.RequestCancelAll() + wait workers with timeout
M4 Auth goroutine storm Worker pool или sync touch with debounce
M5 HTTP proxy support Custom Transport reading EVOBGP_HTTP_PROXY
M6 Memory store copies Defensive copy in Get/List (dev/test safety)
M7 Rate limiting golang.org/x/time/rate on auth middleware

Low

# Описание Как исправить
L1 Jitter in backoff wait + rand.Intn(wait/2) in DoWithRetry
L2 Half-open breaker Single probe request after cooldown
L3 Remove dead EVOBGP_DEV_INSECURE from compose Docs + compose cleanup
L4 Prefetch error logging log.Printf or structured log in prefetch
L5 peerLiveCache defensive copy append([]T(nil), views...) on store

8. Quick wins (максимальный эффект / минимум усилий)

  1. Ops (0 code): docs/production-checklist.md — SEED_DEMO=0, BUNDLE_SEED_HEX, DB_MAX_CONNS=25, JOB_MAX=16, COLLECT_CONCURRENCY=16, TLS edge, restrict metrics
  2. DoH retry — 5–10 строк в refresh.go, reuse existing DoWithRetry
  3. CDN preview → upstreamHTTPDo — 1 line change in handler
  4. Job.Meta read fix — replace 4 reads in worker.go with Snapshot() parsing
  5. Log prefetch failures — visibility без изменения behavior
  6. Document DoH failover playbook — multiple profiles (Cloudflare, Google, Quad9) + failover policy for censored regions
  7. Run go test -race ./internal/jobs/... in CI — catch Meta race
  8. Prefer evobgp-all over split reference for <20 tenants — eliminates Registry split bug

Диаграмма: refresh под сетевым stress

sequenceDiagram
  participant Op as Operator
  participant API as evobgp_api
  participant Job as module_refresh
  participant CDN as CDN_upstream
  participant PG as PostgreSQL

  Op->>API: POST /modules/id/refresh
  API->>Job: Enqueue
  Job->>CDN: GET with ETag
  alt CDN timeout or 5xx
    CDN-->>Job: error after 3 retries
    Job->>PG: load prior snapshot
    alt stale exists
      Job->>PG: CreateRenderRevision stale
      Job-->>API: succeeded degraded
    else no stale
      Job-->>API: failed
    end
  else CDN 200
    CDN-->>Job: new prefixes
    Job->>PG: CreateRenderRevision
  end

Итоговая оценка зрелости

Область Оценка Комментарий
Архитектура 7/10 Чистые слои; HA/API scaling — слабое место
Сеть/resilience 6/10 CDN/ASN хорошо; DoH/preview — пробелы
Concurrency 6/10 Registry продуман; Meta race, shutdown
Performance 7/10 Parallel collect, caching; tuning needed at scale
Security 6/10 Crypto OK; ops/config risks dominate
Maintainability 8/10 Docs, rules, OpenAPI, tests

Вердикт: проект готов для 10+ клиентов в single-CP deployment (evobgp-all + PostgreSQL + production checklist) при условии ops discipline. Для multi-CP HA и агрессивных сетевых блокировок — приоритет: DoH retry, partial CDN failure, distributed job queue, HTTP proxy.