# 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**: ~1500–2000 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 10–30 сек, 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. Анализ при 50–100 tenant с churn 10 ns/час ### Informer Count (steady-state) При 100 активных tenant: - **Poolmgr**: 2 factory × 100 NS × ~3–5 goroutine = **600–1000 goroutine** - **NewDeploy**: аналогично ~600–1000 goroutine - **Router**: 1 factory × 100 NS × ~2 goroutine = **200 goroutine** - **Buildermgr**: ~200 goroutine Итого: **~1600–2400 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.