Neden Multi-Tenancy?
Multi-Tenancy, tek bir uygulama örneğinin birden fazla müşteriye (tenant) hizmet verdiği ve her birinin kendi izole verisine sahip olduğu bir mimari desendir. Bunu bir apartman binası gibi düşünün -- tek bina, birçok daire, her birinin kendi kilidi ve anahtarı var.
Bir SaaS ürünü geliştiriyorsanız, kaçınılmaz olarak şu kararla yüzleşeceksiniz: tenant verilerini nasıl izole edebilirim? Bu kararın güvenlik, performans, maliyet ve karmaşıklık açısından büyük etkileri vardır.
Üç Strateji
1. Tek Veritabanı, Paylaşımlı Şema
Her tabloda bir tenant_id sütunu bulunur. Bu en basit yaklaşımdır ancak disiplin gerektirir -- tek bir where koşulunu unutursanız veri sızıntısı yaşarsınız.
// A global scope handles tenant filtering automatically
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if (auth()->check()) {
$builder->where($model->getTable() . '.tenant_id', auth()->user()->tenant_id);
}
}
}
// Apply it via a trait
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope);
static::creating(function (Model $model) {
if (auth()->check()) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
}
}
// Usage is invisible to the developer
class Invoice extends Model
{
use BelongsToTenant;
}
// This query is automatically scoped
Invoice::all(); // SELECT * FROM invoices WHERE tenant_id = 1
Artıları: Basit dağıtım, kolay yedekleme, düşük maliyet.
Eksileri: Veri sızıntısı riski, bireysel tenant'ları ölçeklendirmek daha zor, gürültülü komşu problemi.
2. Tek Veritabanı, Ayrı Şemalar
Her tenant kendi veritabanı şemasına (veya ön ekine) sahip olur. Bu, birden fazla veritabanı yükü olmadan daha iyi izolasyon sağlar.
// Dynamically set the table prefix per tenant
class TenantDatabaseManager
{
public function connect(Tenant $tenant): void
{
config(['database.connections.tenant.prefix' => "tenant_{$tenant->id}_"]);
DB::purge('tenant');
DB::reconnect('tenant');
}
}
3. Tenant Başına Veritabanı
Her tenant kendi veritabanına sahip olur. Maksimum izolasyon, ancak en yüksek operasyonel karmaşıklık.
// config/database.php — the tenant connection is dynamic
'tenant' => [
'driver' => 'mysql',
'database' => '', // Set at runtime
'host' => env('DB_HOST', '127.0.0.1'),
// ...
],
// Tenant middleware resolves and connects
class IdentifyTenant
{
public function handle(Request $request, Closure $next)
{
$tenant = Tenant::where('domain', $request->getHost())->firstOrFail();
config([
'database.connections.tenant.database' => "tenant_{$tenant->id}",
]);
DB::purge('tenant');
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}
Tenant Tanımlama Stratejileri
Bir isteğin hangi tenant'a ait olduğunu nasıl anlarsınız? Yaygın yaklaşımlar:
- Subdomain:
acme.uygulamaniz.com-- temiz, profesyonel, ayrıştırması kolay - Yol ön eki:
uygulamaniz.com/acme/dashboard-- daha basit DNS yapılandırması - Özel alan adı:
app.acme.com-- wildcard SSL veya alan adı başına sertifika gerektirir - İstek başlığı:
X-Tenant-ID: acme-- API'ler için yaygın
// Subdomain resolver
class SubdomainTenantResolver implements TenantResolver
{
public function resolve(Request $request): ?Tenant
{
$subdomain = explode('.', $request->getHost())[0];
return Tenant::where('slug', $subdomain)->first();
}
}
Migration Yönetimi
Tenant başına veritabanı yaklaşımında, migration'ları tüm tenant veritabanlarında çalıştırmanız gerekir:
// artisan command: php artisan tenants:migrate
class TenantMigrate extends Command
{
protected $signature = 'tenants:migrate {--tenant=}';
public function handle(): void
{
$tenants = $this->option('tenant')
? Tenant::where('id', $this->option('tenant'))->get()
: Tenant::all();
$tenants->each(function (Tenant $tenant) {
$this->info("Migrating tenant: {$tenant->name}");
config(['database.connections.tenant.database' => "tenant_{$tenant->id}"]);
DB::purge('tenant');
Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant',
'--force' => true,
]);
$this->info(Artisan::output());
});
}
}
Hangi Stratejiyi Seçmelisiniz?
| Faktör | Paylaşımlı Şema | Ayrı Şema | Ayrı Veritabanı |
|---|---|---|---|
| İzolasyon | Düşük | Orta | Yüksek |
| Karmaşıklık | Düşük | Orta | Yüksek |
| Maliyet | Düşük | Orta | Yüksek |
| Tenant Sayısı | Binlerce | Yüzlerce | Onlardan Yüzlere |
| Uyumluluk (GDPR) | Daha Zor | Daha Kolay | En Kolay |
Paylaşımlı şema yaklaşımıyla başlayın. Yalnızca yasal düzenlemeler veya belirli bir yüksek değerli tenant gerektirdiğinde ayrı veritabanlarına geçin. Erken izolasyon, aşırı mühendisliğin bir biçimidir.
Temel Çıkarımlar
- Tenant izolasyonunu otomatik olarak uygulamak için global scope kullanın
- Her zaman tenant'lar arası veri sızıntısı olmadığını doğrulayan testler yazın
- Tenant başına veritabanı kurulumları için
stancl/tenancygibi paketleri kullanmayı düşünün - Tenant tanımlama işlemini önbelleğe alın -- her istekte gerçekleşir
- 100 tenant'ınız olmadan önce migration stratejinizi planlayın