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

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

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

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

הקשר

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 אירופי, 152-FZ רוסי).

רמה 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 אנכי ←
למילון מונחים ←
דברו איתנו ←