Microservislerden Modüler Monolite Geçiş
Dağıtık monolitleri tanıma, stratejik servis konsolidasyonu ve karmaşıklık sürdürülemez olunca modüler monolite geri dönüşün gerçekleri.
Microservis mimarisi dağıtık monolit haline geldiğinde ekipler, kaçınmaya çalıştıkları sorunların tam ortasında kalır. Deployment coupling, veri tutarlılığı zorlukları ve koordinasyon yükü bunun görünen belirtileri.
Bu servisleri modüler monolite konsolide etmek geçerli bir mimari pattern. Yaklaşık 50 mühendis ve altındaki ekipler için makul varsayılan, ürün takımı başına iyi yapılandırılmış tek bir deployable. Bir modülü ancak ölçülebilir bir ihtiyaç operasyonel maliyeti karşıladığında servis olarak ayır.
Sepete Ürün Eklemek 47 Servise Dokunduğunda#
Ürün sayfasından ödemeye giden yolda 47 microservise ulaşmış bir e-ticaret platformu düşün. Her servisin kendi veritabanı, deployment pipeline’ı ve nöbet rotasyonu var. Tek bir satın alma işlemi 12 farklı takımın koordinasyonunu gerektiriyor.
Mimari diyagram sunumlarda etkileyici görünür. Gerçekte ise feature geliştirmekten çok servisler arası iletişimi debug etmek zaman alır. “Loosely coupled” servisler bağımsız deploy edilemez çünkü bir API’yi değiştirmek beş takımla koordinasyon demektir; bu da klasik dağıtık monolit belirtisidir.
Microservis Bozulma Kalıpları#
Microservislerin teknik borca dönüştüğüne dair tutarlı uyarı işaretleri:
Deployment Sıralama Sorunu#
# Deployment "orkestrasyon"unuz böyle görünüyor
deploy-order:
- auth-service # İlk deploy edilmeli
- user-service # Auth değişikliklerine bağımlı
- profile-service # Yeni user alanlarına ihtiyaç duyar
- order-service # Profile güncellemelerini gerektirir
- inventory-service # Order değişikliklerine ihtiyaç duyar
- payment-service # Yukarıdakilerin hepsine bağımlı
# ... Belirli sırada 41 servis daha
Bu sabit deploy sırası, deployment coupling’in somut hâlidir: hiçbir servis, üstündekiler önce gitmeden kendi takvimiyle deploy edilemez.
Beş Servis, Bir Başarısız Sipariş#
// Temiz servis sınırları olarak başlayan...
class OrderService {
async createOrder(orderData: OrderRequest) {
// Dağıtık transaction cehennemine dönüştü
const user = await this.userService.getUser(orderData.userId);
const inventory = await this.inventoryService.checkStock(orderData.items);
const pricing = await this.pricingService.calculateTotal(orderData);
const payment = await this.paymentService.authorize(pricing.total);
// Şimdi ortada hiçbir şey başarısız olmaması için dua et
try {
const order = await this.saveOrder(orderData);
await this.inventoryService.reserve(orderData.items);
await this.paymentService.capture(payment.id);
// Bu başarısız olursa ne olur?
await this.emailService.sendConfirmation(order);
return order;
} catch (error) {
// Tüm bunları tutarlı şekilde geri almak için bol şans
await this.attemptDistributedRollback(error);
}
}
}
Bir sonraki adım genellikle dağıtık bir transaction koordinatörüdür ve bu koordinatör her servisin bağımlı olduğu bir bileşene dönüşür.
Konsolidasyon Stratejisi#
47 servisi 3 modüler monolite indirmek bir karar framework’üyle başlar:
Servis Konsolidasyon Karar Matrisi#
interface ConsolidationCandidate {
services: string[];
criteria: {
sharedDataModel: boolean; // Aynı kavramsal veri mi?
teamOwnership: string; // Aynı takım mı sahip?
deploymentCoupling: number; // Ne sıklıkla birlikte deploy ediliyor?
communicationVolume: number; // Aralarında dakikada kaç çağrı?
transactionBoundary: boolean; // ACID garantileri gerekli mi?
};
consolidationScore(): number {
// Skor > 0.7 ise, güçlü konsolidasyon adayı
return (
(this.criteria.sharedDataModel ? 0.3 : 0) +
(this.criteria.teamOwnership ? 0.2 : 0) +
(this.criteria.deploymentCoupling > 0.8 ? 0.2 : 0) +
(this.criteria.communicationVolume > 100 ? 0.2 : 0) +
(this.criteria.transactionBoundary ? 0.3 : 0)
);
}
}
Konsolide Uygulama#
47 servis yerine, net iç sınırlara sahip 3 modüler monolit:
// Tek deployable, birden fazla modül
class ECommerceApplication {
// Net sınırlara sahip modüller
private modules = {
user: new UserModule(this.sharedDb),
order: new OrderModule(this.sharedDb),
inventory: new InventoryModule(this.sharedDb),
payment: new PaymentModule(this.sharedDb)
};
// Her modüle enjekte edilen paylaşılan altyapı
private sharedDb = new DatabaseConnection();
private cache = new RedisCache();
async processOrder(request: OrderRequest) {
// Tüm modüller için tek bir ACID transaction
return await this.sharedDb.transaction(async (tx) => {
const user = await this.modules.user.validateUser(request.userId, tx);
const items = await this.modules.inventory.reserveItems(request.items, tx);
const payment = await this.modules.payment.processPayment(request.payment, tx);
const order = await this.modules.order.createOrder(user, items, payment, tx);
// Her şey birlikte commit olur veya rollback yapar
return order;
});
}
}
Deployment, onlarca pipeline üzerinde sıralı bir orkestrasyon olmaktan çıkıp tek bir blue-green rollout’a döner. Daha önemlisi, sipariş akışı tek bir transaction sınırı kazanır: yarıda kalan bir işlem dört ayrı veritabanına yayılmış yarım kayıtlar bırakmak yerine temiz biçimde geri alınır.
Konsolidasyonu Asıl Tetikleyen Durumlar#
Küçük Takım, Dev Şirket Diyagramı#
En sık görülen tetikleyici, servis sayısının takım sayısından bağımsız biçimde her feature’la birlikte artması. Her yeni yetenek kendi servisini alır, çünkü yayımlanmış referans mimariler böyle görünür. O mimariler binlerce mühendis çalıştıran şirketlerden çıkar; otuz kişilik bir ekip aynı operasyonel yüzeyi onu işletecek insan olmadan devralır.
Hasar, çok sayıda sınırı geçen akışlarda ortaya çıkar. Sekiz servise dokunan bir kayıt akışı tek parça halinde başarısız olur: payment çağrısı timeout aldığında akış yarıda kesilir ve geride yarı oluşturulmuş bir kullanıcı kalır, çünkü servisleri kapsayan bir transaction yoktur.
Uygulanabilir bir konsolidasyon hedefi, birkaç domain odaklı deployable:
- Identity: kullanıcılar, auth, profiller, izinler
- Transactions: ödemeler, siparişler, faturalama, mutabakat
- Product: katalog, fiyatlama, envanter, öneriler
- Communication: email, SMS, push bildirimleri, webhook’lar
Her biri işin veriyi yazma biçimine karşılık gelen bir transaction sınırına sahip olduğu için kısmi hatalar temiz biçimde rollback’e döner.
Servisler Arası Raporlama#
Raporlama farklı bir şekilde bozulur. Analitik ve uyumluluk sorguları servis sınırlarına saygı duymaz; tek bir rapor onlarca servise dağılır ve bu dağılımın herhangi bir yerindeki timeout tüm çalışmayı düşürür. Tekrar denemesi her şeyi baştan okur.
Kodu konsolide etmeden önce veriyi konsolide etmek, servislere dokunmadan bu sorunu çözer. Tek bir veritabanı içindeki domain şemaları join yeteneğini geri getirir:
-- Ayrı veritabanları yerine domain şemaları
CREATE SCHEMA customer_domain;
CREATE SCHEMA product_domain;
CREATE SCHEMA order_domain;
CREATE SCHEMA compliance_domain;
-- İlgili tabloları domain şemalarına taşı
ALTER TABLE users SET SCHEMA customer_domain;
ALTER TABLE profiles SET SCHEMA customer_domain;
ALTER TABLE preferences SET SCHEMA customer_domain;
-- Uyumluluk raporları basit join'lere dönüşür
SELECT
c.user_id,
c.registration_date,
o.total_orders,
o.total_revenue,
p.product_categories
FROM customer_domain.users c
JOIN order_domain.order_summary o ON c.user_id = o.user_id
JOIN product_domain.user_products p ON c.user_id = p.user_id
WHERE c.registration_date >= '2024-01-01';
-- Servislere dağılan çağrılar yerine tek bir sorgu planı
Veri tek bir motorda toplandığında raporlama, veritabanının optimize edebileceği bir sorgu planına döner. Servislerin kendisinin sonradan birleştirilip birleştirilmeyeceği ayrı bir karardır; ölçütü deployment coupling’dir.
Gecikmeye Duyarlı Yollar#
Gecikme bütçesi dar olduğunda durum farklıdır. Bir istek yolunu beş servise bölmek her sıçrama başına bir network gidiş-dönüşü ekler ve kısa yollarda her sıçrama, sardığı işten daha pahalıya mal olur. Bütçesi tek haneli milisaniye olan bir trade execution yolunda bu sıçramalar bütçenin tamamını yer:
// Önce: Network overhead'li microservisler
class TradingSystemDistributed {
async executeTrade(order: Order) {
// Her çağrı 10-20ms latency ekliyor
const validation = await this.validationService.validate(order); // +15ms
const pricing = await this.pricingService.getPrice(order); // +12ms
const risk = await this.riskService.checkLimits(order); // +18ms
const execution = await this.executionService.execute(order); // +14ms
const settlement = await this.settlementService.settle(order); // +16ms
// Toplam: Ortalama 75ms latency
}
}
// Sonra: Shared memory'li monolit
class TradingSystemMonolithic {
async executeTrade(order: Order) {
// Her şey in-process ve shared memory ile
const validation = this.validateOrder(order); // <1ms
const pricing = this.calculatePrice(order); // <1ms
const risk = this.checkRiskLimits(order); // <1ms
const execution = this.executeOrder(order); // <1ms
const settlement = this.settleOrder(order); // <1ms
// Toplam: <5ms latency
}
}
In-process sürüm, her adım arasındaki serileştirme ve ağ transferi bedelini ödemeden aynı işi yapar.
Migration Stratejileri#
Strangler Fig Pattern (Tersten)#
Bu pattern’in ters versiyonu, trafiği eski microservislerden alıp konsolide monolite taşır:
class ConsolidationProxy {
private legacyServices = new Map<string, MicroserviceClient>();
private consolidatedHandlers = new Map<string, Handler>();
async handleRequest(request: Request): Promise<Response> {
const feature = this.extractFeature(request);
// Trafiği yavaş yavaş konsolide versiyona taşı
if (this.shouldUseConsolidated(feature)) {
return await this.consolidatedHandlers.get(feature)!(request);
}
// Legacy microservice'e geri dön
return await this.legacyServices.get(feature)!.call(request);
}
private shouldUseConsolidated(feature: string): boolean {
// %10 trafikle başla, yavaş yavaş artır
const rolloutPercentage = this.getRolloutPercentage(feature);
return Math.random() < rolloutPercentage;
}
}
Her seferinde bir business capability taşı ve her adımda hata oranlarını ve gecikmeyi izle. Konsolide handler gerilerse rollout yüzdesi deploy gerektirmeden sıfıra iner.
Gözyaşsız Veritabanı Konsolidasyonu#
Konsolidasyonun en korkutucu kısmı genellikle veritabanlarını birleştirmektir. İşe yarayan pattern:
-- Adım 1: Konsolide veritabanında domain şemaları oluştur
CREATE SCHEMA user_domain;
CREATE SCHEMA order_domain;
CREATE SCHEMA inventory_domain;
-- Adım 2: Microservis DB'lerinden logical replication kur
CREATE PUBLICATION user_pub FOR ALL TABLES;
CREATE SUBSCRIPTION user_sub
CONNECTION 'host=user-service-db dbname=users'
PUBLICATION user_pub;
-- Adım 3: Read'leri yavaş yavaş konsolide DB'ye taşı
-- Adım 4: Write'ları feature flag'lerle değiştir
-- Adım 5: Eski veritabanlarını devre dışı bırak
Bunu herhangi bir veri migration’ı gibi ele al: logical replication, ardından kademeli read geçişi, ardından flag’li write geçişi.
Kararın Maliyet Tarafı#
Çoğu konsolidasyon iş gerekçesi compute faturası üzerine kurulur; oysa o genellikle en küçük kalemdir. Karşılaştırmaya dört kategori girmeli:
- Altyapı: servis başına load balancer, mesh sidecar’ları ve her servis için ayrı ayrı ayrılan boşluk payı
- Observability: log hacmi, span ingestion ve host ya da container başına ajan fiyatlaması; hepsi trafikle değil servis sayısıyla ölçeklenir
- Nöbet: mevcut kadronun kaç rotasyonu doldurmak zorunda olduğu ve her rotasyonun taşıdığı pager yükü
- Koordinasyon: sürümleri takımlar arasında sıraya dizmeye harcanan mühendislik saatleri
İlk ikisi kabaca servis sayısıyla orantılı biçimde küçülür; argümanın kolay kısmı budur. Son ikisi genellikle daha büyük rakamlardır ve maliyet karşılaştırmasına çoğunlukla dahil edilmez.
Takım Yapısı ve Conway Yasası#
Takım Sınırları Değiştiğinde#
Bir organizasyon 12 küçük takımdan 4 büyük ürün takımına yeniden yapılandığında, 47 microservisi sürdürmek imkansız hale gelir. Her takım 10-12 servise sahip olacaktır. Takım yapısı, Conway Yasası’nı izleyerek artık servis yapısına doğrudan yansır:
// Takım yapısı mimariyi yönlendirdi
interface TeamArchitectureAlignment {
teamStructure: {
identityTeam: 8, // 8 mühendis
commerceTeam: 10, // 10 mühendis
fulfillmentTeam: 6, // 6 mühendis
platformTeam: 6 // 6 mühendis
};
serviceStructure: {
identityService: 'identityTeam', // Takım başına 1 servis
commerceService: 'commerceTeam', // Net sahiplik
fulfillmentService: 'fulfillmentTeam',// Koordinasyon gerekmiyor
platformService: 'platformTeam' // Paylaşılan altyapı
};
}
Modül Sınırlarını Build Zamanında Zorlamak#
Modüler monolitlerin ihtiyacı, teslim tarihi baskısına dayanacak modül sınırlarıdır:
// Dependency injection ile net modül arayüzleri
@Module({
imports: [], // Döngüsel bağımlılık yok!
providers: [
OrderService,
OrderRepository,
OrderValidator,
OrderEventPublisher
],
exports: [OrderService] // Sadece servisi expose et
})
export class OrderModule {
// İç sınıflar modül-private
private repository: OrderRepository;
private validator: OrderValidator;
private events: OrderEventPublisher;
// Public arayüz minimal ve stabil
public service: OrderService;
}
// Build time'da sınırları zorla
class OrderService {
constructor(
// Sadece izin verilen modüllerden inject edilebilir
@Inject(UserModule) private users: UserService,
@Inject(InventoryModule) private inventory: InventoryService,
// @Inject(RandomModule) <- Bu build time'da başarısız olur
) {}
}
Yukarıdaki izin verilmeyen @Inject(RandomModule) satırı, build zamanı zorlamanın neye benzediğini gösterir: review’a ulaşmadan hemen başarısız olan bir ihlal.
Monitoring ve Observability#
Konsolidasyon, monitoring’in sana söyleyebildiği şeyi de değiştirir.
12 Servis Boyunca İstek İzleme#
// Tek bir kullanıcı isteğini 12 servis üzerinden takip etmek
{
traceId: "abc-123",
spans: [
{ service: "api-gateway", duration: 5 },
{ service: "auth-service", duration: 45 },
{ service: "user-service", duration: 23 },
{ service: "profile-service", duration: 67 },
{ service: "preference-service", duration: 12 },
{ service: "recommendation-service", duration: 234 },
{ service: "content-service", duration: 56 },
{ service: "cache-service", duration: 3 },
{ service: "analytics-service", duration: 89 },
{ service: "notification-service", duration: 34 },
{ service: "email-service", duration: 156 },
{ service: "audit-service", duration: 45 }
],
totalDuration: 769,
status: "failed",
error: "recommendation-service'de 234ms sonra timeout"
}
Root cause’u bulmak 12 farklı servisten log’ları correlate etmeyi gerektiriyordu, her birinin kendi log formatı ve timestamp hassasiyeti vardı.
Deployment Başına Tek Log Akışı#
// Aynı istek modüler monolitte
{
requestId: "xyz-789",
module_timings: {
"auth.validateToken": 8,
"user.loadProfile": 15,
"recommendations.generate": 45,
"content.fetch": 12
},
totalDuration: 80,
databaseQueries: 4,
cacheHits: 12,
status: "success"
}
İkinci görünüm tek bir log stream ve tek bir deployment’tan gelir; alarm sonrası ilk soru doğrudan hangi modülün yavaşladığı olur, trace’in sahibini aramak gerekmez. Kalan deployable’lar arasında dağıtık tracing hâlâ değerli; ancak artık basit bir gecikme sorusunu yanıtlamanın tek yolu değil.
Sayılar Konsolidasyonu İşaret Ettiğinde#
Konsolidasyona girişmeden önce tartılması gereken sinyaller:
class ConsolidationDecisionFramework {
shouldConsolidate(): boolean {
const factors = {
// Teknik faktörler
deploymentCoupling: this.measureDeploymentCoupling(), // > 0.7 = konsolide et
sharedDataRequirements: this.assessDataSharing(), // > 0.6 = konsolide et
networkChattiness: this.measureServiceCommunication(), // > 100 çağrı/dk = konsolide et
transactionRequirements: this.needsAcidTransactions(), // true = kesinlikle düşün
// Organizasyonel faktörler
teamSize: this.getEngineeringHeadcount(), // < 50 = monolite yönel
teamStructure: this.assessTeamBoundaries(), // uyumsuz = konsolide et
onCallBurden: this.measureOnCallLoad(), // > 40saat/ay = konsolide et
// İş faktörleri
developmentVelocity: this.measureFeatureDelivery(), // azalıyor = uyarı işareti
operationalCost: this.calculateMonthlyBurn(), // sürdürülemez = konsolide et
timeToMarket: this.measureFeatureLeadTime(), // artıyor = problem
};
// Faktörlerin yarısından fazlası konsolidasyon öneriyorsa, yap
return this.calculateConsolidationScore(factors) > 0.5;
}
}
Bir Sonraki Sistemde Ne Değişmeli#
Modüler Monolitle Başla#
İyi yapılandırılmış bir modüler monolitten başlamak çoğu durumda tercih edilir. Servis çıkarma yalnızca şu durumlarda gereklidir:
- Bir modülün bağımsız ölçeklenmesi gerekiyor (spekülasyon değil, metriklerle kanıtlanmış)
- Bir modül farklı teknoloji gerektiriyor (meşru teknik gereksinim)
- Bir modül bağımsız deployment gerektiriyor (farklı release cycle’ları nedeniyle)
- Ayrı bir takım tamamen sahip olacak (Conway Yasası uyumluluğu)
Ölçüm ve Tasarım Alışkanlıkları#
Response time ve throughput zaten her ekibin topladığı metrikler. Mimarinin bozulacağını önceden haber verenleri ölçmek ise daha zor: alert’ten çözüme debug süresi, bir feature’ı anlamak için gereken kişi sayısı, developer başına bilişsel yük (günlük context switch sayısı) ve koordinasyona karşı yaratıma harcanan zaman.
Servisleri, bir kısmının ileride birleştirileceği varsayımıyla geliştir:
- Uyumlu teknoloji stack’leri kullan
- Tutarlı veri modelleri koru
- API pattern’lerini standartlaştır
- Servis sınırlarının ve neden var olduklarının iyi belgelerini tut
Bu Varsayılan Nerede Geçerliliğini Yitirir#
Konsolidasyon; servisler birlikte deploy ediliyorsa, aynı transaction sınırını paylaşıyorsa veya tek bir takıma aitse varsayılan olarak geçerlidir. Bunu geçersiz kılan üç durum var: bir bileşenin sistemin geri kalanından gerçekten farklı bir eğride ölçeklenmesi, farklı bir runtime ya da donanım profiline ihtiyaç duyulması, düzenleyici veya kiracı izolasyonunun ayrı bir etki alanı zorunlu kılması. Bu üç durumun her biri bölmeden önce doğrulanabilir.
İkinci sınır takım büyüklüğü. Ürün takımı başına bir deployable’ın ötesine geçildiğinde ortak kod tabanındaki koordinasyon maliyeti, ağ sınırının koordinasyon maliyetini aşmaya başlar ve servis çıkarmak kendini amorti eder. Bu çizginin altında, modül sınırları build’de zorlanan bir modüler monolit aynı zemini büyük ölçüde kaplar.
Servis grafiği çoktan büyümüşse ve nereden başlanacağı belirsizse önce deployment coupling’i ölç.
Kaynaklar#
- MonolithFirst - Martin Fowler (yeni sekmede açılır) - Yeni projelere mikroservisler değil monolit olarak başlanması gerektiğine dair Martin Fowler’ın temel argümanı ve erken ayrıştırmanın neden ters teptiği
- Mikroservisler - Martin Fowler (yeni sekmede açılır) - Mikroservis mimari stilini, trade-off’larını ve ne zaman uygun olduğunu tanımlayan temel makale
- Mikroservis Trade-off’ları - Martin Fowler (yeni sekmede açılır) - MicroservicePremium kavramı dahil mikroservislerin gerçek maliyetleri ve faydalarının analizi
- Strangler Fig Uygulaması - Martin Fowler (yeni sekmede açılır) - Alt sistemleri servislerle kademeli olarak değiştirerek monolitten geçiş için kullanılan örüntü
- Mikroservisler Oluşturmak, 2. Baskı - Sam Newman (yeni sekmede açılır) - Ayrıştırma, iletişim ve organizasyonel örüntüleri kapsayan mikroservis tasarımına kapsamlı rehber
- Mikroservisler Örüntü Dili - microservices.io (yeni sekmede açılır) - Chris Richardson’ın saga, strangler fig ve servis ayrıştırma yaklaşımları dahil mikroservis örüntüleri kataloğu
İlgili yazılar
Dayanıklı event-driven sistemler için multi-account AWS pattern'leri: hesap yapısı, EventBridge routing ve servisler arası iletişim.
aws · eventbridge · multi-account +5
Claude Code konfigürasyonlarını kopyalamak context window şişmesine, araç seçiminin bozulmasına ve uyumsuz iş akışlarına yol açar; token bütçesiyle bilinçli kurun.
developer-experience · ai-tools · productivity +2
AWS Verified Permissions, SpiceDB, OpenFGA, Cerbos ve OPA gibi harici yetkilendirme platformlarını mimari, maliyet ve karar çerçevesi açısından tarafsızca inceliyoruz.
authorization · security · architecture +4
SpiceDB ve Auth0 FGA (OpenFGA) karşılaştırması: şema, tutarlılık modelleri, dağıtım ve ölçeklenebilirlik açısından farklı tercihler yapan iki Zanzibar tabanlı sistem.
authorization · security · architecture +3
Çok katmanlı caching için pratik rehber: in-memory, Redis ve CDN katmanları, cache-aside ve write-through, ElastiCache ve MemoryDB, stampede önleme.
caching · redis · aws +4