Files
fission-src/doc/FORENSIC_ARCHITECTURE_AUDIT.md
T

270 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Forensic Architecture Audit: Fission Fork (multitenant, May 2026)
> Актуальная редакция. Легаси-версия: `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md`
> Обновлено: 2026-05-18 после реализации namespace lifecycle hardening.
---
## Статус исправлений
| Риск | Статус | Коммит |
|------|--------|--------|
| Informer goroutine/FD leak при TrackOnly removal | ✅ ЗАКРЫТ | `4eedf95f` |
| Router stale routes при повторном добавлении NS | ✅ ЗАКРЫТ | `4eedf95f` |
| Executor dedup dirty state при re-add NS | ✅ ЗАКРЫТ | `4eedf95f` |
| Синхронный subscriber dispatch (onboarding latency) | ✅ ЗАКРЫТ | предыдущая сессия |
| `DefaultNSResolver` только append (нет RemoveNamespace) | ✅ ЗАКРЫТ | предыдущая сессия |
| Stuck-failed namespace без auto-recovery | ❌ ОТКРЫТ | — |
| AdoptExistingResources race при rolling update | ❌ ОТКРЫТ | — |
| No explicit state machine (implicit phase transitions) | ❌ ОТКРЫТ | — |
| Sharded mutex (bottleneck при >500 concurrent tenant) | ⏳ BACKLOG | не актуально при текущей нагрузке |
---
## Architectural Decisions (реально принятые)
- **Dynamic Namespace Discovery**: Механизм динамического обнаружения и подключения tenant-namespace через label `fission.io/managed=true` (`pkg/utils/namespace_manager.go`, `pkg/executor/multitenant/ns_watcher.go`).
- **Namespace Lifecycle Management**: Жизненный цикл namespace централизован через интерфейс `NamespaceManager` с подписчиками (executor, router, buildermgr).
- **Decoupled Registration**: Каждый компонент подписывается как `NamespaceSubscriber` и реализует свою логику инициализации/чистки ресурсов.
- **Backward Compatibility**: Поддержка статического списка через env (`FISSION_RESOURCE_NAMESPACES`) с динамическим расширением.
- **No-Restart Onboarding**: Добавление tenant не требует рестарта pod-ов.
- **RBAC/SA Provisioning**: Автоматическое создание SA и RBAC для новых namespace (`EnsureNamespaceSA`).
- **Informer Factories Per Namespace**: Отдельная informer factory для каждого NS, с per-NS context cancellation.
- **Explicit Namespace Removal Strategy**: `DispatchRemove` — при удалении NS вызываются `RemoveFunc` у всех подписчиков, останавливаются informer-ы через `context.CancelFunc`.
- **Parallel Subscriber Dispatch**: Подписчики вызываются параллельно через `errgroup` — onboarding не блокируется медленным SA provisioning.
---
## Core Complexity Centers
- **NamespaceManager & Watcher**: Центр всей динамики — координация событий, фаз, подписчиков.
- **ExecutorType Subsystems**: Poolmgr, NewDeploy, Container — каждый хранит собственный per-NS кэш, lister-ы, логику adoption и reaping.
- **Informer Lifecycle**: Динамическое создание/остановка informer-ов через per-NS `context.CancelFunc`. Чистка `envLister[ns]`/`deplLister[ns]`/`triggerInformer[ns]` при `RemoveNamespace`.
- **FunctionServiceCache**: Кэширование и lifecycle function pod-ов, синхронизация с событиями из разных источников. **Не очищается при RemoveNamespace**`idleObjectReaper` убирает устаревшие записи через `IsValid()` check.
---
## Hidden Coupling & Accidental Complexity
- **Implicit Contract**: Все компоненты обязаны реализовывать `NamespaceSubscriber` симметрично (и `AddFunc`, и `RemoveFunc`). Нарушение → silent drift.
- **Global vs Local State**: Глобальный `DefaultNSResolver` + локальные lister-ы в каждом executor type. `RemoveNamespace` в NSResolver и в каждом executor type должны быть вызваны согласованно.
- **Deduplication Responsibility**: `AddNamespace` дедупликация — через `DefaultNSResolver().AddNamespace()` возвращающий `bool`, и через проверку локального lister-а (`envLister[ns] != nil`). После `RemoveNamespace` оба guard сбрасываются → re-add корректно создаёт новые informer-ы.
- **Event Handler Ordering**: Порядок подписчиков в `Subscribe` влияет на side-effects, но `errgroup` делает их параллельными — ordering больше не определяет latency, но всё ещё влияет на приоритет ошибок.
- **RBAC Drift**: Provisioning SA/RBAC в `registerNamespace`, cleanup — в `deregisterNamespace`. При сбое cleanup — dangling SA/ClusterRoleBinding.
---
## Workaround-Driven Decisions
- ~~**Track-Only Removal**~~ → **ЗАМЕНЕНО** на `DispatchRemove` — cleanup вызывается всегда.
- **Manual Adoption**: При старте executor-ы делают adopt orphaned ресурсов — workaround для несовершенного lifecycle. Активная проблема (см. §1.3).
- **Explicit Reaper Loops**: `idleObjectReaper` чистит `FunctionServiceCache` вместо event-driven подхода. Приемлемо: `IsValid()` check достаточен при корректной работе per-NS informer-ов.
---
## Fragile Operational Components
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением SA/ролей при сбое в `deregisterNamespace`.
- **Cache Invalidation**: `FunctionServiceCache` не очищается при `RemoveNamespace` — расчёт на `idleObjectReaper`. При высоком churn rate может накапливать stale записи быстрее, чем reaper убирает.
- **Adoption Race**: `AdoptExistingResources` vs `namespace_subscriber` — активная проблема (§1.3).
- **Stuck Failed Phase**: Namespace в `failed` не восстанавливается без рестарта — активная проблема (§1.4).
---
## Poor Scalability Risks
- **Informer Explosion**: ~15002000 goroutine при 100 tenant (см. §2). **Частично смягчено**: goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления при churn. Но в steady-state 100 NS — линейный рост горутин остаётся.
- ~~**Synchronous Dispatch**~~ → **ИСПРАВЛЕНО**: параллельный dispatch через `errgroup`.
- **Centralized Locking**: Глобальный mutex на NamespaceManager. При текущей нагрузке (<50 ns) — не узкое место. При >500 concurrent tenant — backlog (sharded mutex, §5).
- **Thundering Herd на resync**: 100 NS × 5 informer-типов × LIST каждые 30 мин — 500 concurrent LIST к API.
---
## Future Maintenance Problems
- **Hidden State Machines**: Фазы namespace реализованы неявно — сложно дебажить stuck state. Нет формализованной машины состояний с explicit transitions.
- **Implicit Error Handling**: Ошибки в `deregisterNamespace` логируются, но NS может остаться в некорректном состоянии. Нет `NamespaceCondition` на k8s-объекте.
- **Contract Drift**: Изменение интерфейса `NamespaceSubscriber` (например, добавление `ResyncFunc`) требует синхронного обновления всех компонентов.
- **FunctionServiceCache без per-NS cleanup**: если `idleObjectReaper` будет отключён/изменён — stale cache может накапливаться.
---
## Risky / Hard-to-Maintain Decisions
| Решение | Статус | Примечание |
|---------|--------|------------|
| Informer Lifecycle Management | ✅ Hardened | per-NS context cancel + RemoveNamespace во всех компонентах |
| Centralized Mutex | ⚠️ Приемлемо | sharding в backlog, не актуально до >500 NS |
| Manual Adoption | ❌ Активная проблема | race при rolling update |
| No Explicit State Machine | ❌ Активная проблема | stuck-failed без retry |
| Eventual Consistency | ⚠️ Смягчено | параллельный dispatch уменьшает окно, но не устраняет |
---
## Multi-Tenancy, Isolation, Orchestration, Lifecycle, State, Reconciliation
- **Multi-Tenancy**: Label-based discovery, каждый tenant — отдельный namespace, изоляция на уровне k8s.
- **Isolation Model**: Namespace-level isolation, per-NS SA/RBAC, per-NS informer factory.
- **Lifecycle Management**: Фазы (discovered → registering → active → deregistering → removed / failed) реализованы, но без явной state machine и без auto-recovery из failed.
- **State Handling**: Глобальный `DefaultNSResolver` + локальные lister-ы. После `RemoveNamespace` — оба синхронизованы. После re-add — оба корректно инициализируются заново.
- **Reconciliation Logic**: Каждый компонент через subscribe. Отсутствует reconcile-очередь для failed state.
- **Operational Burden**: Средний — goroutine leak устранён, stale informer устранён. Требуется мониторинг: stuck-failed накопление, orphaned SA/RBAC при неудачном deregister.
---
## Engineering Maturity
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая в синхронизации и lifecycle. Снижена за счёт формализации `RemoveNamespace` контракта.
- **Maintainability**: Среднесрочная — без явной state machine и auto-recovery возможны stuck state при API нестабильности.
- **Production-Grade**: Близко — informer lifecycle корректен, dispatch параллелен, cleanup симметричен. Основной gap: stuck-failed и AdoptExistingResources race.
---
# Deep Risk Analysis (актуальная, May 2026)
---
## 1. Сценарии отказа
### 1.1 Informer Lifecycle Management — ✅ ЗАКРЫТ
**Что было:** relabel-цикл NS создавал phantom-состояние: informer-ы не останавливались при track-only removal, `DefaultNSResolver` не очищал запись → re-add возвращал `false` → новые informer-ы не создавались.
**Что сделано (коммит `4eedf95f`):**
- `RemoveNamespace(ns)` добавлен в интерфейс `ExecutorType` и реализован в poolmgr, newdeploy, container.
- В каждом executor type: per-NS context cancel (`nsCancels map[string]context.CancelFunc`). `AddNamespace` создаёт `nsCtx, nsCancel := context.WithCancel(ctx)`, передаёт `nsCtx` в `factory.Start()`. `RemoveNamespace` вызывает `nsCancel()` и удаляет lister-ы из карт.
- Router: `HTTPTriggerSet.RemoveNamespace()` отменяет per-NS ctx, удаляет `triggerInformer[ns]`/`funcInformer[ns]` под `informerMu.Lock()`, вызывает `syncTriggers()`.
- Buildermgr: `envWatcher.RemoveNamespace()` и `pkgWatcher.RemoveNamespace()` — аналогично.
- `DefaultNSResolver.RemoveNamespace(ns)` удаляет NS из глобального map → re-add корректно проходит guard.
- Стратегия `DispatchRemove` во всех 3 компонентах → `RemoveFunc` вызывается при удалении NS.
**Текущий статус:** informer goroutine/FD корректно останавливаются; re-add NS создаёт чистые informer-ы; router не видит stale routes.
---
### 1.2 Centralized Mutex — ⚠️ ПРИЕМЛЕМО
**Сценарий:** высокая churn + concurrent Snapshot.
`dispatch()` отпускает mutex перед вызовом каждого subscriber, берёт снова для следующего. При батч-онбординге 10+ NS параллельно: конкуренция за mutex, latency spike на `Snapshot()` в `idleObjectReaper`.
**Смягчено:** `dispatch()` теперь параллельный (errgroup) — подписчики не вызываются последовательно, время блокировки mutex между подписчиками устранено. `Snapshot()` конкурирует только с `Upsert` — при текущей нагрузке (<50 NS) практически нет.
**Остаётся:** при >500 concurrent tenant с >1 onboarding/sec — sharded mutex даст выигрыш. В backlog.
---
### 1.3 Manual Adoption (AdoptExistingResources) — ❌ АКТИВНАЯ ПРОБЛЕМА
**Сценарий: гонка adoption vs watcher при старте**
1. Executor стартует. `AdoptExistingResources` берёт `DefaultNSResolver().Snapshot()` — только статические NS из env.
2. Параллельно: `RunManagedNamespaceWatcher``BootstrapAndDispatch()``registerNamespace()` добавляет managed NS.
3. `AdoptExistingResources` уже завершила loop — managed NS не попал в snapshot. Orphaned pods в tenant NS не приняты.
4. `CleanupOldExecutorObjects` сочтёт старые pod-ы orphaned → удалит → cold start для всех функций.
**При rolling update:** два executor-а параллельно патчат `instanceID` на одних pod-ах → race.
**Статус:** не исправлено. Требует либо задержки `AdoptExistingResources` до завершения первого `BootstrapAndDispatch`, либо включения managed NS в `Snapshot()` на момент adoption.
---
### 1.4 No Explicit State Machine — ❌ АКТИВНАЯ ПРОБЛЕМА
**Сценарий: stuck в `failed` без auto-recovery**
1. `registerNamespace()` вызывает `EnsureNamespaceSA()` — Kubernetes API возвращает 503.
2. Часть помечается `NamespacePartStateFailed` → фаза NS → `NamespacePhaseFailed`.
3. **Нет reconcile-цикла**: фаза остаётся `failed` до рестарта процесса.
4. Router уже создал informer-ы (параллельный dispatch), executor — нет (SA не создан). Dirty state: router видит NS активным, executor — нет. Вызовы → 503.
**Накопление при churn:** 10 ns/час × 1% API error rate = ~2.4 failed NS/сутки. За 30 дней = ~72 "мёртвых" записи. `Snapshot()` возвращает их в `idleObjectReaper` → лишние LIST запросы к k8s.
**Предложение (не реализовано):** reconcile-очередь в `inMemoryNamespaceManager` — см. FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §3 для кода.
**Статус:** не исправлено.
---
### 1.5 Eventual Consistency — ⚠️ СМЯГЧЕНО
**Сценарий:** HTTPTrigger создан в окне до готовности informer.
**Было:** последовательный dispatch → если executor делал SA provisioning 1030 сек, router не начинал `WaitForCacheSync`. Trigger, созданный в этом окне, пропускался до следующего resync (30 мин).
**Смягчено:** параллельный dispatch через errgroup → router и executor стартуют `AddNamespace` одновременно. Окно уязвимости = время `WaitForCacheSync` в router (~2–5 сек), а не время SA provisioning (~30 сек).
**Остаётся:** trigger, созданный за 2–5 сек до `WaitForCacheSync` в router → нормально обрабатывается через `AddFunc` после sync. Фактически проблема устранена для практических сценариев.
---
## 2. Анализ при 50100 tenant с churn 10 ns/час
### Informer Count (steady-state)
При 100 активных tenant:
- **Poolmgr**: 2 factory × 100 NS × ~35 goroutine = **6001000 goroutine**
- **NewDeploy**: аналогично ~6001000 goroutine
- **Router**: 1 factory × 100 NS × ~2 goroutine = **200 goroutine**
- **Buildermgr**: ~200 goroutine
Итого: **~16002400 goroutine** от informer-ов. **Линейный рост с числом NS — неизбежен при текущей архитектуре.**
**Что изменилось после hardening:** при churn goroutine-ы корректно останавливаются при `RemoveNamespace` — нет накопления мёртвых goroutine. Steady-state = ~O(active_NS), а не O(total_NS_ever_seen).
### Thundering Herd на resync
100 NS × 5 informer-типов × LIST каждые 30 мин = **500 concurrent LIST** к Kubernetes API. Не изменилось, не исправлено.
### Stuck Failed Accumulation
10 ns/час × 1% API error rate = ~2.4 failed NS/сутки. ~72 за 30 дней. Не исправлено (§1.4).
### AdoptExistingResources Race
При rolling update — race на `instanceID` патч. Не исправлено (§1.3).
---
## 3. Рекомендации (приоритизированные)
### P1 — Reconcile-очередь для failed NS
Минимальное изменение без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §3`. Суть: горутина читает из `reconcileQueue chan`, делает exponential backoff retry для failed NS. Max 5 попыток.
**Влияние:** устраняет stuck-failed накопление, dirty state между router и executor.
### P2 — AdoptExistingResources после BootstrapAndDispatch
Либо: подождать первый `BootstrapAndDispatch()` через канал-сигнал, затем запускать `AdoptExistingResources`. Либо: в `AdoptExistingResources` использовать `NamespaceManager.Snapshot()` вместо `DefaultNSResolver().Snapshot()` (managed NS уже добавлены к этому моменту через BootstrapAndDispatch).
**Влияние:** устраняет orphaned pods при холодном старте и rolling update.
### P3 — NamespaceCondition на k8s Namespace объекте
Пометить Namespace через `kubectl annotate` или через status subresource при failed phase → оператор видит причину без чтения логов.
### Backlog — Sharded mutex
Актуально при >500 concurrent tenant с >1 onboarding/sec. Технически feasible без breaking interface change (см. `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`).
---
## 4. FunctionServiceCache — текущий инвариант
`FunctionServiceCache` (`fsCache` в gpm и newdeploy) **не очищается** при `RemoveNamespace`. Это осознанное решение:
- `idleObjectReaper` периодически вызывает `fsCache.ListOldForPool()` → для каждой записи проверяет `podLister[ns]` → если NS удалён, `podLister[ns]` == nil → pod не найден → запись считается expired → `fsCache.DeleteEntry()`.
- Временной лаг = интервал reaper-а (по умолчанию ~1 мин). При высоком churn возможно накопление stale записей, но они не вызывают функциональных ошибок (только небольшой overhead на reaper iteration).
**Когда станет проблемой:** при отключении/изменении reaper-а или при >10 000 stale записей (O(n) iteration).
---
## 5. Sharded Mutex — вердикт
**Технически реализуемо** без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`.
**Вердикт:** не оправдано при текущей нагрузке. Реальные bottleneck-и — AdoptExistingResources race и stuck-failed, не mutex. Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.