Monolit'in İntikamı: Microservisler Teknik Borç Haline Geldiğinde
Dağıtık monolitleri tanıma, stratejik servis konsolidasyonu ve microservis karmaşıklığı sürdürülemez hale geldiğinde modüler monolitlere geri dönüşün gerçekleri üzerine bir principal engineer perspektifi.
Microservis mimarinizin, kaçınmaya çalıştığınız dağıtık monolit haline geldiğini fark ettiğiniz o batma hissini biliyor musun? 20+ yıllık kariyerimde mimari sarkaçların sallanışını izlerken, bu pattern'i birçok şirkette tekrar tekrar gördüm. Microservislerin ne zaman teknik borca dönüştüğünü tanıma ve stratejik olarak nasıl normale dönüleceği konusunda öğrendiklerimi paylaşayım.
47 Servisle Alışveriş Sepeti Kâbusu#
Tanıdık gelebilecek bir hikaye anlatayım. Basit bir e-ticaret platformumuz vardı ve bir şekilde sadece sepete ürün eklemek için 47 microservise evrilmişti. Her servisin kendi veritabanı, deployment pipeline'ı ve nöbet rotasyonu vardı. Tek bir satın alma işlemi 12 farklı takımın koordinasyonunu gerektiriyordu.
Mimari diyagram sunumlarda etkileyici görünüyordu. Gerçek? Feature geliştirmekten çok servisler arası iletişimi debug etmekle uğraşıyorduk. "Loosely coupled" servislerimiz bağımsız deploy edilemiyordu çünkü bir API'yi değiştirmek beş takımla koordinasyon demekti. Klasik dağıtık monolit sendromu.
Microservisler Saldırdığında: Tanıma Kalıpları#
Bunu üç farklı şirkette gördükten sonra, microservislerin teknik borca dönüştüğüne dair tutarlı uyarı işaretleri fark ettim:
Deployment Ölüm Dansı#
# 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
Servisleri bağımsız deploy edemiyorsan, microservisin yok - ekstra adımları olan dağıtık bir monolitin var.
Veri Tutarlılığı Kâbusu#
// 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);
}
}
}
Sonunda bir dağıtık transaction koordinatörü geliştirdik. İronik olan, bu aslında microservisleri koordine eden bir monolitti. Evrenin mizah anlayışı var.
Büyük Konsolidasyon Stratejisi#
Son şirketimde iki yıllık microservis karmaşıklığından sonra, 23 servisi 3 modüler monolite konsolide ettik. İşte geliştirdiğimiz framework:
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)
);
}
}
Gerçekten İşe Yarayan Modüler Monolit Pattern'i#
47 servis yerine, net iç sınırlara sahip 3 modüler monolit geliştirdik:
// 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)
};
// Paylaşılan altyapı - sihirli sos
private sharedDb = new DatabaseConnection();
private cache = new RedisCache();
async processOrder(request: OrderRequest) {
// Dağıtık saga'lar yerine güzel ACID transaction'ları
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 süresi 45 dakikalık orkestrasyon kaosundan 4 dakikalık basit blue-green deployment'a düştü. Incident oranımız %60 azaldı. Takım hızı %40 arttı.
Gerçek Konsolidasyon Hikayeleri: İyi, Kötü, Pahalı#
Hızlı Büyüyen Startup Hesaplaşması#
Hızla ölçeklenen bir fintech startup'ta, 18 ay içinde 47 microservise ulaştık. Her yeni feature yeni bir servis demekti çünkü "Netflix böyle yapıyor." Ama biz Netflix değildik - 3.000 değil, 30 mühendisimiz vardı.
Dönüm noktası yönetim kurulu demosunda geldi. Basit bir kullanıcı kaydı 8 servis üzerinden çağrı tetikliyordu. Payment servisi timeout aldığında, tüm akış başarısız oldu ve sistemde yarı oluşturulmuş bir kullanıcı kaldı. CTO'nun o demodaki yüz ifadesi hala beni rahatsız ediyor.
Sonraki çeyreği 4 domain odaklı servise konsolide ederek geçirdik:
- Identity Service: Kullanıcı, auth, profiller, izinler
- Transaction Service: Ödemeler, siparişler, faturalama, mutabakat
- Product Service: Katalog, fiyatlama, envanter, öneriler
- Communication Service: Email, SMS, push bildirimleri, webhook'lar
Sonuç? Feature teslimatı 3x iyileşti ve bug'ları dağıtık sistem arkeolojisi yapmadan gerçekten takip edebildik.
Tam Tur Atan Enterprise Migration#
Danışmanlık yaptığım bir Fortune 500 şirketi, 3 yılda legacy monolitten 200+ microservise geçmişti. Mimari o kadar karmaşıktı ki, neyin neyle konuştuğunu belgelemek için özel bir "Servis Haritacılığı Takımı" vardı.
Kritik bir denetim sırasında, tek bir uyumluluk raporunun 73 farklı servisten veri gerektirdiğini keşfettiler. Rapor 6 saat sürüyordu ve timeout cascade'leri nedeniyle %30 oranında başarısız oluyordu.
Konsolidasyon stratejisi cerrahi oldu:
-- Ayrı veritabanları yerine domain şemaları oluşturduk
CREATE SCHEMA customer_domain;
CREATE SCHEMA product_domain;
CREATE SCHEMA order_domain;
CREATE SCHEMA compliance_domain;
-- İlgili tabloları domain şemalarına taşıdık
ALTER TABLE users SET SCHEMA customer_domain;
ALTER TABLE profiles SET SCHEMA customer_domain;
ALTER TABLE preferences SET SCHEMA customer_domain;
-- Artık uyumluluk raporları basit join'ler
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';
-- 6 saat değil, 3 saniyede çalışıyor
200+ servisten business domain'e göre organize edilmiş 12 modüler monolite geçtiler. Uyumluluk raporu üretimi 6 saatlik dağıtık sistem macerasından 3 saniyelik bir query'ye dönüştü.
Ölçeklenemeyen Performans Kritik Sistem#
Çalıştığım bir gerçek zamanlı trading platformu, "sonsuz ölçeklenebilirlik" için sistemlerini microservislere ayırmıştı. Sorun? Servisler arası network latency'si her trade execution'a 50-100ms ekliyordu. Yüksek frekanslı trading'de bu bir sonsuzluk.
Çözüm mantığa aykırıydı - her şeyi tek, yüksek optimize edilmiş bir process'e konsolide et:
// Ö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
}
}
Trade execution 15x iyileşti. Bazen eski yöntemler en iyi yöntemlerdir.
Gerçekten İşe Yarayan Migration Stratejileri#
Strangler Fig Pattern (Tersten)#
Monoliti microservislerle boğmak yerine, microservisleri monolitle boğduk:
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 migrate ettik, her adımda hata oranlarını ve performansı izledik. Tüm konsolidasyon 6 ay sürdü ama hiç büyük bir incident yaşamadık.
Gözyaşsız Veritabanı Konsolidasyonu#
Konsolidasyonun en korkutucu kısmı genellikle veritabanlarını birleştirmek. İşe yarayan pattern şu:
-- 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
Kilit içgörü: bunu özel bir microservis sihri değil, herhangi bir veri migration'ı gibi ele al.
Kimsenin Konuşmadığı Maliyet Analizi#
Konsolidasyonumuzdan gerçek rakamları paylaşayım:
Konsolidasyon Öncesi (47 Microservis)#
- AWS Infrastructure: $12,000/ay
- DataDog Monitoring: $3,000/ay
- PagerDuty: $500/ay (çok fazla eskalasyon)
- Developer Zamanı (koordinasyon overhead'i): ~$45,000/ay
- Toplam Aylık Maliyet: $60,500
Konsolidasyon Sonrası (3 Modüler Monolit)#
- AWS Infrastructure: $4,000/ay
- DataDog Monitoring: $800/ay
- PagerDuty: $100/ay (artık nadiren page atıyor)
- Developer Zamanı (azaltılmış overhead): ~$15,000/ay
- Toplam Aylık Maliyet: $19,900
Yıllık Tasarruf: $487,200
Ama gerçek fayda maliyet tasarrufu değildi - developer mutluluğuydu. Mühendisler üzerinde çalıştıkları sistemi gerçekten anlayabildiklerinde çalışan memnuniyeti dramatik şekilde iyileşti.
Takım Dinamikleri ve Conway Yasasının İntikamı#
Mimarlık derslerinde öğretmedikleri bir şey var: takım yapınız nihayetinde mimarinizi belirleyecek, tersi değil.
Konsolidasyonu Zorlayan Takım Reorganizasyonu#
Şirketimiz 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 geldi. Her takım 10-12 servise sahip olacaktı. Conway Yasası'yla savaşmak yerine, onu kucakladık:
// 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ı
};
}
Her takım bir modüler monolit'e sahipti. Nöbet yönetilebilir hale geldi. Bilgi paylaşımı iyileşti. Code review'ları mantıklı gelmeye başladı çünkü reviewer'lar bağlamı anlıyordu.
Zamanın Testine Dayanan Modül Sınırları#
Başarılı modüler monolitlerin sırrı modül sınırlarını doğru belirlemek. İşe yarayan şey şu:
// 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
) {}
}
Anahtar: yanlış bağımlılıkları code review'da cesaretini kırmak değil, compile time'da imkansız hale getirmek.
Basitleştirilmiş Monitoring ve Observability#
Konsolidasyonun beklenmedik bir faydası: monitoring gerçekten kullanışlı hale geldi.
Önce: Dağıtık Tracing Kâbusu#
// 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ı.
Sonra: Uygulama Seviyesi Observability#
// 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"
}
Tek log stream. Tek deployment. İşler ters gittiğinde bakacak tek yer. Devrim niteliğinde.
Karar Framework'ü#
Bunu birkaç kez yaşadıktan sonra, ne zaman konsolide edeceğime karar vermek için framework'üm şu:
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;
}
}
Farklı Yapacaklarım (Geçmişe Bakış 20/20)#
Birden fazla microservis yolculuğuna bakınca, keşke daha önce bilseydim dediklerim:
Modüler Monolitle Başla#
Her projenin başına geri sarabilsem, iyi yapılandırılmış bir modüler monolitle başlar ve sadece şu durumlarda servis çıkarırdım:
- 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)
Sadece Performansı Değil, Karmaşıklığı Ölç#
Her zaman response time'ları ve throughput'u ölçtük. Ölçmemiz gerekenler:
- Bir sorunu debug etme süresi (alert'ten çözüme)
- Bir feature'ı anlamak için gereken kişi sayısı
- Developer başına bilişsel yük (günlük context switch'leri)
- Koordinasyona vs. yaratıma harcanan zaman
İlk Günden Konsolidasyon İçin Tasarla#
Servisleri daha sonra birleştirebileceğin 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
Mimari Evrim Hakkında Dürüst Gerçek#
Bu sektörde iki on yıl sonra öğrendiğim şey: mükemmel mimari yok, sadece mevcut bağlamınıza uyan mimariler var. Microservisler kötü değil. Monolitler kötü değil. Microservis gibi davranan dağıtık monolitler - işte onlar kötü.
Monolitten microservislere, oradan modüler monolite sarkaç salınımı başarısızlık değil - öğrenme. Her mimari kararı gelecekteki gereksinimler, takım yapısı ve iş ihtiyaçları üzerine bir bahis. Bazen bu bahsi kaybedersin. Anahtar, kaybettiğini tanımak ve rotayı değiştirme cesaretine sahip olmak.
Şu anki şirketimde, milyonlarca kullanıcıya hizmet veren bir modüler monolit çalıştırıyoruz. Microservislere bölebilir miyiz? Elbette. Bölecek miyiz? Karmaşıklık maliyetini haklı çıkaracak zorlayıcı bir nedenimiz olana kadar hayır. Bu dersi pahalı yoldan öğrendik.
Temel Çıkarımlar#
Teknik Liderler İçin:
- Servis konsolidasyonu geçerli bir mimari pattern, başarısızlık itirafı değil
- Takım bilişsel yükünü sistem metriklerini izlediğin kadar yakından izle
- Takım sınırlarının servis sınırlarını yönlendirmesine izin ver, tersi değil
- Operasyonel karmaşıklığın gerçek maliyetleri var - mimari kararlara dahil et
Development Takımları İçin:
- Modüler monolitler karmaşıklık olmadan microservis faydaları sağlayabilir
- Paylaşılan veritabanları ve ACID transaction'ları genellikle eventual consistency'den daha basit
- Monolitin içindeki modül sınırlarına odaklan - servis sınırlarından daha önemli
- Mutluluğun ve üretkenliğin geçerli mimari gereksinimler
Mimarlar İçin:
- Konsolidasyon olasılığı dahil, değişim için tasarla
- Sadece altyapıyı değil, mimarinin toplam maliyetini ölç
- Transaction sınırları genellikle servis sınırlarından daha önemli
- Bazen en iyi hamle geriye doğru - ve bu sorun değil
Unutma: amaç mimari saflık değil - takımının verimli bir şekilde değer sunmasını sağlayan sistemler geliştirmek. Bazen bu, microservislerin teknik borca dönüştüğünü kabul etmek ve daha basit bir şeye geri konsolide etme cesaretine sahip olmak demek.
Monolitin intikamını bekliyor. Belki de ona sahip olmasına izin verme zamanı geldi.
Yorumlar (0)
Sohbete katıl
Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap
Henüz yorum yok
Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!
Yorumlar (0)
Sohbete katıl
Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap
Henüz yorum yok
Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!