Единый сводный документ, описывающий полную архитектуру мультитенантного Fission. Заменяет необходимость читать 50+ пошаговых thinking-файлов. Содержит: - Причина и концепция решения - Архитектурная карта изменений (ASCII diagram) - Таблица ключевых файлов с ролями - Инженерные решения: Snapshot API, NamespaceManager event bus, EnsureNamespaceSA, buildermgr dedup bug, router nil guard - RBAC: что и почему (включая нетривиальные events:create и LSAR) - Backward compatibility guarantees - Описание test scenario (Layer 1, PASS=5) - Порядок деплоя нового форка - Направления дальнейшей работы
11 KiB
Мультитенантный Fission: сводная архитектура и инженерная логика
Дата: 2026-05-15
Контекст: форк Fission v1.22.0, веткаfeature/multitenant
Статус: реализовано, тесты зелёные
Зачем это было нужно
Стандартный Fission требует, чтобы все namespace-ы, в которых живут функции,
были перечислены в переменной окружения FISSION_RESOURCE_NAMESPACES до старта
процессов. Добавление нового namespace = rolling restart всех компонентов (executor,
router, buildermgr). На сотнях тенантов — постоянный restart loop, каскадные сбои.
Наша задача: добавить новый tenant (namespace) без какого-либо рестарта.
Концепция решения
Единственный public contract для внешних систем — label на Namespace:
apiVersion: v1
kind: Namespace
metadata:
name: tenant-abc123
labels:
fission.io/managed: "true"
Никакого другого coupling с Fission internals не требуется.
После появления namespace с этим label Fission автоматически:
- Регистрирует namespace во всех компонентах (executor, router, buildermgr)
- Создаёт SA
fission-fetcherи необходимый RBAC в namespace - Подключает informer factory для CRD (Functions, Environments, HTTPTriggers и т.д.)
- Тенант может деплоить функции без задержки
Архитектурная карта изменений
Kubernetes Namespace API
│
│ watch: label fission.io/managed=true
▼
utils.RunManagedNamespaceWatcher(...)
│
│ (shared utility, один и тот же вызов из трёх компонентов)
▼
utils.NamespaceManager (interface)
│
├─ Bootstrap(envNamespaces) ← уже существующие NS при старте
├─ DispatchAdd(ns) ← новый NS от watcher
└─ DispatchRemove(ns) ← NS удалён (track-only)
│
▼
NamespaceSubscriber.OnNamespaceAdd(...)
│
┌───────────┼───────────┐
▼ ▼ ▼
executor router buildermgr
│ │ │
registerNS AddNS(ts) envw+pkgw
│ .AddNamespace
├─ DefaultNSResolver().AddNamespace(ns) ← thread-safe, dedup
├─ EnsureNamespaceSA(ctx, client, log, ns) ← SA + RBAC provisioning
└─ et.AddNamespace(ns, mgr) ← для каждого executor type
Ключевые файлы
| Файл | Роль |
|---|---|
pkg/utils/namespace.go |
NamespaceResolver — хранит список NS, thread-safe Snapshot/AddNamespace |
pkg/utils/namespace_manager.go |
NamespaceManager — lifecycle, subscribers, event dispatch |
pkg/utils/namespace_manager_model.go |
Типы: Record, Phase, Event, Source, Summary |
pkg/utils/serviceaccount.go |
EnsureNamespaceSA — создаёт fission-fetcher SA/Role/RoleBinding |
pkg/executor/multitenant/ns_watcher.go |
Executor NSWatcher + registerNamespace |
pkg/executor/multitenant/namespace_subscriber.go |
Executor subscriber adapter |
pkg/router/ns_watcher.go |
Router NSWatcher (1 строка, через shared utility) |
pkg/router/namespace_subscriber.go |
Router subscriber adapter |
pkg/buildermgr/ns_watcher.go |
BuilderMgr NSWatcher (1 строка, через shared utility) |
pkg/buildermgr/namespace_subscriber.go |
BuilderMgr subscriber adapter |
deploy/multitenant/rbac.yaml |
ClusterRole/ClusterRoleBinding для всех трёх компонентов |
Инженерные решения и почему именно так
1. Snapshot API вместо прямого чтения map
Проблема: NamespaceResolver.FissionResourceNS — mutable map, защищённая mutex
только на запись. Читатели в разных горутинах обращались к ней напрямую — data race.
Решение: Snapshot() []string — под read lock копирует map в sorted slice.
Потребители итерируют по стабильной копии, безопасно даже при конкурентных AddNamespace.
Почему slice а не map: потребителям нужен обход, а не lookup. Sorted slice даёт детерминированный порядок — важно для тестов и для startup factory generation.
2. NamespaceManager как event bus
Проблема: каждый компонент реализовывал свой namespace watcher с нуля — дублирование кода watcher setup, event handlers, deduplication, logging.
Решение: единый utils.NamespaceManager + NamespaceSubscriber interface.
Компонент реализует только OnNamespaceAdd/Remove/Resync, всё остальное — shared utility.
Это сократило router/ns_watcher.go до 5 строк, buildermgr/ns_watcher.go до 5 строк.
3. EnsureNamespaceSA — одно место, один вызов
Проблема: при динамической регистрации нового NS executor пытался создать pool pod,
но SA fission-fetcher ещё не существовал → FailedCreate, pod не стартует.
Решение: в registerNamespace (executor) вызывается utils.EnsureNamespaceSA
до вызова et.AddNamespace. SA всегда существует к моменту создания первого pod.
Важно: EnsureNamespaceSA — идемпотентная. Повторный вызов = safe no-op.
4. Buildermgr dedup bug
Проблема: buildermgr.StartNSWatcher при добавлении NS вызывал envw.AddNamespace
и pkgw.AddNamespace. Но глобальный DefaultNSResolver().AddNamespace() вызывался
внутри каждого watcher — dedup срабатывал после первого и блокировал второй.
Решение: buildermgr/namespace_subscriber.go вызывает DefaultNSResolver().AddNamespace()
один раз в registerBuilderNamespace, а затем оба watcher добавляют NS независимо.
5. Router informer maps — guard против nil panic
Проблема: в router/httpTriggers.go AddNamespace мог вызываться до инициализации
внутренних informer maps → nil pointer dereference.
Решение: добавлена explicit проверка nil перед операцией, с логом предупреждения.
RBAC — что и почему
deploy/multitenant/rbac.yaml содержит три ClusterRole:
fission-executor-ns-watcher
namespaces: list, watch
Нужен executor для регистрации Namespace informer. Без этого NSWatcher не стартует.
fission-router-ns-watcher
namespaces: list, watch
То же для router.
fission-executor-sa-provisioner
serviceaccounts: get, list, watch, create, update, patch
roles: get, list, watch, create, update, patch
rolebindings: get, list, watch, create, update, patch
events: create
authorization.k8s.io/localsubjectaccessreviews: create
Нетривиальные пункты:
-
events:create — Kubernetes запрещает создавать
Role, выдающую право, которого нет у создающего субъекта.fission-fetcherполучаетevents:create, значит executor тоже должен его иметь. -
localsubjectaccessreviews:create —
setupSAAndRoleBindingsпроверяет существующие права через LSAR перед созданием Role. Без этого — 403.
Что НЕ изменилось (backward compatibility)
FISSION_RESOURCE_NAMESPACESenv var работает как раньше — namespace-ы из него регистрируются при старте черезBootstrap().- Существующие tenant namespace-ы, добавленные через env var, не нуждаются в label.
- Поведение функций, HTTP-триггеров, builder — неизменно.
- Helm chart стандартный; RBAC применяется отдельно:
kubectl apply -f deploy/multitenant/rbac.yaml.
Тест-сценарий (Layer 1)
Проверяет сквозной сценарий без рестарта:
- Создать namespace
l1-test-XXXXX - Добавить label
fission.io/managed=true - Подождать, пока executor зарегистрирует NS (лог
registered namespace) - Создать Environment + Function + HTTPTrigger в namespace
- Вызвать функцию через router — ожидаемый ответ
200 OK
Все шаги (PASS=5 FAIL=0) проходят стабильно после полного RBAC fix.
Порядок деплоя нового форка
# 1. Применить RBAC (один раз на кластер)
kubectl apply -f deploy/multitenant/rbac.yaml
# 2. Деплоить fission-bundle с нашим образом
# (helm upgrade или kubectl apply с новым image tag)
# 3. Создать tenant
kubectl create namespace tenant-abc123
kubectl label namespace tenant-abc123 fission.io/managed=true
# 4. Готово. Можно деплоить функции в tenant-abc123.
Направления дальнейшей работы
- e2e тесты — автоматизированный
test_layer1.sh-подобный тест в Go - Helm chart — включить
deploy/multitenant/rbac.yamlкак условный template - Мониторинг — expose namespace lifecycle events в metrics (Prometheus)
- Remove lifecycle — сейчас при удалении NS стратегия
track-only;
нуженdispatch-remove+ cleanup informers - Feature branch portability — при выходе Fission 1.23 сделать rebase
этой ветки поверх нового upstream тега