Updated FORENSIC_ARCHITECTURE_AUDIT.md to reflect the fix from commit919e8439: Status table: - 'Stuck-failed namespace без auto-recovery': ❌ ОТКРЫТ → ✅ ЗАКРЫТ (919e8439) - 'No Explicit State Machine': ❌ ОТКРЫТ → ⚠️ СМЯГЧЕНО (stuck-failed закрыт; явная state machine остаётся в backlog) Sections updated: - §1.4: полное описание что было (void-функции, мёртвый reconciler) и что сделано (error propagation chain, end-to-end flow retry) - Fragile Components: Stuck Failed Phase — вычеркнуто как закрытое - Risky Decisions table: No Explicit State Machine → частично закрыто - Lifecycle Management: добавлено что auto-recovery работает через RunReconciler - Operational Burden: убрано упоминание stuck-failed как активной проблемы - Maintainability/Production-Grade: обновлены под текущее состояние - §2 Stuck Failed Accumulation: ✅ ЗАКРЫТ - §3 P1 Reconcile-очередь: ✅ ЗАКРЫТ - §5 Sharded mutex вердикт: убрано упоминание stuck-failed
22 KiB
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 | ✅ ЗАКРЫТ | 919e8439 |
| AdoptExistingResources race при rolling update | ❌ ОТКРЫТ | — |
| No Explicit State Machine (implicit phase transitions) | ⚠️ СМЯГЧЕНО | 919e8439 |
| 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:
AdoptExistingResourcesvsnamespace_subscriber— активная проблема (§1.3). - Stuck Failed Phase:
Namespace в— закрыто:failedне восстанавливается без рестартаRunReconciler+ полная цепочка error propagation (§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 закрыт (919e8439); явная state machine в backlog |
| 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) реализованы. Auto-recovery из failed работает через
RunReconciler. Явная state machine в backlog. - 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 из failed работает.
- Production-Grade: Близко — informer lifecycle корректен, dispatch параллелен, cleanup симметричен, stuck-failed закрыт. Основной gap: 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 при старте
- Executor стартует.
AdoptExistingResourcesберётDefaultNSResolver().Snapshot()— только статические NS из env. - Параллельно:
RunManagedNamespaceWatcher→BootstrapAndDispatch()→registerNamespace()добавляет managed NS. AdoptExistingResourcesуже завершила loop — managed NS не попал в snapshot. Orphaned pods в tenant NS не приняты.CleanupOldExecutorObjectsсочтёт старые pod-ы orphaned → удалит → cold start для всех функций.
При rolling update: два executor-а параллельно патчат instanceID на одних pod-ах → race.
Статус: не исправлено. Требует либо задержки AdoptExistingResources до завершения первого BootstrapAndDispatch, либо включения managed NS в Snapshot() на момент adoption.
1.4 No Explicit State Machine — ✅ ЗАКРЫТ (коммит 919e8439)
Сценарий: stuck в failed без auto-recovery
Что было: EnsureNamespaceSA и registerNamespace были void-функциями — ошибки только логировались, до MarkPartFailed не доходили. Executor subscriber всегда возвращал nil → namespace никогда не попадал в NamespacePhaseFailed → RunReconciler для executor был мёртвым кодом.
Что сделано:
setupSAAndRoleBindings→ возвращаетerrorEnsureNamespaceSA→ возвращаетerror, пробрасываетregisterNamespace→ возвращаетerror(SA + executorTypes) сfmt.Errorfwrapping- Executor
AddFunc/ResyncFunc→ пробрасывают ошибку вместоreturn nil RunReconciler→ принимает*zap.Logger, логирует каждый retry и исход
End-to-end flow:
EnsureNamespaceSAfails (k8s 503) →registerNamespacereturns error- Executor AddFunc returns error →
dispatch()→MarkPartFailed("executor") deriveNamespacePhase→NamespacePhaseFailedRunReconcilertick (30s) находит namespace →DispatchResync→ retry- Если API восстановился:
MarkPartActive→NamespacePhaseActive→ логresync succeeded
Накопление при churn: ликвидировано — failed NS автоматически выходят из этой фазы при восстановлении API.
Ограничение: нет max-retries. Namespace, у которого SA создать принципиально невозможно (например, удалённый k8s namespace), будет ретраиться вечно. Приемлемо на текущем масштабе.
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 — ✅ ЗАКРЫТ (919e8439)
Error propagation исправлена во всей цепочке: setupSAAndRoleBindings → EnsureNamespaceSA → registerNamespace → executor subscriber. RunReconciler логирует retry и исход.
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, не mutex. Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.