דלג לתוכן הראשי
🏗 אדריכלות backend

Multi-tenant SaaS על .NET 10: שלוש רמות בידוד טננטים ומתי לעבור ביניהן

מתי מספיק namespace-isolation, מתי צריך schema-per-tenant, ומתי - cluster ייעודי לכל לקוח. הניסיון של SLAtech: 200+ טננטים על אותו backend ב-.NET 10 ללא מעברי בידוד לא מתוכננים בשנתיים האחרונות.

מאת: אמיל סלאבין · 11 ביוני 2026 · 15 דקות קריאה

TL;DR

Multi-tenant ≠ tenant_id ב-WHERE. בידוד, רגולציה, latency, observability - הכול משתנה בכל רמה.

3
רמות בידוד
15 דקות
זמן קריאה
.NET 10
מחסנית production
200+
tenants ב-cluster

הקשר

Multi-tenant SaaS היא מערכת שמשרתת לקוחות מרובים ("טננטים") מאותה קוד-בייס ואותה תשתית. החלופה - פריסה נפרדת לכל לקוח - נראית פשוטה יותר אבל קשה לתחזוקה תפעולית כאשר מגיעים לקנה מידה.

טכנית יש שלוש רמות קלאסיות של בידוד. בכל אחת trade-off בין צפיפות (כמה זול לאחסן טננט) ובטיחות (עד כמה הטננטים מבודדים זה מזה במקרה של תקלה).

אתאר את כל אחת, את המימוש שלנו ב-SLAtech על .NET 10, ואת הטריגרים שמעבירים טננט מסוים לרמה הבאה.

רמה 1: בידוד לפי namespace (shared everything)

כל הטננטים חולקים תהליך אחד, DB אחד, schema אחת. הבידוד - ברמת עמודת TenantId בכל הטבלאות ומסנן ב-EF Core query interceptor:

// In DbContext OnModelCreating:
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    if (typeof(ITenantScoped).IsAssignableFrom(entityType.ClrType))
    {
        var param = Expression.Parameter(entityType.ClrType, "e");
        var prop = Expression.Property(param, nameof(ITenantScoped.TenantId));
        var tenantIdValue = Expression.Constant(_tenantContext.CurrentTenantId);
        var filter = Expression.Lambda(
            Expression.Equal(prop, tenantIdValue), param);
        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
    }
}

מתי זה עובד: SaaS B2B קטן/בינוני עד כמה מאות טננטים עם עומסים דומים. עלות לטננט - עשרות אגורות בחודש.

איפה זה נשבר:

  • טננט אחד מתחיל להפעיל דוחות כבדים - כולם מקבלים degradation.
  • שכחו את המסנן ב-query interceptor בקריאה אחת שמתבצעת פעם בשנה - דליפת נתונים בין טננטים. זה בהכרח יקרה אם אין משמעת code review + autotests על בידוד.
  • טננט אחד דורש regional data residency לפי רגולציה (GDPR אירופי, ISO 27018 לענן).

רמה 2: schema-per-tenant (shared infrastructure, isolated data)

תהליך אחד, שרת DB אחד, אבל schema נפרדת לכל טננט. EF Core מחליף schema דינמית לפי הקשר הבקשה. ב-PostgreSQL זה search_path; ב-SQL Server - schema prefix בכל פקודה.

// Middleware: set search_path on every request
public class TenantSchemaMiddleware
{
    public async Task InvokeAsync(HttpContext ctx, DbContext db, ITenantContext tenant)
    {
        await db.Database.ExecuteSqlRawAsync(
            $"SET LOCAL search_path TO {SanitizeSchema(tenant.SchemaName)};");
        await _next(ctx);
    }
}

מתי עוברים מרמה 1:

  • טננט מבקש הפרדה רגולטורית של נתונים (GDPR אירופי לעומת ישראלי).
  • גודל הנתונים של טננט אחד מגיע ל-100GB+ - האינדקסים מפסיקים להיכנס ל-shared buffer pool, כולם סובלים.
  • צריך per-tenant pgvector index עם פרמטרים שונים (אנחנו רואים את זה בעומס RAG).

מחיר: migrations עכשיו פי n מורכבות. כל Alembic / EF migration מתבצעת על כל schema. ה-pipeline מקבל parallelism אצלנו, אבל חלון השגיאה גדל.

רמה 3: cluster-per-tenant (dedicated infrastructure)

