Skalowalna architektura LMS: wzorzec wielu najemców
Globalny rynek System zarządzania nauką (LMS) minął m.in 28 miliardów dolarów w 2025 roku, z prognozą wzrostu na poziomie prawie 70 miliardów do 2030 r. Za platformami takimi jak Moodle, Canvas, Docebo i Coursera kryją w sobie ogromne wyzwania architektoniczne: służąc tysiącom organizacji jednocześnie zapewniają izolację danych, skalują poziomo podczas szczytowego wykorzystania i utrzymuj opóźnienia poniżej sekundy dla milionów studentów podłączonych jednocześnie.
La multi-tenancy e il pattern architetturale che rende tutto questo possibile. In un LMS multi-tenant, una singola istanza dell'applicazione serve molteplici organizzazioni (tenant), ciascuna con i propri utenti, corsi, contenuti e configurazioni, ma condividendo l'infrastruttura sottostante. La scelta del pattern multi-tenant corretto determina costi operativi, sicurezza, performance e la capacità di scalare da 10 a 10.000 tenant senza riscrivere il sistema.
In questo articolo inaugurale della serie EdTech Engineering, analizzeremo in profondità le architetture multi-tenant per piattaforme LMS, confrontando pattern di isolamento dei dati, strategie di scaling, modelli di dati ottimizzati e pattern event-driven per funzionalità in tempo reale. Ogni concetto sarà accompagnato da esempi di codice concreti in TypeScript e Python.
Cosa Imparerai in Questo Articolo
- I tre pattern fondamentali di multi-tenancy e quando usare ciascuno
- Come progettare un modello dati LMS che scala con migliaia di tenant
- Autenticazione e autorizzazione in contesti multi-tenant con JWT e RBAC
- Design di API REST e GraphQL per la risoluzione del tenant
- Architettura event-driven per notifiche in tempo reale e analytics
- Decomposizione in microservizi di una piattaforma LMS enterprise
- Confronto tra le architetture di Moodle, Canvas, Blackboard e piattaforme moderne
- Benchmark di performance e strategie di caching per LMS ad alto traffico
Panoramica della Serie EdTech Engineering
| # | Articolo | Focus |
|---|---|---|
| 1 | Sei qui - Architettura LMS Scalabile | Pattern multi-tenant per piattaforme LMS |
| 2 | Video Streaming Educativo | Architettura di distribuzione contenuti video |
| 3 | Learning Analytics e xAPI | Raccolta e analisi dati di apprendimento |
| 4 | SCORM e Content Packaging | Standard per i contenuti didattici |
| 5 | Adaptive Learning con AI | Personalizzazione dei percorsi formativi |
| 6 | Gamification nelle Piattaforme | Badge, leaderboard e motivazione |
| 7 | Proctoring e Assessment | Esami online sicuri e antifrode |
| 8 | Collaborazione Real-Time | Lavagne condivise e editing collaborativo |
| 9 | Mobile e Offline Learning | PWA e sincronizzazione offline |
| 10 | LLM come Tutor Virtuale | Integrazione di modelli linguistici nell'e-learning |
Il Panorama dei Learning Management System
Un LMS moderno non e più un semplice repository di corsi con un sistema di tracciamento. Le piattaforme di oggi integrano streaming video adattivo, assessment interattivi, analytics predittive, collaborazione in tempo reale e, sempre più spesso, tutor basati su intelligenza artificiale. Questa complessità funzionale si traduce direttamente in complessità architetturale.
Le piattaforme LMS si dividono in tre macro-categorie in base al modello di deployment:
- Self-hosted (on-premise): Moodle, Open edX. L'organizzazione gestisce l'intera infrastruttura. Massimo controllo, massimo overhead operativo.
- SaaS multi-tenant: Chmura płótna, TalentLMS, Docebo. Sprzedawca zajmuje się wszystkim. Każdy najemca ma swoją logicznie wydzieloną przestrzeń.
- PaaS/hybrydowy: Tablica Dowiedz się Ultra, Brightspace. Infrastruktura zarządzana przez dostawcę, ale z zaawansowanymi opcjami dostosowywania i wdrożeniami prywatnymi.
Dominującym trendem w latach 2025-2026 jest architektura natywnie dla wielu dzierżawców w chmurze, przy wzroście o 34% rok do roku w przypadku platform SaaS w porównaniu do 7% w przypadku rozwiązań lokalnych. Zmiana ta wynika z konieczności obniżenia kosztów operacyjnych i skrócenia czasu wprowadzenia produktu na rynek nowych funkcji i służyć rosnącej liczbie organizacji z małymi zespołami inżynieryjnymi.
Kluczowe wyzwania związane z platformami LMS obsługującymi wielu najemców
- Izolacja danych: Dane z jednego uniwersytetu nie mogą być nigdy dostępne z innego
- Hałaśliwy sąsiad: Najemca, w którym studiuje 50 000 studentów, nie powinien pogarszać wyników małych najemców
- Personalizacja: Każdy najemca chce innego brandingu, przepływu pracy i integracji
- Zgodność: RODO, FERPA, SOC 2 wymagają szczególnych gwarancji dotyczących pobytu i przetwarzania danych
- Niejednorodne skalowanie: Szczyty wykorzystania mają charakter sezonowy (początek semestru, egzaminy) i różnią się w zależności od najemcy
Trzy wzorce wielodostępności
Wybór wzorca obsługującego wielu najemców jest najważniejszą decyzją architektoniczną przy projektowaniu LMS. Istnieją trzy podstawowe podejścia, każde o innym profilu izolacji, kosztach i złożoności.
Wzorzec 1: Baza danych na dzierżawcę (model silosowy)
Każdy najemca otrzymuje dedykowaną bazę danych. Takie podejście zapewnia najwyższy poziom izolacji: dane są fizycznie oddzielone, wydajność jednego dzierżawcy nie wpływa na pozostałych i operacje kopia zapasowa/przywracanie są niezależne. Jest to wzór preferowany przez platformy obsługujące duże rozmiary przedsiębiorstwo o rygorystycznych wymaganiach dotyczących zgodności.
- Izolacja: Maksymalny. Całkowita fizyczna separacja danych.
- Koszt: Wysoki. Każda baza danych zużywa dedykowane zasoby (połączenia, pamięć masowa, kopie zapasowe).
- Złożoność: Wysoki. Migracje schematów należy przeprowadzić w każdej bazie danych.
- Skalowalność: Liniowy, ale drogi. Każdy nowy najemca = nowa infrastruktura.
- Idealny przypadek użycia: Przedsiębiorstwo składające się z mniej niż 100 dużych organizacji, wymagania FERPA/HIPAA.
Wzór 2: Schemat najemcy (model pomostowy)
Wszyscy dzierżawcy korzystają z tej samej instancji bazy danych, ale każdy ma swój własny schemat (przestrzeń nazw). Na przykład w PostgreSQL każdy najemca działa według własnego schematu z identycznymi tabelami, ale izolowanymi danymi. Takie podejście równoważy izolację i koszty operacyjne.
- Izolacja: Mocny. Separacja logiczna na poziomie schematu. Zanieczyszczenie krzyżowe niemożliwe bez wyraźnych błędów.
- Koszt: Średni. Pojedynczy klaster DB obsługuje wszystkich dzierżawców.
- Złożoność: Przeciętny. Migracje wymagają iteracji we wszystkich schematach, ale można je zautomatyzować.
- Skalowalność: Dobre do ~ 500-1000 schematów na instancję PostgreSQL, następnie potrzebne jest sharding.
- Idealny przypadek użycia: SaaS z 50–1000 średnich dzierżawców wymaga dostosowania według schematu.
Wzorzec 3: Schemat udostępniony z identyfikatorem dzierżawy (model puli)
Wszyscy najemcy korzystają z tych samych tabel. Izolację gwarantuje kolumna tenant_id
obecny w każdej tabeli i od Bezpieczeństwo na poziomie wiersza (RLS) na poziomie bazy danych. I wzór
najbardziej opłacalne i najłatwiejsze w zarządzaniu operacyjnym, ale wymaga dyscypliny
rygorystyczne w kodzie aplikacji.
- Izolacja: Logiczny. To zależy od RLS i walidacji aplikacji. Błąd w kodzie może ujawnić dane pochodzące od wielu dzierżawców.
- Koszt: Bas. Pojedyncza instancja bazy danych obsługuje tysiące dzierżawców.
- Złożoność: Niski dla migracji (pojedynczy schemat), wysoki dla bezpieczeństwa (każde zapytanie musi zawierać filtr dzierżawcy).
- Skalowalność: Doskonały. Obsługuje tysiące najemców za pomocą fragmentowania opartego na identyfikatorze najemcy (Citus dla PostgreSQL).
- Idealny przypadek użycia: Masowa usługa SaaS z tysiącami małych i średnich najemców.
Porównanie wzorów
| Kryterium | Baza danych dla najemców | Schemat najemcy | Schemat wspólny + RLS |
|---|---|---|---|
| Izolacja danych | Fizyczne (maksymalne) | Logiczne (silne) | Logiczne (RLS) |
| Koszt na najemcę | Wysoka (50-200 USD/miesiąc) | Średni (10-50 USD/miesiąc) | Niski (1-10 USD/miesiąc) |
| Migracje schematów | N wykonań (1 na bazę danych) | N wykonań (1 na schemat) | 1 wykonanie |
| Max polecał najemców | 50-200 | 500-2000 | 10 000+ |
| Ryzyko hałaśliwego sąsiada | Nikt | Średni (wspólne we/wy) | Wysoka (możliwa do złagodzenia za pomocą shardingu) |
| Zgodność z FERPA/RODO | Doskonały | Dobry | Wymaga dodatkowych audytów |
| Złożoność operacyjna | Wysoki | Przeciętny | Niski |
| Dostosowanie wzoru | Całkowity | Według schematu | Ograniczone (pola niestandardowe) |
Rozwiązanie najemcy: oprogramowanie pośredniczące i strategie
W systemie z wieloma dzierżawcami każde żądanie HTTP musi być powiązane z właściwym dzierżawcą Zanim dowolnej logiki aplikacji. Proces ten, tzw uchwała najemcyi pierwszy warstwa architektury i punkt krytyczny dla bezpieczeństwa. Jeśli rozwiązanie nie powiedzie się lub zostanie pominięte, cała izolacja upada.
Strategie rozwiązywania problemów
Istnieją cztery główne strategie identyfikacji dzierżawcy żądania:
- Subdomena:
mit.lms-platform.com,stanford.lms-platform.com. Najbardziej powszechny wymaga konfiguracji DNS z użyciem symboli wieloznacznych. - Przedrostek ścieżki:
lms-platform.com/tenants/mit/courses. Proste, ale zanieczyszcza ścieżki API. - Nagłówki HTTP:
X-Tenant-ID: mit-university. Elastyczny dla interfejsu API maszyna-maszyna. - Twierdzenie JWT: Dzierżawca jest zakodowany w tokenie uwierzytelniania. Bezpieczne, ale wymaga ponownego uwierzytelnienia w celu zmiany dzierżawców.
Implementacja oprogramowania pośredniczącego do rozwiązywania problemów (TypeScript/Express)
Następujące oprogramowanie pośredniczące Express implementuje łączoną strategię: subdomena jako podstawowa, Nagłówek HTTP jako rezerwowy z weryfikacją w rejestrze dzierżawców.
import { Request, Response, NextFunction } from 'express';
import { LRUCache } from 'lru-cache';
// Interfaccia tenant immutabile
interface TenantConfig {
readonly id: string;
readonly slug: string;
readonly dbSchema: string;
readonly plan: 'free' | 'pro' | 'enterprise';
readonly region: 'eu-west-1' | 'us-east-1' | 'ap-southeast-1';
readonly features: ReadonlyArray<string>;
readonly maxUsers: number;
readonly isActive: boolean;
}
// Cache per evitare lookup al DB su ogni richiesta
const tenantCache = new LRUCache<string, TenantConfig>({
max: 5000,
ttl: 1000 * 60 * 5, // 5 minuti
});
// Repository per il lookup dei tenant
interface TenantRepository {
findBySlug(slug: string): Promise<TenantConfig | null>;
}
export function createTenantMiddleware(repo: TenantRepository) {
return async (req: Request, res: Response, next: NextFunction) => {
// 1. Estrai slug dal sottodominio
const host = req.hostname;
let tenantSlug = extractSubdomain(host);
// 2. Fallback: header X-Tenant-ID
if (!tenantSlug) {
tenantSlug = req.headers['x-tenant-id'] as string | undefined
?? null;
}
// 3. Validazione: slug obbligatorio
if (!tenantSlug) {
res.status(400).json({
error: 'TENANT_NOT_RESOLVED',
message: 'Unable to determine tenant from request',
});
return;
}
// 4. Lookup con cache
let config = tenantCache.get(tenantSlug) ?? null;
if (!config) {
config = await repo.findBySlug(tenantSlug);
if (config) {
tenantCache.set(tenantSlug, config);
}
}
// 5. Tenant non trovato o disattivato
if (!config || !config.isActive) {
res.status(404).json({
error: 'TENANT_NOT_FOUND',
message: `Tenant '${tenantSlug}' not found or inactive`,
});
return;
}
// 6. Inietta il contesto tenant nella request
(req as any).tenantConfig = config;
(req as any).tenantId = config.id;
next();
};
}
function extractSubdomain(host: string): string | null {
const parts = host.split('.');
// es: "mit.lms-platform.com" -> ["mit", "lms-platform", "com"]
if (parts.length >= 3) {
const subdomain = parts[0];
// Escludi subdomini di sistema
const reserved = ['www', 'api', 'admin', 'status'];
return reserved.includes(subdomain) ? null : subdomain;
}
return null;
}
Implementacja Pythona (FastAPI)
Dla osób korzystających z FastAPI, tutaj jest odpowiednik zastrzyk zależności w sprawie uchwały najemcy:
from fastapi import Request, HTTPException, Depends
from functools import lru_cache
from dataclasses import dataclass, field
from typing import Optional
import asyncio
@dataclass(frozen=True)
class TenantConfig:
"""Configurazione immutabile del tenant."""
id: str
slug: str
db_schema: str
plan: str # 'free' | 'pro' | 'enterprise'
region: str
features: tuple[str, ...] = field(default_factory=tuple)
max_users: int = 100
is_active: bool = True
class TenantResolver:
"""Risolve il tenant dalla richiesta HTTP."""
def __init__(self, repository):
self._repo = repository
self._cache: dict[str, TenantConfig] = {}
async def resolve(self, request: Request) -> TenantConfig:
# 1. Sottodominio
slug = self._extract_subdomain(request.headers.get("host", ""))
# 2. Fallback: header
if not slug:
slug = request.headers.get("x-tenant-id")
if not slug:
raise HTTPException(
status_code=400,
detail="Unable to determine tenant from request"
)
# 3. Cache lookup
if slug in self._cache:
config = self._cache[slug]
else:
config = await self._repo.find_by_slug(slug)
if config:
self._cache[slug] = config
if not config or not config.is_active:
raise HTTPException(
status_code=404,
detail=f"Tenant '{slug}' not found or inactive"
)
return config
@staticmethod
def _extract_subdomain(host: str) -> Optional[str]:
parts = host.split(".")
if len(parts) >= 3:
subdomain = parts[0]
reserved = {"www", "api", "admin", "status"}
return None if subdomain in reserved else subdomain
return None
# Dependency injection in FastAPI
async def get_tenant(request: Request) -> TenantConfig:
resolver = request.app.state.tenant_resolver
return await resolver.resolve(request)
Model danych dla wielodostępnego systemu LMS
Model danych LMS musi uwzględniać złożone podmioty i ich relacje: użytkowników, organizacje, kursy, moduły, lekcje, ocena, złożenie, rejestracja, postęp, certyfikaty. W kontekście wielu dzierżawców, każda jednostka musi być powiązana z dzierżawcą będącym właścicielem, a zapytania muszą być efektywne zarówno w przypadku operacji z jednym dzierżawcą (norma), jak i z wieloma dzierżawcami (raportowanie administracyjne).
Podstawowy schemat bazy danych
Poniższy schemat SQL reprezentuje rdzeń systemu LMS z wieloma dzierżawcami ze wzorcem „schemat współdzielony + identyfikator_dzierżawy”. Użyj PostgreSQL z Bezpieczeństwo na poziomie wiersza (RLS), aby zapewnić izolację.
-- ============================================
-- TABELLA TENANT (registro delle organizzazioni)
-- ============================================
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(63) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
plan VARCHAR(20) NOT NULL DEFAULT 'free',
settings JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- ============================================
-- TABELLA UTENTI
-- ============================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
email VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'student',
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, email)
);
-- ============================================
-- TABELLA CORSI
-- ============================================
CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
instructor_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(500) NOT NULL,
slug VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
max_enrollment INTEGER,
settings JSONB NOT NULL DEFAULT '{}',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, slug)
);
-- ============================================
-- MODULI E LEZIONI (struttura gerarchica)
-- ============================================
CREATE TABLE modules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
content_type VARCHAR(30) NOT NULL, -- 'video', 'text', 'quiz', 'assignment'
content_ref TEXT, -- URL o ID del contenuto
duration_min INTEGER,
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- ISCRIZIONI (enrollments)
-- ============================================
CREATE TABLE enrollments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
user_id UUID NOT NULL REFERENCES users(id),
course_id UUID NOT NULL REFERENCES courses(id),
status VARCHAR(20) NOT NULL DEFAULT 'active',
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
UNIQUE (tenant_id, user_id, course_id)
);
-- ============================================
-- PROGRESSI (lesson completion tracking)
-- ============================================
CREATE TABLE lesson_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
user_id UUID NOT NULL REFERENCES users(id),
lesson_id UUID NOT NULL REFERENCES lessons(id),
status VARCHAR(20) NOT NULL DEFAULT 'not_started',
progress_pct SMALLINT NOT NULL DEFAULT 0,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
UNIQUE (tenant_id, user_id, lesson_id)
);
-- ============================================
-- ASSESSMENT E SUBMISSION
-- ============================================
CREATE TABLE assessments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
course_id UUID NOT NULL REFERENCES courses(id),
title VARCHAR(500) NOT NULL,
assessment_type VARCHAR(30) NOT NULL, -- 'quiz', 'exam', 'assignment', 'peer_review'
max_score DECIMAL(6,2) NOT NULL DEFAULT 100,
passing_score DECIMAL(6,2) NOT NULL DEFAULT 60,
time_limit_min INTEGER,
max_attempts INTEGER DEFAULT 1,
questions JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
assessment_id UUID NOT NULL REFERENCES assessments(id),
user_id UUID NOT NULL REFERENCES users(id),
attempt_number SMALLINT NOT NULL DEFAULT 1,
answers JSONB NOT NULL DEFAULT '{}',
score DECIMAL(6,2),
status VARCHAR(20) NOT NULL DEFAULT 'submitted',
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
graded_at TIMESTAMPTZ
);
-- ============================================
-- ROW-LEVEL SECURITY (RLS)
-- ============================================
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE courses ENABLE ROW LEVEL SECURITY;
ALTER TABLE enrollments ENABLE ROW LEVEL SECURITY;
ALTER TABLE lesson_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE submissions ENABLE ROW LEVEL SECURITY;
-- Policy: ogni utente vede solo i dati del proprio tenant
CREATE POLICY tenant_isolation_users ON users
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
CREATE POLICY tenant_isolation_courses ON courses
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
CREATE POLICY tenant_isolation_enrollments ON enrollments
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
-- INDICI per performance multi-tenant
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_courses_tenant ON courses(tenant_id);
CREATE INDEX idx_enrollments_tenant_user ON enrollments(tenant_id, user_id);
CREATE INDEX idx_enrollments_tenant_course ON enrollments(tenant_id, course_id);
CREATE INDEX idx_lesson_progress_tenant_user ON lesson_progress(tenant_id, user_id);
CREATE INDEX idx_submissions_tenant_assessment ON submissions(tenant_id, assessment_id);
Uwaga: Indeksy wielokolumnowe z identyfikatorem najemcy
We wspólnym schemacie LMS, każdy indeks musi zawierać tenant_id jako pierwsza kolumna.
Indeks na (email) i bezużyteczne dla zapytań filtrowanych według najemców; to pomaga (tenant_id, email).
W przypadku rozproszonego Citus/sharding kolumna dystrybucji (tenant_id) musi być obecny
w każdym złożonym kluczu podstawowym i w każdym ograniczeniu UNIQUE.
Schemat relacji
| Podmiot | Relacja | Połączona jednostka | Kardynał |
|---|---|---|---|
| Najemca | posiada | Użytkownicy, kursy | 1: Nie |
| Użytkownik | zapisujesz się | Kurs (poprzez rejestrację) | N:M |
| Kurs | zawiera | Moduły | 1: Nie |
| Moduł | zawiera | Lekcje | 1: Nie |
| Kurs | ha | Oceny | 1: Nie |
| Użytkownik | poddaje się | Zgłoszenia (poprzez ocenę) | 1: Nie |
| Użytkownik | śledzić postępy | Postęp lekcji (poprzez lekcję) | 1: Nie |
Uwierzytelnianie i autoryzacja wielu dzierżawców
W wielodostępnym systemie LMS uwierzytelnianie musi zostać rozwiązane dwa problemy jednocześnie: zweryfikuj tożsamość użytkownika i określ, do którego najemcy należy użytkownik. Autoryzacja dodaje trzeci poziom: do jakich zasobów użytkownik może uzyskać dostęp w ramach swojej dzierżawy.
JWT z najemcą roszczenia
Najpopularniejsze podejście wykorzystuje Token sieciowy JSON (JWT) z dedykowaną roszczeniem na rzecz najemcy. Token wydawany jest po uwierzytelnieniu i zawiera wszystkie informacje niezbędne do rozwiązania kontekstu bez dalszego przeszukiwania bazy danych.
import jwt from 'jsonwebtoken';
// Payload JWT immutabile
interface LmsTenantJwtPayload {
readonly sub: string; // User ID
readonly tenantId: string; // Tenant UUID
readonly tenantSlug: string; // Per display e logging
readonly role: 'student' | 'instructor' | 'admin' | 'super_admin';
readonly permissions: ReadonlyArray<string>;
readonly iat: number;
readonly exp: number;
}
// Generazione token con contesto tenant
function generateTenantToken(
user: { id: string; role: string },
tenant: { id: string; slug: string },
permissions: ReadonlyArray<string>
): string {
const payload: Omit<LmsTenantJwtPayload, 'iat' | 'exp'> = {
sub: user.id,
tenantId: tenant.id,
tenantSlug: tenant.slug,
role: user.role as LmsTenantJwtPayload['role'],
permissions,
};
return jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: '8h',
issuer: 'lms-platform',
audience: tenant.slug,
});
}
// Middleware di validazione
function validateTenantToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'MISSING_TOKEN' });
}
try {
const token = authHeader.slice(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET!, {
issuer: 'lms-platform',
}) as LmsTenantJwtPayload;
// Verifica coerenza tenant tra URL e token
const urlTenantId = (req as any).tenantId;
if (urlTenantId && decoded.tenantId !== urlTenantId) {
return res.status(403).json({
error: 'TENANT_MISMATCH',
message: 'Token tenant does not match request tenant',
});
}
(req as any).user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'INVALID_TOKEN' });
}
}
Model RBAC dla LMS
Autoryzacja w systemie LMS zazwyczaj przebiega według szablonu Kontrola dostępu oparta na rolach (RBAC) z czterema głównymi rolami, każda z skumulowanymi uprawnieniami:
| Rola | Kluczowe uprawnienia | Miotły |
|---|---|---|
| Student | Przeglądaj zapisane kursy, przesyłaj zadania, przeglądaj swoje oceny | Tylko kursy, na które jesteś zapisany |
| Instruktor | Twórz/edytuj kursy, oceniaj zgłoszenia, przeglądaj statystyki swoich kursów | Tylko własne kursy |
| Administrator | Zarządzaj użytkownikami, konfiguruj najemcę, przeglądaj wszystkie kursy, eksportuj raporty | Cały najemca |
| Superadministrator | Zarządzaj najemcami, rozliczeniami, flagami funkcji i obsługą wielu dzierżawców | Wszyscy najemcy (operator platformy) |
Projekt API dla wielodostępnego systemu LMS
Interfejsy API wielodostępnego systemu LMS muszą być zrównoważone prostota użytkowania dla programistów klienta (frontend, mobile, integracje) z rygorystyczne bezpieczeństwo w izolacji danych. Wybór pomiędzy REST i GraphQL zależy od dominujących wzorców dostępu.
REST API: Konwencje dotyczące wielu najemców
W architekturze REST dla LMS, najemca jest rozpoznawany przez oprogramowanie pośredniczące (subdomena lub nagłówek), dlatego ścieżki zasobów nie zawierają identyfikatora dzierżawy. Upraszcza to interfejsy API i sprawia, że są one przenośne między środowiskami.
# Tenant risolto via sottodominio: mit.lms-api.com
# Il tenant_id e iniettato dal middleware, NON presente nei path
# === CORSI ===
GET /api/v1/courses # Lista corsi del tenant
GET /api/v1/courses/:courseId # Dettaglio corso
POST /api/v1/courses # Crea corso (instructor+)
PUT /api/v1/courses/:courseId # Aggiorna corso
DELETE /api/v1/courses/:courseId # Archivia corso (admin+)
# === ISCRIZIONI ===
POST /api/v1/courses/:courseId/enroll # Iscrivi utente corrente
GET /api/v1/enrollments # Le mie iscrizioni
GET /api/v1/courses/:courseId/students # Studenti iscritti (instructor+)
# === CONTENUTI ===
GET /api/v1/courses/:courseId/modules # Moduli del corso
GET /api/v1/modules/:moduleId/lessons # Lezioni del modulo
PUT /api/v1/lessons/:lessonId/progress # Aggiorna progresso
# === ASSESSMENT ===
GET /api/v1/courses/:courseId/assessments # Assessment del corso
POST /api/v1/assessments/:assessmentId/submissions # Sottometti risposta
GET /api/v1/submissions/:submissionId # Dettaglio submission
# === ANALYTICS (instructor/admin) ===
GET /api/v1/analytics/courses/:courseId/completion # Tassi completamento
GET /api/v1/analytics/tenant/overview # Overview tenant
GraphQL: Elastyczne zapytania dla frontendu
GraphQL jest szczególnie skuteczny dla klientów LMS, ponieważ strony często tego wymagają złożone agregacje: panel studentów z zapisanymi na kursy, postępami, nadchodzącymi kursami przydział i powiadomienia w jednym żądaniu.
# Il contesto tenant e iniettato automaticamente dal middleware
# Ogni resolver filtra per tenant_id dal contesto
type Query {
# Corsi disponibili per il tenant corrente
courses(
status: CourseStatus
search: String
pagination: PaginationInput
): CourseConnection!
# Dettaglio corso (solo se nel tenant corrente)
course(id: ID!): Course
# Dashboard studente: dati aggregati in una singola query
myDashboard: StudentDashboard!
# Analytics per instructor
courseAnalytics(courseId: ID!): CourseAnalytics!
}
type Course {
id: ID!
title: String!
description: String
instructor: User!
modules: [Module!]!
enrollmentCount: Int!
completionRate: Float
status: CourseStatus!
createdAt: DateTime!
}
type StudentDashboard {
enrolledCourses: [EnrolledCourseProgress!]!
upcomingAssessments: [Assessment!]!
recentActivity: [ActivityEvent!]!
overallProgress: Float!
certificatesEarned: Int!
}
type EnrolledCourseProgress {
course: Course!
progressPercent: Float!
lastAccessedAt: DateTime
nextLesson: Lesson
}
type Mutation {
enrollInCourse(courseId: ID!): Enrollment!
updateLessonProgress(lessonId: ID!, progress: Int!): LessonProgress!
submitAssessment(assessmentId: ID!, answers: JSON!): Submission!
createCourse(input: CreateCourseInput!): Course!
}
type Subscription {
# Notifiche real-time per lo studente
studentNotifications: Notification!
# Aggiornamenti live del progresso (per instructor dashboard)
courseProgressUpdates(courseId: ID!): ProgressUpdate!
}
Dostarczanie treści i strumieniowe przesyłanie multimediów
LMS zarządza heterogeniczną treścią: dokumentami PDF, prezentacjami, filmami na żądanie, transmisją na żywo, interaktywne quizy i wirtualne warsztaty. Architektura dostarczania treści musi gwarantować niskie opóźnienie, izolacja każdego najemcy e kontrola dostępu ziarnisty.
Strategia przechowywania danych dla wielu dzierżawców
Wybór wzorca przechowywania odzwierciedla wybór bazy danych: większa izolacja oznacza większe koszty. Najpopularniejsze podejście do SaaS LMS wykorzystuje a pojedyncze wiadro S3 z izolacją opartą na prefiksach:
# Pattern di organizzazione S3 per LMS multi-tenant
# Bucket: lms-content-production
# Contenuti del corso
s3://lms-content/{tenant_id}/courses/{course_id}/videos/lecture-01.mp4
s3://lms-content/{tenant_id}/courses/{course_id}/documents/syllabus.pdf
s3://lms-content/{tenant_id}/courses/{course_id}/images/cover.webp
# Submission degli studenti
s3://lms-content/{tenant_id}/submissions/{assessment_id}/{user_id}/file.pdf
# Asset del tenant (logo, branding)
s3://lms-content/{tenant_id}/branding/logo.svg
s3://lms-content/{tenant_id}/branding/theme.json
# Bucket policy limita l'accesso per prefix = tenant_id
# Signed URLs con expiry di 1 ora per accesso ai contenuti
Architektura CDN dla filmów edukacyjnych
Wideo stanowi 70-80% ruchu współczesnego LMS-a. Architektura dostaw wideo wymaga:
- Transkodowanie adaptacyjne: Każdy film jest kodowany w wielu rozdzielczościach (360p, 720p, 1080p) za pomocą HLS/DASH w celu przesyłania strumieniowego z adaptacyjną szybkością transmisji bitów.
- CDN z uwierzytelnianiem tokenowym: CloudFront lub Cloudflare z podpisanymi adresami URL, aby zapobiec nieautoryzowanemu dostępowi do płatnych treści.
- Buforowanie brzegowe na dzierżawcę: Treść od większych najemców jest wstępnie ładowana do brzegów PoP, aby zmniejszyć TTFB.
- Dynamiczny znak wodny: Niewidoczna nakładka z identyfikatorem użytkownika do śledzenia wycieków treści.
Architektura potoku wideo
Pełny przebieg filmu przesłanego przez instruktora:
- Przesłane pliki: Wstępnie ustawiony adres URL do bezpośredniego przesłania do S3 (obejście serwera aplikacji)
- Wydarzenie S3: Włącz funkcję Lambda/Cloud
s3:ObjectCreated - Transkodowanie: AWS MediaConvert lub FFmpeg na ECS/Cloud Run w celu wygenerowania HLS o wielu bitrate
- Metadane: Czas trwania, miniatura, rozmiar zapisane w bazie LMS
- Unieważnienie CDN: Ogrzewanie pamięci podręcznej dla dzierżawcy będącego właścicielem
- Powiadomienie: Zdarzenie opublikowane w celu powiadomienia instruktora za pośrednictwem protokołu WebSocket
Architektura sterowana zdarzeniami zapewniająca funkcjonalność w czasie rzeczywistym
Nowoczesny LMS wymaga wielu funkcji asynchronicznych i działających w czasie rzeczywistym: powiadomień push, gdy instruktor opublikuj głos, aktualizacje rankingów na żywo, śledzenie postępów w czasie rzeczywistym na desce rozdzielczej instruktora, automatyczne przypomnienia o nadchodzących terminach. Te potrzeby wymagają aarchitektura sterowana zdarzeniami.
Zdarzenia domeny dla LMS
Pierwszym krokiem jest identyfikacja istotnych zdarzeń w domenie platformy. Każde zdarzenie jest niezmiennym faktem, który opisuje coś, co wydarzyło się w systemie.
// Evento base immutabile
interface LmsDomainEvent {
readonly eventId: string;
readonly eventType: string;
readonly tenantId: string;
readonly userId: string;
readonly timestamp: string; // ISO 8601
readonly version: number; // Schema versioning
}
// === ENROLLMENT EVENTS ===
interface StudentEnrolledEvent extends LmsDomainEvent {
readonly eventType: 'student.enrolled';
readonly payload: {
readonly courseId: string;
readonly courseName: string;
readonly enrollmentId: string;
};
}
// === PROGRESS EVENTS ===
interface LessonCompletedEvent extends LmsDomainEvent {
readonly eventType: 'lesson.completed';
readonly payload: {
readonly courseId: string;
readonly moduleId: string;
readonly lessonId: string;
readonly progressPercent: number; // 0-100 per il corso
};
}
// === ASSESSMENT EVENTS ===
interface AssessmentSubmittedEvent extends LmsDomainEvent {
readonly eventType: 'assessment.submitted';
readonly payload: {
readonly assessmentId: string;
readonly submissionId: string;
readonly courseId: string;
readonly attemptNumber: number;
};
}
interface AssessmentGradedEvent extends LmsDomainEvent {
readonly eventType: 'assessment.graded';
readonly payload: {
readonly submissionId: string;
readonly assessmentId: string;
readonly score: number;
readonly maxScore: number;
readonly passed: boolean;
};
}
// === COURSE LIFECYCLE EVENTS ===
interface CoursePublishedEvent extends LmsDomainEvent {
readonly eventType: 'course.published';
readonly payload: {
readonly courseId: string;
readonly courseName: string;
readonly instructorId: string;
readonly moduleCount: number;
readonly lessonCount: number;
};
}
// Event bus con partizionamento per tenant
class LmsEventBus {
constructor(private readonly kafka: KafkaProducer) {}
async publish(event: LmsDomainEvent): Promise<void> {
await this.kafka.send({
topic: `lms.events.${event.eventType.split('.')[0]}`,
messages: [{
key: event.tenantId, // Partizionamento per tenant
value: JSON.stringify(event),
headers: {
'event-type': event.eventType,
'tenant-id': event.tenantId,
'event-version': String(event.version),
},
}],
});
}
}
Konsument i prognozy
Zdarzenia są konsumowane przez wyspecjalizowane procedury obsługi, które wykonują efekty uboczne: powiadomienia, aktualizacja widoku zmaterializowanego, analityka, harmonogram e-maili. Wzór konsumenta na domenę utrzymuje spójność.
| Wydarzenie | Konsument | Działanie |
|---|---|---|
student.enrolled | Usługa powiadomień | E-mail powitalny + powiadomienie push |
student.enrolled | Usługa analityczna | Zaktualizuj licznik rejestracji na kurs |
lesson.completed | Usługa Postępu | Oblicz ponownie ogólny postęp kursu |
lesson.completed | Usługa grywalizacji | Sprawdź, czy odblokowuje odznakę |
assessment.graded | Usługa powiadomień | Powiadom ucznia o ocenie |
assessment.graded | Usługa certyfikatów | Wygeneruj certyfikat, jeśli kurs został ukończony |
course.published | Usługa SearchIndex | Zaktualizuj katalog Elasticsearch Index |
course.published | Usługa powiadomień | Zarejestrowanych uczniów należy powiadomić nauczyciela |
Dekompozycja na mikrousługi
W przypadku korporacyjnych platform LMS obsługujących ponad 100 000 jednoczesnych użytkowników powstaje monolit wąskie gardło. Dekompozycja na mikrousługi umożliwia niezależne skalowanie najbardziej obciążone elementy. Kluczem jest zidentyfikowanie ograniczony kontekst poprawne zgodnie z zasadami projektowania opartego na domenie.
Ograniczony kontekst LMS
| Mikroserwis | Odpowiedzialność | Bazy danych | Protokół |
|---|---|---|---|
| Obsługa Najemcy | Rejestr najemców, konfiguracje, rozliczenia, flagi funkcji | Dedykowany PostgreSQL | ODPOCZYNEK + gRPC |
| Usługa tożsamości | Uwierzytelnianie, SSO (SAML/OIDC), zarządzanie użytkownikami, RBAC | PostgreSQL + Redis (sesje) | REST + OAuth 2.0 |
| Obsługa kursu | Kursy CRUD, moduły, lekcje, katalog, wyszukiwanie | PostgreSQL + Elasticsearch | ODPOCZYNEK + GraphQL |
| Usługa rejestracji | Rejestracje, postępy, ukończenia, certyfikaty | PostgreSQL + Redis (pamięć podręczna postępu) | REST + wydarzenia Kafki |
| Usługa oceny | Silnik quizów, ocenianie, rubryki, recenzja | PostgreSQL + S3 (przesłanie pliku) | REST + wydarzenia Kafki |
| Usługa treści | Przesyłanie, transkodowanie, przechowywanie, pakowanie CDN, SCORM | S3 + DynamoDB (metadane) | REST + zdarzenia S3 |
| Usługa powiadamiania | E-mail, push, w aplikacji, WebSocket, planowanie podsumowań | Redis + SQS | Zdarzenia Kafka + WebSocket |
| Usługa analityczna | xAPI/Caliper, dashboardy, raporty, eksport danych | ClickHouse + S3 (jezioro danych) | Wydarzenia Kafka + REST |
Komunikacja pomiędzy Usługami
Komunikacja opiera się na dwóch podstawowych wzorcach:
- Synchroniczne (REST/gRPC): Do operacji wymagających natychmiastowej reakcji. Przykład: Usługa kursu wywołuje usługę tożsamości, aby sprawdzić uprawnienia użytkownika przed utworzeniem kursu.
- Asynchroniczny (Kafka): Do operacji, które nie wymagają natychmiastowej reakcji. Przykład: gdy uczeń ukończy lekcję, wydarzenie zostanie opublikowane i wykorzystane niezależnie od Powiadomień, Analiz i Grywalizacji.
Złota zasada: preferuj zdarzenia zamiast wywołań API
Jeśli akcja nie wymaga synchronicznej odpowiedzi, zawsze używaj zdarzenia. Zmniejsza to sprzężenie między usługami, poprawia odporność (awaria konsumenta nie blokuje producenta) i umożliwia odtwarzanie zdarzeń w celu debugowania i odzyskiwania. W LMS idealny stosunek wynosi około 70% zdarzeń / 30% połączeń synchronicznych.
Skalowalność: buforowanie, sharding i CDN
Przedsiębiorstwo LMS musi radzić sobie z dramatycznymi szczytami: pierwszym dniem semestru uniwersyteckiego, dziesiątki tysięcy studentów loguje się jednocześnie. Platforma egzaminacyjna online doświadcza 10-krotnych obciążeń podczas sesji egzaminacyjnych. Bez dobrze zaprojektowanej strategii skalowania, system załamuje się dokładnie wtedy, gdy jest najbardziej potrzebny.
Strategia wielopoziomowego buforowania
| Poziom | Technologia | Dane Cachati | TTL |
|---|---|---|---|
| L1: Przeglądarki | Service Worker + API pamięci podręcznej | Zasoby statyczne, treść lekcji (offline) | 24h (unieważnienie poprzez wersjonowanie) |
| L2: Krawędź CDN | CloudFront/Cloudflare | Filmy HLS, obrazy, CSS/JS, dokumenty PDF | 7 dni (unieważnienie po opublikowaniu) |
| L3: Brama API | Redis/lakier | GET odpowiedzi API (katalog kursów, profil użytkownika) | 5-15 minut |
| L4: Aplikacja | Klaster Redis | Sesje, konfiguracja dzierżawy, postęp uczniów, uprawnienia RBAC | 5-30 minut |
| L5: Bazy danych | Zmaterializowane widoki PostgreSQL | Zbiorcze analizy, tabela wyników, statystyki kursów | Odświeżaj co 5-60 minut |
Sharding za pomocą Citus dla PostgreSQL
W przypadku LMS z milionami użytkowników i wzorcem „współdzielonego schematu” Cytus (Rozszerzenie PostgreSQL
do obliczeń rozproszonych) umożliwia dystrybucję danych pomiędzy wieloma węzłami przy zachowaniu zgodności z SQL.
Naturalna strategia shardingu dla wielodostępnego systemu LMS i partycjonowania tenant_id.
-- Distribuire le tabelle principali per tenant_id
SELECT create_distributed_table('users', 'tenant_id');
SELECT create_distributed_table('courses', 'tenant_id');
SELECT create_distributed_table('enrollments', 'tenant_id');
SELECT create_distributed_table('lesson_progress', 'tenant_id');
SELECT create_distributed_table('submissions', 'tenant_id');
-- Tabelle di riferimento (replicate su ogni nodo)
SELECT create_reference_table('tenants');
-- Query automaticamente distribuite
-- Citus instrada le query al nodo corretto basandosi su tenant_id
SELECT c.title, COUNT(e.id) AS enrolled_students
FROM courses c
JOIN enrollments e ON c.id = e.course_id AND c.tenant_id = e.tenant_id
WHERE c.tenant_id = 'uuid-tenant-mit'
GROUP BY c.title
ORDER BY enrolled_students DESC;
-- Performance: query single-tenant eseguita su un singolo nodo
-- Nessun shuffle di dati tra nodi = latenza minima
Testy wydajności
Poniższe testy porównawcze opierają się na systemie LMS z 5000 najemców i 2 milionami użytkowników ogółem,
wdrożony na AWS z PostgreSQL 16 + Citus, 3 węzły r6g.2xlarge:
| Działanie | Bez Citus (pojedynczy węzeł) | Z Citus (3 węzły) | Poprawa |
|---|---|---|---|
| Lista kursów (na najemcę) | 45 ms | 12 ms | 3,7x |
| Panel studenta | 180 ms | 35 ms | 5,1x |
| Analityka kursu (agregacja) | 2400 ms | 320 ms | 7,5x |
| Seria zapisów (1000 jednocześnie) | Limit czasu (> 5 s) | 89 ms (p99) | >56x |
| Raportowanie między dzierżawcami (administrator) | 12 000 ms | 1800 ms | 6,7x |
Porównanie architektury: prawdziwe platformy LMS
Aby zrozumieć, jak wzorce teoretyczne przekładają się na rzeczywiste wdrożenia, przeanalizujmy architektura głównych platform LMS na rynku.
| Platforma | Architektura | Wielu najemców | Stos techniczny | Skalowalność |
|---|---|---|---|---|
| Moodle'a | Monolit modułowy | Baza danych dla jednego dzierżawcy (MoodleCloud) lub dla jednego dzierżawcy (samodzielnie hostowana). | PHP 8, MySQL/PostgreSQL, zadania cron | Pionowo, ograniczone do ~10 000 użytkowników na instancję bez intensywnego dostrajania |
| Płótno LMS | Monolit z usługami satelitarnymi | Udostępniony schemat z fragmentem według instytucji (klejnot Switchmana) | Ruby on Rails, PostgreSQL, Redis, S3, Consul | Poziomo poprzez sharding, obsługując miliony użytkowników |
| Tablica Dowiedz się Ultra | Mikrousługi (migracja z monolitu Java) | Hybrydowy: baza danych na dzierżawcę (przedsiębiorstwo), schemat współdzielony (SaaS) | Java/Spring Boot, AWS, mikroserwisy, Kafka | Natywne w chmurze automatyczne skalowanie dla poszczególnych usług |
| Otwórz EdX | Zorientowany na usługi (usługi Django) | Instancja na organizację (Tutor) lub w wielu lokalizacjach | Python/Django, MySQL, MongoDB, Seler, RabbitMQ | Skalowanie dla poszczególnych usług w oparciu o Kubernetes |
| Docebo | SaaS natywny w chmurze | Schemat udostępniony z identyfikatorem najemcy + RLS | Zastrzeżone (zakłada się, że Node.js/Go), AWS, natywne dla AI | Automatyczne skalowanie, obsługa ponad 3800 klientów korporacyjnych |
| Kursra | Mikrousługi sterowane zdarzeniami | Natywna obsługa wielu dzierżawców z podziałem na partnerów | Scala, Python, Cassandra, Kafka, Kubernetes | Globalny, ponad 130 milionów użytkowników, wiele regionów |
Lekcje z prawdziwych platform
- Płótno: Sharding za pomocą Switchman gem pokazuje, że dobrze zaprojektowany monolit może być skalowany do milionów użytkowników bez konieczności przepisywania mikrousług.
- Tablica: Migracja z monolitu Java do mikrousług trwała ponad 4 lata i wymagała znacznych inwestycji. Wyciągnięta lekcja: Rozkładaj wcześnie lub wcale.
- Kurs: Architektura sterowana zdarzeniami z Kafką jako szkieletem pozwala na dodawanie nowych funkcji (rekomendacje AI, analityka) bez modyfikowania istniejących usług.
- Otwórz EdX: Podejście oparte na wielu usługach Django pokazuje, że nawet w przypadku „monolitycznego” frameworka można osiągnąć dobrą separację ograniczonych kontekstów.
Przewodnik po wyborze architektury
Nie ma uniwersalnego, najlepszego wzorca. Wybór zależy od profilu produktu, przez przewidywaną liczbę najemców i dostępną kadrę inżynierską. Oto praktyczne drzewo decyzyjne:
Drzewo decyzyjne
- Ilu najemców przewidujesz za 2 lata?
- Mniej niż 50, wszystkie przedsiębiorstwa z umowami niestandardowymi → Baza danych na dzierżawcę
- 50-2000, mix rozmiarów → Schemat na dzierżawcę
- Ponad 2000, głównie SMB → Schemat wspólny + RLS
- Jak ważna jest zgodność?
- FERPA/HIPAA z rygorystyczną ścieżką audytu → Preferuj izolację fizyczną (DB na najemcę)
- Standardowe RODO → Schemat najemcy lub RLS z odpowiednim audytem
- Jak duży jest zespół inżynierów?
- 3-5 programistów → Wspólny schemat (mniejsze koszty operacyjne)
- 10-20 programistów → Schemat dzierżawy lub lekkich mikrousług
- Ponad 20 programistów → Kompletne mikrousługi należące do zespołu
- Mikrousługi czy monolit?
- Mniej niż 50 tys. jednoczesnych użytkowników → Monolit modułowy (a la Canvas)
- 50-500 tys. jednoczesnych użytkowników → Monolith + usługi satelitarne dla mediów i analityki
- Ponad 500 tys. jednoczesnych użytkowników → Kompletne mikrousługi z magistralą zdarzeń
Wnioski i dalsze kroki
Projektowanie skalowalnego systemu LMS dla wielu dzierżawców wymaga decyzji dotyczących architektury, które będą miały wpływ lat na możliwość rozwoju produktu, bezpieczeństwo danych i koszty operacyjne. Nie ma jednego rozwiązania: platformy, które odniosły największy sukces, ewoluowały swoją architekturę z biegiem czasu, często zaczynając od monolitu o wspólnym wzorze i stopniowo się rozkładając najbardziej krytycznych usług.
Kluczowe punkty tego artykułu:
- Wybierz wzorzec z wieloma dzierżawcami na podstawie profilu klienta, a nie moda technologiczna. W przypadku większości systemów LMS SaaS wystarczający jest wspólny schemat + RLS.
- Rozdzielczość najemcy a podstawa bezpieczeństwa. Wdróż go jako niezawodne oprogramowanie pośrednie z buforowaniem i rygorystyczną walidacją.
- Bezpieczeństwo na poziomie wiersza PostgreSQL jest Twoim najlepszym sprzymierzeńcem aby zapewnić izolację na poziomie bazy danych bez nadmiernej złożoności aplikacji.
- Architektura sterowana zdarzeniami jest niezbędna do powiadomień, analiz i wszystkich funkcji czasu rzeczywistego nowoczesnego LMS.
- Citus dla PostgreSQL umożliwia skalowanie w poziomie przy minimalnych zmianach aplikacji, gdy pojedynczy węzeł nie jest już wystarczający.
- Zacznij od modułowego monolitu i rozkładają się na mikrousługi tylko wtedy, gdy wymaga tego skala lub organizacja zespołu.
W następnym artykule z tej serii omówimyedukacyjna architektura strumieniowego przesyłania wideo, rozwiązanie problemu transkodowania adaptacyjnego, DRM dla chronionych treści i bardzo niskie opóźnienia w przypadku zajęć na żywo. Infrastruktura wideo jest często najdroższym i najbardziej złożonym elementem systemu LMS i zasługuje na analizę dokładny.
Zasoby, aby dowiedzieć się więcej
- Blog dotyczący chrupiących danych: „Projektowanie bazy danych Postgres pod kątem wielu najemców” – praktyczny przewodnik po wielodostępności z PostgreSQL
- Dokumentacja Citusa: Samouczek SaaS dla wielu dzierżawców z przykładami z rzeczywistego świata
- Canvas LMS (GitHub): Kod open source z Switchman for Rails + sharding PostgreSQL
- Otwarta architektura edX: Oficjalna dokumentacja architektury usług Open edX
- Wzorce Microsoft Azure dla wielu dzierżawców: Przewodnik dla architektów dotyczący SaaS w chmurze dla wielu dzierżawców







