doc: add multitenant architecture summary (2026-05-15)
Единый сводный документ, описывающий полную архитектуру мультитенантного 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) - Порядок деплоя нового форка - Направления дальнейшей работы
This commit is contained in:
@@ -0,0 +1,231 @@
|
|||||||
|
# Мультитенантный 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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: tenant-abc123
|
||||||
|
labels:
|
||||||
|
fission.io/managed: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Никакого другого coupling с Fission internals не требуется.
|
||||||
|
|
||||||
|
После появления namespace с этим label Fission автоматически:
|
||||||
|
1. Регистрирует namespace во всех компонентах (executor, router, buildermgr)
|
||||||
|
2. Создаёт SA `fission-fetcher` и необходимый RBAC в namespace
|
||||||
|
3. Подключает informer factory для CRD (Functions, Environments, HTTPTriggers и т.д.)
|
||||||
|
4. Тенант может деплоить функции без задержки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурная карта изменений
|
||||||
|
|
||||||
|
```
|
||||||
|
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_NAMESPACES` env var работает как раньше — namespace-ы из него
|
||||||
|
регистрируются при старте через `Bootstrap()`.
|
||||||
|
- Существующие tenant namespace-ы, добавленные через env var, не нуждаются в label.
|
||||||
|
- Поведение функций, HTTP-триггеров, builder — неизменно.
|
||||||
|
- Helm chart стандартный; RBAC применяется отдельно: `kubectl apply -f deploy/multitenant/rbac.yaml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Тест-сценарий (Layer 1)
|
||||||
|
|
||||||
|
Проверяет сквозной сценарий без рестарта:
|
||||||
|
|
||||||
|
1. Создать namespace `l1-test-XXXXX`
|
||||||
|
2. Добавить label `fission.io/managed=true`
|
||||||
|
3. Подождать, пока executor зарегистрирует NS (лог `registered namespace`)
|
||||||
|
4. Создать Environment + Function + HTTPTrigger в namespace
|
||||||
|
5. Вызвать функцию через router — ожидаемый ответ `200 OK`
|
||||||
|
|
||||||
|
Все шаги (PASS=5 FAIL=0) проходят стабильно после полного RBAC fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок деплоя нового форка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Направления дальнейшей работы
|
||||||
|
|
||||||
|
1. **e2e тесты** — автоматизированный `test_layer1.sh`-подобный тест в Go
|
||||||
|
2. **Helm chart** — включить `deploy/multitenant/rbac.yaml` как условный template
|
||||||
|
3. **Мониторинг** — expose namespace lifecycle events в metrics (Prometheus)
|
||||||
|
4. **Remove lifecycle** — сейчас при удалении NS стратегия `track-only`;
|
||||||
|
нужен `dispatch-remove` + cleanup informers
|
||||||
|
5. **Feature branch portability** — при выходе Fission 1.23 сделать rebase
|
||||||
|
этой ветки поверх нового upstream тега
|
||||||
Reference in New Issue
Block a user