Skip to content
~/sph.sh

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 perspektif.

Microservis mimarinizin, kaçınmaya çalıştığınız dağıtık monolit haline geldiğini fark ettiğiniz o batma hissini biliyor musun? 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. Dağıtık monolit sendromu genellikle deployment coupling, veri tutarlılığı zorlukları ve koordinasyon yüküyle kendini gösterir. İyi haber: konsolidasyon geçerli bir mimari pattern, başarısızlık itirafı değil.

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ı

yaml
# Deployment "orkestrasyon"unuz böyle görünüyordeploy-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

typescript
// 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

typescript
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:

typescript
// Tek deployable, birden fazla modülclass 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ı. Modüler monolitler, net iç sınırlarla microservis faydalarını karmaşıklık olmadan sunabilir; paylaşılan veritabanı ve ACID transaction'lar çoğu senaryoda eventual consistency'den daha basit.

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:

sql
-- Ayrı veritabanları yerine domain şemaları oluşturdukCREATE SCHEMA customer_domain;CREATE SCHEMA product_domain;CREATE SCHEMA order_domain;CREATE SCHEMA compliance_domain;
-- İlgili tabloları domain şemalarına taşıdıkALTER 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'lerSELECT   c.user_id,  c.registration_date,  o.total_orders,  o.total_revenue,  p.product_categoriesFROM customer_domain.users cJOIN order_domain.order_summary o ON c.user_id = o.user_idJOIN product_domain.user_products p ON c.user_id = p.user_idWHERE 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:

typescript
// Önce: Network overhead'li microservislerclass 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 monolitclass 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:

typescript
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:

sql
-- Adım 1: Konsolide veritabanında domain şemaları oluşturCREATE SCHEMA user_domain;CREATE SCHEMA order_domain;CREATE SCHEMA inventory_domain;
-- Adım 2: Microservis DB'lerinden logical replication kurCREATE 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 12,000/ay,DataDog12,000/ay, DataDog 3,000/ay, PagerDuty 500/ay,developeroverhead 500/ay, developer overhead ~45,000/ay. Toplam: $60,500/ay.

Konsolidasyon Sonrası (3 Modüler Monolit): AWS 4,000/ay,DataDog4,000/ay, DataDog 800/ay, PagerDuty 100/ay,developeroverhead 100/ay, developer overhead ~15,000/ay. Toplam: 19,900/ay.Yıllıktasarruf:19,900/ay. **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:

typescript
// Takım yapısı mimariyi yönlendirdiinterface 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. Conway Yasası mimari kararları yönlendirir; takım sınırları servis sınırlarından önce gelmeli. Organizasyonel yapı değiştiğinde mimarinin de adapte olması gerekir.

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:

typescript
// 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ı zorlaclass 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. Modül sınırları servis sınırlarından daha önemli; önce iç sınırları doğru belirle.

Basitleştirilmiş Monitoring ve Observability

Konsolidasyonun beklenmedik bir faydası: monitoring gerçekten kullanışlı hale geldi.

Önce: Dağıtık Tracing Kâbusu

javascript
// 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

javascript
// 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:

typescript
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

Öğ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.

İlgili Yazılar