Updated FORENSIC_ARCHITECTURE_AUDIT.md:
- Status table: AdoptExistingResources race ❌→✅ ЗАКРЫТ (2a7d6101)
- §1.3: rewritten as ЗАКРЫТ with end-to-end fix description
- Fragile Components: Manual Adoption → закрыто
- Risky Decisions table: Manual Adoption ❌→✅
- Production-Grade summary: AdoptExistingResources race закрыт
- §2 Stuck Failed Accumulation: ✅ ЗАКРЫТ
- §2 AdoptExistingResources Race: ✅ ЗАКРЫТ
- §3 P2: ✅ ЗАКРЫТ
- §5 Sharded mutex verdict: updated (race закрыт)
23 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 | ✅ ЗАКРЫТ | 2a7d6101 |
| 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 ресурсов. Закрыто:
PreRegisterManagedNamespacesобеспечивает полный NS snapshot до adopt/cleanup (§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— активная проблемаPreRegisterManagedNamespacesперед adopt/cleanup (2a7d6101). - 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 (2a7d6101) |
| 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 закрыт, 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) — ✅ ЗАКРЫТ (коммит 2a7d6101)
Сценарий: гонка adoption vs watcher при старте
Что было: AdoptExistingResources и CleanupOldExecutorObjects запускались до StartNSWatcher. DefaultNSResolver().Snapshot() возвращал только статические NS из FISSION_RESOURCE_NAMESPACES → managed NS не покрывались:
- Pods от предыдущего executor в managed NS не adoptировались (сохраняли старый
instanceID) → poolmgr создавал новые pool pods → cold start. - Старые RS/deployments в managed NS не чистились → накапливались.
Что сделано: multitenant.PreRegisterManagedNamespaces(ctx, logger, kubernetesClient) — синхронный Namespaces.List с label fission.io/managed=true вызывается в executor.go до goroutines adopt+cleanup. Добавляет все managed NS в DefaultNSResolver. Идемпотентен с последующим AddFunc из watcher. Не ломает при ошибке API (warn + proceed).
End-to-end после фикса:
PreRegisterManagedNamespaces→DefaultNSResolverсодержит static + managed NSAdoptExistingResources→ патчит pods в managed NS с новымinstanceIDCleanupOldExecutorObjects/GetReaperNamespace()→ видит managed NS → чистит стale объектыStartNSWatcher→AddFuncсрабатывает для тех же NS —DefaultNSResolver().AddNamespace()idempotent,AddNamespaceexecutor types dedup-protected
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 — ✅ ЗАКРЫТ
Failed NS автоматически ретраятся RunReconciler каждые 30с и выходят из failed при восстановлении API. Накопления больше не происходит.
AdoptExistingResources Race — ✅ ЗАКРЫТ
PreRegisterManagedNamespaces синхронно добавляет managed NS в DefaultNSResolver до adopt/cleanup. Старые pods adoptируются, stale объекты чистятся. Подробно — §1.3.
3. Рекомендации (приоритизированные)
P1 — Reconcile-очередь для failed NS — ✅ ЗАКРЫТ (919e8439)
Error propagation исправлена во всей цепочке: setupSAAndRoleBindings → EnsureNamespaceSA → registerNamespace → executor subscriber. RunReconciler логирует retry и исход.
P2 — AdoptExistingResources после BootstrapAndDispatch — ✅ ЗАКРЫТ (2a7d6101)
PreRegisterManagedNamespaces вызывается синхронно до adopt/cleanup. Делает один Namespaces.List(label=fission.io/managed=true) → добавляет все managed NS в DefaultNSResolver. После этого adopt и cleanup покрывают полный tenant NS set.
Влияние: устранены orphaned pods при холодном старте и resource leak (stale RS/deployments).
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 — закрыт (2a7d6101). Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.