İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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#

İlgili yazılar