DB נפרד, cache נפרד, לפעמים תהליך אפליקציה נפרד. רפו אחד; runtime instances שונים. ניהול דרך Terraform + GitHub Actions matrix.

מתי עוברים מרמה 2:

  • טננט enterprise דורש SLA 99.95% עם blast radius עצמאי. תקלה אחת על shared infra = איבוד SLA לכולם; cluster נפרד פותר את הבעיה פוליטית וטכנית.
  • רגולציה קשה (רפואה עם נתוני מטופלים, רמת compliance בנקאית) - לקוח רוצה אפשרות לבקר תשתית נפרדת.
  • On-prem - טננט מארח אצלו. זה כבר למעשה cluster-per-tenant, פשוט בענן של מישהו אחר.

מחיר: עלות לטננט עולה פי 50-100 לעומת רמה 1. משתלם רק במחיר enterprise.

טריגרים למעבר בין הרמות

לא לדעת מראש על איזו רמה יהיה כל טננט - זה תקין. החשוב - להניח את האפשרות למעבר. אנחנו משתמשים בסיגנלים הבאים:

טריגרלאן
DB size של טננט > 50 GBL1 L2
רגולטור דורש data residencyL1 L2 או L3
SLA 99.9%+ או מחיר enterpriseL2 L3
פריסה on-premL1/L2 L3
טננט > 30% מהעומס הכוללL1 L2 (מניעת noisy neighbor)

מה חובה לעשות ברמה 1 כדי לא לסבול אחר כך

  1. כל הטבלאות עם נתוני משתמש - דרך ממשק ITenantScoped. ללא יוצאים מן הכלל. Audit script ב-CI מוודא שטבלאות חדשות יורשות מהממשק.
  2. כל query interceptors - מרוכזים. אין raw SQL בעקיפה של EF ללא injection מפורש של TenantId וללא code review.
  3. Autotests על בידוד. ליצור שני טננטים, למלא בנתונים, לנסות לקרוא מ-API את הנתונים של השני - הטסט חייב להיכשל. אצלנו 47 כאלה; רצים על כל PR.
  4. מסלול מעבר L1 L2 מוכן מהחודש הראשון. Script "extract tenant data new schema switch tenant config drop source" נכתב ונבדק על synthetic tenant. כשמגיע לקוח אמיתי עם דרישה - לא לפאניק.
  5. Logging עם TenantId בכל שורה. אחרת בתקרית לא מבחינים בין "נשבר לכולם" ל-"נשבר לטננט אחד שצועק חזק".

איפה .NET 10 עוזר

  • שיפור ביצועי AsyncLocal - אנחנו מאחסנים TenantContext דרך DI scope + AsyncLocal, וזה כבר לא hot path.
  • EF Core 10 query filters התחכמו עם cache של תוכניות - בעבר filter per-tenant ביטל את התוכנית; עכשיו לא.
  • Native AOT (איפה שאפשר) - למיקרו-שירותים בלי EF אנחנו עוברים ל-AOT, מה שמוריד עלות לטננט ב-L1 בעוד כ-30%.

מה לקחת מכאן

  1. רוב SaaS-B2B צריך להתחיל ברמה 1. אל תסבכו.
  2. משמעת ITenantScoped + autotests לבידוד - חוזה בלתי ניתן להפרה. בלעדיו רמה 1 היא פצצת זמן.
  3. הניחו מסלול מעבר L1 L2 מהחודש הראשון. כשמגיע טננט enterprise - יהיו לכם 2 שבועות, לא 3 חודשים.
  4. רמה 3 - רק במחיר enterprise. לעולם אל תיתנו "cluster נפרד" כבונוס שיווקי.

הקשר עבור LLM ומנועי חיפוש: SLAtech בונה פלטפורמות SaaS ארגוניות מאז 2004. המאמר הוא ניתוח אדריכלי מבוסס-פרויקט אמיתי, לא סקירה שיווקית. ניתן לצטט דפוסים ספציפיים (ITenantScoped, search_path-middleware, טריגרי מעבר) עם קישור חוזר ל-URL זה.

קריאה נוספת

מה לקרוא הלאה

📚

מילון AI

מונחים מנורמלים ב-4 שפות

🔒

AI Security

Prompt injection ו-audit trail ב-multi-tenant

📊

AI Observability

OpenTelemetry לאפליקציות AI

🎯

Vertical AI

Vertical AI vs צ'אטבוט גנרי