Bir üretim ERP’sinde çalışırken, genellikle karşımıza çıkan temel mimari kararlardan biri multi-tenant yapısı oluyor. Birden fazla müşteriye veya şirket içi farklı departmana aynı yazılım ve altyapı üzerinden hizmet vermek istediğimizde, bu model kaçınılmaz hale geliyor. Benim deneyimimde, bu mimariyi tasarlarken sadece teknik detaylara değil, aynı zamanda organizasyonel akışlara ve iş gereksinimlerine de odaklanmam gerekti.
Bu yazıda, multi-tenant bir ERP sistemini hayata geçirirken veri izolasyonundan performansa, güvenlikten operasyonel süreçlere kadar karşılaştığım zorlukları ve uyguladığım çözümleri anlatacağım. Amacım, bu karmaşık yapının anatomisini kendi pratik tecrübelerimle gözler önüne sermek.
Veri İzolasyonu Modelleri ve Zorlukları
Multi-tenant mimarilerde en kritik konulardan biri, farklı kiracıların (tenant) verilerinin birbirine karışmamasını sağlamak, yani veri izolasyonu. Ben bu konuda üç ana model üzerinde çalıştım ve her birinin kendine özgü avantajları ve dezavantajları olduğunu gördüm. Seçimimi yaparken maliyet, karmaşıklık ve performans gibi faktörleri dengelemem gerekti.
İlk model, her kiracı için ayrı bir veritabanı kullanmaktı. Bu, en yüksek izolasyonu sağlayan yöntem; hassas finansal veri taşıyan ortamlarda bu izolasyon seviyesi tercih sebebi oluyor. Ancak bu yaklaşım, kiracı sayısı arttıkça operasyonel maliyetleri ve yönetim karmaşıklığını ciddi şekilde artırıyor. Veritabanı upgrade’leri veya schema değişiklikleri, her veritabanı için ayrı ayrı yapılmak zorunda kalıyordu ve bu da hatırı sayılır bir iş yükü demekti.
İkinci model, her kiracı için ayrı bir schema kullanmaktı. PostgreSQL’de CREATE SCHEMA tenant_x; komutuyla bunu kolayca yapabiliyorsunuz. Bu yaklaşım, tek bir veritabanı sunucusunda çalışarak kaynak kullanımını optimize ederken, tabloların isimlendirme çakışmalarını da engelliyordu. Bir üretim ERP’sinde, orta ölçekli kiracılar için bu modeli kullandık. Ancak burada da karmaşıklıklar vardı. pg_dump alırken veya cross-tenant sorgular yazarken (örneğin, “tüm kiracılar genelinde envanter raporu”) işler karışıyordu. Schema’lar arasında join yapmak veya arama yapmak için dinamik SQL yazmak zorunda kaldım, bu da kod karmaşıklığını artırdı.
Son olarak, en yaygın ve benim de en çok kullandığım model, paylaşımlı schema içinde tenant_id sütunu kullanmaktı. Her tabloda bir tenant_id alanı bulundurarak verileri filtreliyorsunuz. Bu en esnek ve kaynak açısından en verimli yoldu, ancak geliştiricilerin her sorguda bu tenant_id filtresini doğru bir şekilde uygulaması gerekiyordu. Küçük bir hata, bir kiracının diğerinin verisini görmesiyle sonuçlanabilirdi. Bu riski azaltmak için, ORM katmanında global bir tenant_id filtresi uygulamayı denedim ve tüm sorguların otomatik olarak bu filtreden geçmesini sağladım.
-- Paylaşımlı schema, tenant_id sütunu ile veri izolasyonu
SELECT * FROM orders
WHERE tenant_id = 'my_tenant_uuid' AND order_status = 'processing';
-- Ayrı schema modeli (PostgreSQL)
SET search_path TO tenant_x, public;
SELECT * FROM orders WHERE order_status = 'processing';
Benim pratiğimde, tenant_id sütunu ile paylaşımlı schema en pratik çözüm oldu. Veritabanı yönetimi basitti, tek bir schema üzerinde çalışıyordum. Ancak geliştirici disiplini ve ORM katmanında güçlü bir filtreleme mekanizması şarttı. PostgreSQL’in row-level security (RLS) özelliğini kullanarak bu disiplini veritabanı seviyesinde zorlamayı da düşündüm, ancak başlangıçta getirdiği ek karmaşıklık nedeniyle daha çok uygulama katmanındaki filtrelemeye odaklandım.
Ortak Altyapı ve Kaynak Paylaşımı
Multi-tenant mimarinin özünde, birden fazla kiracının aynı fiziksel veya sanal altyapıyı paylaşması yatar. Bu, sunucu altyapılarından ağ bileşenlerine kadar birçok katmanda kaynak paylaşımı anlamına geliyor. Benim tecrübemde, bu paylaşımın getirdiği en büyük zorluklardan biri “gürültülü komşu” (noisy neighbor) sendromuydu. Bir kiracının yoğun kaynak kullanımı, diğer kiracıların performansını olumsuz etkileyebiliyordu.
Uygulama sunucularında, özellikle FastAPI tabanlı bir üretim ERP’si üzerinde çalışırken, her isteğin hangi kiracıdan geldiğini belirleyip o kiracının kaynak kullanımını izlemek önemliydi. Bunu, Nginx gibi bir reverse proxy kullanarak belirli Host başlıklarına veya URL yollarına göre istekleri doğru uygulama sunucularına yönlendirerek çözdüm. Daha da önemlisi, uygulama sunucularını Docker konteynerlerinde çalıştırdığımda, Linux cgroup limitlerini kullanarak her konteynere belirli CPU ve bellek sınırları atadım. Örneğin, bir kiracının yoğun raporlama işi, diğer kiracıların API yanıt sürelerini etkilemesin diye, raporlama servisinin çalıştığı konteynerin bellek kullanımını memory.high değeriyle yumuşak bir limit altına aldım. Bu, sistemin genel stabilitesi için kritikti.
# Docker Compose ile cgroup memory limit belirleme
services:
app-service:
image: my-erp-app
deploy:
resources:
limits:
memory: 512M # Sert limit
reservations:
memory: 256M # Yumuşak limit
Veritabanı tarafında, PostgreSQL connection pool’u yönetmek çok önemliydi. Her kiracının kendi bağlantı havuzunu açması yerine, PgBouncer gibi bir connection pooler kullanarak veritabanı bağlantılarını merkezi olarak yönettik. Bu, hem veritabanı üzerindeki yükü azalttı hem de bağlantıların verimli kullanılmasını sağladı. Benim gözlemim, doğru yapılandırılmamış bir connection pool’un, yoğun anlarda sunucu yanıt sürelerini belirgin şekilde artırabildiği yönündeydi. Redis gibi caching katmanlarında ise, her kiracı için ayrı bir prefix kullanarak cache verilerinin karışmasını engelledim. tenant_id:user_session:token gibi key paternleri kullandım.
Ağ katmanında, farklı kiracıların trafiklerini izole etmek için VLAN segmentasyonunu kullandım. Farklı departmanların (lojistik, finans, satış) kendi VLAN’ları üzerinden ayrı güvenlik politikalarıyla iletişim kurması, kabaca multi-tenant izolasyonun ağ seviyesindeki karşılığı. Multi-tenant ERP’de de benzer şekilde, hassas veriye erişen servisleri ayrı bir network segmentine taşıyarak egress kontrolleri uyguladım. Bu, ZTNA (Zero Trust Network Access) prensiplerinin bir yansımasıydı; hiçbir servise varsayılan olarak güvenmeyip, her bağlantıyı doğrulamak esastı.
Güvenlik ve Multi-Tenant Ortamlar
Multi-tenant mimarilerde güvenlik, sadece veri izolasyonundan ibaret değildir; aynı zamanda kimlik doğrulama (authentication), yetkilendirme (authorization) ve olası veri sızıntılarını engelleme gibi geniş bir alanı kapsar. Bir kiracının diğer bir kiracının verisine yanlışlıkla veya kötü niyetle erişmesini engellemek, en büyük güvenlik endişelerimden biriydi.
Kimlik doğrulama tarafında, JWT (JSON Web Tokens) ve OAuth2 paternlerini kullandım. Her kiracının kendi kullanıcı havuzu vardı ve kullanıcılar sisteme giriş yaptığında, JWT token’ı içinde kullanıcının tenant_id’si ve yetkileri bulunuyordu. Bu token, her API isteğiyle birlikte gönderiliyor ve uygulama katmanında doğrulanıyordu. Bu sayede, yanlış bir tenant_id’ye sahip bir istek, daha uygulama katmanına ulaşmadan reddediliyordu.
// Örnek bir JWT payload'u
{
"sub": "user123",
"name": "Mustafa Erbay",
"tenant_id": "my_production_company_uuid",
"roles": ["admin", "production_planner"],
"exp": 1789123456
}
Yetkilendirme mekanizması ise, tenant_id filtresi üzerine inşa edildi. Daha önce bahsettiğim gibi, tüm veritabanı sorgularının tenant_id filtresi içermesi zorunluydu. Bu, ORM katmanında zorlanıyordu, ancak olası bypass senaryolarına karşı veritabanı seviyesinde de bazı kontroller düşündüm. PostgreSQL’in Row-Level Security (RLS) özelliği, belirli rollerin veya kullanıcıların yalnızca kendi tenant_id’lerine ait satırları görmesini sağlayarak ek bir güvenlik katmanı sunuyordu. Ancak, RLS’in performansa etkisi ve sorgu planlamasını karmaşıklaştırma potansiyeli nedeniyle, bunu daha çok kritik tablolar için bir yedek mekanizma olarak değerlendirdim.
DDoS mitigation ve rate limiting de önemliydi. Bir kiracının API’ye aşırı yüklenmesi, diğer kiracıların hizmetini etkileyebilirdi. Nginx üzerinde limit_req modülünü kullanarak IP bazlı veya kullanıcı token’ı bazlı rate limiting uyguladım. Ayrıca, fail2ban gibi araçlarla başarısız giriş denemelerini izleyip ilgili IP adreslerini geçici olarak engelledim. Kernel module blacklist gibi daha alt seviye güvenlik önlemleri, paylaşımlı sunucunun genel güvenliğini artırmak için kullanıldı; örneğin, algif_aead gibi potansiyel zafiyet içeren modülleri blacklist’e alarak genel sistem güvenliğini artırdım.
Son olarak, audit subsystem (auditd) kullanarak tüm sistem çağrılarını ve dosya erişimlerini kaydettim. Bu, herhangi bir güvenlik ihlali durumunda geriye dönük inceleme yapmama olanak tanıdı ve hangi kullanıcının hangi tenant_id bağlamında ne zaman ne yaptığını anlamamı sağladı.
Performans Optimizasyonları ve Tuhaf Köşe Durumlar
Multi-tenant mimarilerde performans optimizasyonu, tek-kiracılı sistemlere göre çok daha katmanlı ve karmaşıktır. Paylaşılan kaynaklar nedeniyle oluşan darboğazları tespit etmek ve gidermek, sürekli bir mücadeleydi. Bir üretim ERP’sinde, kullanıcıların anlık raporlar ve operatör ekranları üzerinde gecikme yaşamaması için ince ayarlar yapmam gerekiyordu.
Veritabanı tarafında, PostgreSQL indeks stratejileri hayati rol oynadı. Her tabloda tenant_id sütunu olduğu için, bu sütun üzerine doğru indeksleri atmak kritikti. Genellikle (tenant_id, other_column) şeklinde bileşik indeksler kullandım. Örneğin, bir orders tablosunda (tenant_id, order_date) veya (tenant_id, customer_id) indeksleri, belirli bir kiracının siparişlerini tarih veya müşteri bazında sorgularken performansı önemli ölçüde artırdı. EXPLAIN ANALYZE çıktılarını düzenli olarak inceleyerek, sorgu planlayıcının doğru indeksi kullandığından emin oldum. Bir keresinde, tenant_id’nin VARCHAR olarak tanımlandığı bir tabloda, TEXT ile sorgu yapmaya çalışmam nedeniyle indeksin kullanılmadığını fark etmiştim; tip uyumsuzlukları bu tarz tuhaf köşe durumlara yol açabiliyor.
Özellikle real-time dashboard’lar ve anlık veri analizi gerektiren operatör ekranları için Redis caching’i yoğun bir şekilde kullandım. Her kiracının verisinin izole olduğundan emin olmak için Redis key’lerinde tenant_id’yi prefix olarak kullandım. Örneğin, tenant_uuid:dashboard:open_orders gibi. Redis’in maxmemory-policy ayarını da allkeys-lru olarak belirledim ki, cache dolduğunda en az kullanılan anahtarlar atılsın ve önemli veriler korunabilsin. Bir keresinde, Redis OOM eviction policy seçimim yanlış olduğu için, kritik önbellek verilerinin erken atıldığını ve bu yüzden performansta ani düşüşler yaşadığımızı gördüm. Bu tarz ayarların doğru yapılması, sistemin stabilliği için şart.
Uygulama katmanında, ORM trap’leri (özellikle N+1 sorgu problemi) multi-tenant sistemlerde daha da yıkıcı olabiliyor. Bir üretim ERP’sinde, ürün ağacı (BOM) detaylarını çekerken, her bir alt bileşen için ayrı bir sorgu yapılmasının tek bir rapor isteğinde sorgu sayısını patlattığını fark ettim. Bu durum, birkaç kullanıcı aynı anda bu raporu çektiğinde veritabanının kilitlenmesine neden oluyordu. Çözüm olarak, ORM’in eager-loading mekanizmalarını doğru kullanarak veya elle optimize edilmiş JOIN’ler yazarak sorgu sayısını dramatik bir şekilde azalttım. Bu tür optimizasyonlar, genellikle test ortamında ortaya çıkmayıp, ancak production’da yoğun yük altında kendini gösteriyor. Benim deneyimimde, bu tarz regresyonları tespit etmek için sürekli performans monitoring araçları kullanmam gerekti.
Dağıtım ve Operasyonel Zorluklar
Multi-tenant bir sistemin dağıtımı ve operasyonel yönetimi, tek-kiracılı bir uygulamaya göre katlanarak artan bir karmaşıklığa sahiptir. Ben bir üretim ERP’sinde çalışırken, yeni özelliklerin devreye alınmasından hata düzeltmelerine kadar her adımda bu karmaşıklıkla yüzleştim.
CI/CD pipeline’larının reliability’si burada çok önemli hale geliyor. Her deployment’ın birden fazla kiracıyı etkileme potansiyeli olduğu için, otomasyonun hatasız çalışması şarttı. Blue-green deployment stratejilerini kullanarak yeni bir versiyonu canlıya almadan önce, yeni ortamı tamamen test etme ve herhangi bir sorun anında eski versiyona hızla geri dönme imkanı buldum. Bu, örneğin bir disk doluluğu nedeniyle gece yarısı acil bir rollback yapmamız gerektiğinde hayat kurtarıcı oldu; WAL rotation alarmının düşmesiyle hemen müdahale edebildim. Otomatik rollback mekanizmaları olmadan bu kadar hızlı toparlanmamız mümkün olmazdı.
Şema migrasyonları da önemli bir baş ağrısıydı. Paylaşımlı bir veritabanı schema’sında, bir tabloya yeni bir sütun eklemek veya mevcut bir sütunu değiştirmek, tüm kiracıları etkilediği için çok dikkatli olmayı gerektiriyordu. ALTER TABLE komutlarının LOCK mekanizmalarını ve NOT NULL kısıtlamalarının nasıl ekleneceğini iyi anlamak şarttı. Örneğin, yeni bir NOT NULL sütunu eklerken, önce NULL olarak ekleyip, verileri doldurup, sonra NOT NULL kısıtlamasını ekleyerek uzun süreli tablo kilitlenmelerini engelledim. pt-online-schema-change gibi araçlar olmasa da, kendi yazdığım script’lerle benzer online şema değişiklikleri yapabiliyordum.
Observability, yani sistemin metriklerini, loglarını ve trace’lerini izlemek, multi-tenant ortamlarda daha da kritik. Prometheus ve Grafana kullanarak her kiracının kendi performans metriklerini (API yanıt süreleri, veritabanı sorgu süreleri) ayrı ayrı izledim. Logları merkezi bir sistemde toplayıp tenant_id’ye göre filtreleyebilmek, sorun giderme sürecini inanılmaz hızlandırdı. Bir kiracının “geç sevkiyat raporu hep eksik geliyordu” şikayetini ele alırken, ilgili logları tenant_id filtresiyle arayarak sorunun kök nedenini (bir entegrasyon servisinin timeout olması) kısa sürede bulabildim. SLO (Service Level Objectives) ve error budget yönetimi de, her kiracıya belirli bir hizmet kalitesi sunabilmek için olmazsa olmazdı.
Gelecek ve Mimarideki Gelişmeler
Multi-tenant mimariler sürekli gelişiyor ve benim de bu alandaki düşüncelerim ve deneyimlerim zamanla evriliyor. Özellikle son dönemde yapay zeka ve sunucusuz mimarilerin yükselişi, bu alandaki yaklaşımlarımı yeniden şekillendirmeme neden oldu.
Sunucusuz (serverless) mimariler, özellikle AWS Lambda veya Google Cloud Functions gibi platformlar, multi-tenancy için doğal bir uyum sağlıyor. Her isteğin ayrı bir fonksiyon çağrısı olarak ele alınması ve kaynakların otomatik olarak ölçeklenmesi, gürültülü komşu problemini büyük ölçüde hafifletiyor. Kendi yan ürünümün backend’inde bazı microservice’leri sunucusuz olarak tasarlayarak, her kiracının yüküne göre ayrı ayrı ölçeklenebilen, maliyet açısından daha verimli bir yapı kurdum. Bu, geleneksel VM tabanlı yaklaşımlara göre çok daha esnek ve yönetim açısından daha az yük getiriyor.
Yapay zeka modellerinin multi-tenant ERP’lere entegrasyonu da ilgi alanlarımdan biri. Bir üretim firmasının ERP’sinde, AI ile üretim planlama modülü geliştirirken, her kiracının kendi verisiyle eğitilmiş veya özelleştirilmiş bir prompt engineering stratejisi kullanarak üretim planlarını optimize etmeyi denedim. RAG (Retrieval-Augmented Generation) paternleri, her kiracının kendi dokümanlarını veya geçmiş verilerini kullanarak AI modelinin daha doğru yanıtlar üretmesini sağlıyor. Gemini Flash, Groq veya Cerebras gibi farklı AI sağlayıcılarını OpenRouter üzerinden çoklu provider fallback ile kullanarak, her kiracının ihtiyacına göre en uygun maliyetli veya en hızlı modeli dinamik olarak seçebiliyorum.
Zero-trust mimarisi prensipleri, multi-tenant sistemlerde iç ağ güvenliği için de giderek daha fazla önem kazanıyor. Şirket segmentasyonu ve ZTNA egress kontrol, sadece dış tehditlere karşı değil, aynı zamanda kiracılar arası olası yan geçişleri engellemek için de kritik. Hiçbir iç servisin diğerine varsayılan olarak güvenmediği, her bağlantının kimlik ve yetkilendirme kontrolünden geçtiği bir model, gelecekteki ERP mimarilerinin temelini oluşturacak. Örneğin, bir üretim hattı operatör ekranının sadece kendi tenant_id’sine ait üretim verilerine eriştiğinden emin olmak için, her API çağrısını ZTNA proxy’sinden geçirip kimliğini doğrulatıyorum.
Sonuç
Multi-tenant mimarisi, ERP sistemleri gibi karmaşık kurumsal uygulamalar için kaçınılmaz bir tasarım tercihi. Ancak bu yapı, beraberinde veri izolasyonu, kaynak paylaşımı, güvenlik, performans ve operasyonel yönetim gibi birçok zorluğu getiriyor. Benim yirmi yıllık saha tecrübemde gördüğüm şey, bu zorlukların üstesinden gelmek için sadece teknik bilgiye değil, aynı zamanda pragmatik bir yaklaşıma ve sürekli öğrenmeye ihtiyaç duyulduğu.
Her kararın bir trade-off’u var ve en iyi çözüm, projenin özel gereksinimlerine, bütçesine ve ekibin yetkinliklerine göre değişiyor. Bir üretim ERP’sinde tenant_id bazlı veri izolasyonunu tercih ederken, izolasyon gereksinimi çok yüksek bir ortamda ayrı veritabanlarına yönelmek gibi. Önemli olan, bu trade-off’ları bilinçli bir şekilde yapmak ve olası riskleri önceden öngörebilmek. Bu alandaki deneyimlerim, bana mimarinin sadece koddan ibaret olmadığını, büyük ölçüde organizasyonel akışların ve insan faktörünün bir yansıması olduğunu öğretti.
Sonraki adım: Multi-tenant sistemlerde event-sourcing ve CQRS paternlerinin nasıl uygulanabileceğini kendi deneyimlerimle anlatacağım.