Mikro Servis mi Monolitik mi: Doğru Mimari Seçimi

Yıllar önce bir e-ticaret projesinde tam production’a geçmeden iki gün önce mimari kararımızı sorgulamak zorunda kalmıştık. Ekip olarak “her şeyi mikro servis yapalım” diye tutturmuştuk, ama o son iki günde yaşadığımız debugging seansı bize çok şey öğretti. Şimdi bu konuyu biraz daha olgun bir bakış açısıyla ele almak istiyorum çünkü Türkiye’deki birçok ekibin aynı tuzağa düştüğünü görüyorum.

Önce Şunu Anlayalım: İkisi de Araç

Mikro servis tartışmaları çoğu zaman ideolojik bir boyut kazanıyor. “Mikro servis kullanan modern ekip, monolitik kullanan eski kafalı ekip” gibi bir algı oluşuyor. Bu tamamen yanlış. Her iki yaklaşımın da kendi bağlamında doğru olduğu durumlar var. Asıl mesele şu soruyu doğru yanıtlamak: Şu an bulunduğum ekip, ürün ve iş senaryosu için hangisi daha uygun?

Bunu belirlemek için önce her iki yaklaşımın gerçek operasyonel maliyetini anlamak gerekiyor. Blog yazılarında genellikle diyagramlar çizilir, teorik avantajlar sıralanır. Ben size günlük hayatta ne yaşandığını anlatacağım.

Monolitik Mimarinin Gerçek Hayatı

Bir monoliti düzgün çalıştırmak sandığınızdan çok daha basit ama yönetmek düşündüğünüzden çok daha zordur. Başlangıçta her şey güzel: tek bir repository, tek bir deployment pipeline, tek bir log dosyası. Bir şey bozulduğunda nereye bakacağınızı biliyorsunuz.

Tipik bir monolit deployment şöyle görünür:

#!/bin/bash
# Basit bir monolit deployment scripti
APP_DIR="/var/www/myapp"
BACKUP_DIR="/var/backup/myapp"
SERVICE_NAME="myapp"

echo "Deployment başlıyor..."

# Mevcut versiyonu yedekle
cp -r $APP_DIR $BACKUP_DIR/$(date +%Y%m%d_%H%M%S)

# Yeni kodu çek
cd $APP_DIR
git pull origin main

# Bağımlılıkları güncelle
composer install --no-dev --optimize-autoloader

# Veritabanı migrasyonlarını çalıştır
php artisan migrate --force

# Cache'i temizle
php artisan cache:clear
php artisan config:cache

# Servisi yeniden başlat
systemctl restart $SERVICE_NAME

echo "Deployment tamamlandı."

Görüyor musunuz? On beş satır. Bir junior sysadmin bile bunu anlıyor, takip edebiliyor, hata çıktığında ne yapacağını biliyor.

Ama zamanla kod büyüdükçe bu kolaylık bir yük haline gelmeye başlıyor. Özellikle şu senaryolarda:

  • Takım büyüdüğünde: Beş kişilik bir ekip aynı codebase üzerinde çalışırken merge conflict’ler bir kâbusa dönüşüyor.
  • Bağımsız ölçekleme gereğinde: Sitenizin sadece ödeme modülü yük altında kalıyor ama tüm monoliti scale etmek zorunda kalıyorsunuz.
  • Deployment frekansı arttığında: Bir özelliği deploy etmek için tüm uygulamayı yeniden deploy etmek gerekiyor, bu da riski artırıyor.
  • Teknoloji değiştirme ihtiyacında: Legacy PHP kodu modern bir servisle birlikte yaşamak zorunda kalıyor.

Şimdi bir düşünün: bu sorunların hepsini yaşıyorsanız ne zaman yaşıyorsunuz? Genellikle belirli bir büyüklüğe ulaştıktan sonra. Bir startup’ın ilk bir iki yılında bu sorunların çoğu anlamsız. Asıl sorun şu ki birçok ekip büyüme gerçekleşmeden önce “ileride lazım olur” diye mikro servise geçiyor.

Mikro Servis Mimarisinin Gerçek Maliyeti

Mikro servislerin getirdiği operasyonel yükü küçümsemek çok yaygın bir hata. Production’da on beş farklı servisiniz varsa ve bunların hepsi birbirleriyle konuşuyorsa, bu sadece bir mimari değil aynı zamanda bir operasyon sorunudur.

Şöyle bir senaryo düşünelim: Kullanıcı sipariş veriyor, ödeme servisi çalışıyor ama bildirim servisi cevap vermiyor. Ne oldu? Log’lara bakmak için hangi servisi açacaksınız?

# Distributed tracing olmadan log analizi böyle görünür
# Her servis için ayrı ayrı kontrol etmek zorunda kalıyorsunuz

# Sipariş servisi logları
kubectl logs -n production deployment/order-service --tail=100

# Ödeme servisi logları
kubectl logs -n production deployment/payment-service --tail=100

# Bildirim servisi logları
kubectl logs -n production deployment/notification-service --tail=100

# Kullanıcı servisi logları
kubectl logs -n production deployment/user-service --tail=100

# Üstelik bu loglar arasında correlation ID ile eşleştirme yapmanız gerekiyor
# Correlation ID yoksa başınızın belasısınız
grep "order-id-12345" /var/log/order-service/*.log
grep "order-id-12345" /var/log/payment-service/*.log

Distributed tracing kurulmadan bu debugging seansı saatler alabilir. Ve distributed tracing kurmak, yönetmek, bakımını yapmak ayrı bir uzmanlık ve efor gerektiriyor. Bunun üstüne service mesh, API gateway, circuit breaker, health check mekanizmaları eklenince ekibinizin zamanının önemli bir kısmı infrastructure yönetimine gidiyor.

Kubernetes üzerinde çalışan bir mikro servis ortamının sağlık kontrolü için tipik bir script:

#!/bin/bash
# Mikro servis health check scripti
SERVICES=("order-service" "payment-service" "user-service" "notification-service" "inventory-service")
NAMESPACE="production"
FAILED_SERVICES=()

for service in "${SERVICES[@]}"; do
    READY=$(kubectl get deployment $service -n $NAMESPACE -o jsonpath='{.status.readyReplicas}' 2>/dev/null)
    DESIRED=$(kubectl get deployment $service -n $NAMESPACE -o jsonpath='{.spec.replicas}' 2>/dev/null)
    
    if [ "$READY" != "$DESIRED" ]; then
        echo "UYARI: $service servisi sağlıksız! Ready: $READY, Desired: $DESIRED"
        FAILED_SERVICES+=($service)
    else
        echo "OK: $service ($READY/$DESIRED replika hazır)"
    fi
done

if [ ${#FAILED_SERVICES[@]} -gt 0 ]; then
    echo "Başarısız servisler: ${FAILED_SERVICES[*]}"
    # Alerting sistemine bildir
    curl -X POST $SLACK_WEBHOOK -d "{"text":"ALARM: Aşağıdaki servisler sağlıksız: ${FAILED_SERVICES[*]}"}"
    exit 1
fi

echo "Tüm servisler sağlıklı."

Bu script bile monolitin tek satır systemctl status myapp komutundan ne kadar uzak olduğunu gösteriyor. Bunu yazıp çalıştırmanız yetmiyor, bunu bir monitoring sistemiyle entegre etmeniz, alarmlara bağlamanız, runbook’lar yazmanız gerekiyor.

Karar Kriterleri: Sihirli Formül Yok

Peki nasıl karar vereceksiniz? Size “şu kadar kullanıcıdan sonra mikro servise geçin” gibi sihirli bir eşik söyleyemem çünkü böyle bir formül yok. Ama şu soruları kendinize sormanızı öneririm:

Ekip büyüklüğü ve organizasyon yapısı: Conway’s Law gerçek. Mimari yapınız takım yapınızı yansıtıyor. Üç kişilik bir ekipseniz beş mikro servis yönetmek eforunuzu mimari yönetimine harcamanıza yol açıyor. Amazon’un “iki pizza kuralı” buna dayanıyor: bir servisi yönetecek ekip iki pizza ile doyabilecek büyüklükte olmalı.

Bağımsız deployment ihtiyacı: Farklı ekipler farklı servisleri bağımsız deploy edebilmeli mi? Evet ise mikro servis mantıklı. Hayır ise monoliti modüler tutmak yeterli.

Ölçekleme gereksinimleri: Sisteminizin farklı bileşenleri çok farklı yük altında mı kalıyor? Örneğin bir e-ticaret sitesinde ürün arama servisi ödeme servisine göre yüzlerce kat daha fazla istek alıyor olabilir. Bu durumda bağımsız ölçekleme ciddi maliyet tasarrufu sağlıyor.

Teknoloji çeşitliliği ihtiyacı: Farklı servislerin farklı teknoloji stackleri kullanması gerekiyor mu? Makine öğrenmesi modeli Python istiyor, asıl uygulama Java’da, gerçek zamanlı bildirimler için Node.js daha uygun… Bu durumda mikro servis doğal seçim.

Pratik Bir Yaklaşım: Modüler Monolit

İkisi arasında sıkışıp kaldıysanız “modüler monolit” yaklaşımını değerlendirin. Bu yaklaşımda tek bir deployment yapısınız var ama kod organizasyonu açısından servisler arasındaki sınırlar net.

# Modüler monolit dizin yapısı örneği
myapp/
├── modules/
│   ├── orders/
│   │   ├── controllers/
│   │   ├── services/
│   │   ├── repositories/
│   │   └── tests/
│   ├── payments/
│   │   ├── controllers/
│   │   ├── services/
│   │   ├── repositories/
│   │   └── tests/
│   ├── users/
│   │   ├── controllers/
│   │   ├── services/
│   │   ├── repositories/
│   │   └── tests/
│   └── notifications/
│       ├── controllers/
│       ├── services/
│       └── tests/
├── shared/
│   ├── database/
│   ├── middleware/
│   └── utils/
└── main.go

Bu yapı size şunu sağlıyor: bugün monoliti kolayca yönetiyorsunuz, yarın belirli bir modülü ayırmak istediğinizde sınırlarınız zaten net olduğu için geçiş çok daha kolay oluyor. “Strangler Fig Pattern” denen bu yaklaşım, büyük sistemleri aşamalı olarak mikro servise dönüştürmek için de kullanılıyor.

# Strangler Fig Pattern ile aşamalı geçiş
# Nginx config: Yeni user-service'e traffic yönlendirme

upstream legacy_monolith {
    server monolith-app:8080;
}

upstream new_user_service {
    server user-service:3000;
}

server {
    listen 80;
    
    # Kullanıcı ile ilgili endpoint'ler yeni servise
    location /api/users {
        proxy_pass http://new_user_service;
    }
    
    location /api/auth {
        proxy_pass http://new_user_service;
    }
    
    # Geri kalan her şey hâlâ monolite
    location / {
        proxy_pass http://legacy_monolith;
    }
}

Bu yaklaşımla ekibiniz büyük bang migration yerine parça parça, riski kontrollü bir şekilde geçiş yapabiliyor.

Gerçek Dünya Senaryosu: Ne Zaman Hangisi?

Şimdi birkaç somut senaryo üzerinden konuşalım.

Senaryo 1: Yeni bir SaaS ürünü geliştiriyorsunuz, ekibiniz dört kişi, henüz product-market fit bulmadınız ve hızla iterate etmeniz gerekiyor. Cevap: Monolit. Kesinlikle monolit. Mikro servis kurulum maliyeti sizi yavaşlatır, pivot yapmanız gerektiğinde servisler arasındaki bağımlılıkları yönetmek çıldırtır.

Senaryo 2: Kurumsal bir banka için ödeme sistemi geliştiriyorsunuz. Compliance gereksinimleri var, farklı ekipler farklı bileşenleri yönetiyor, bazı modüller çok daha sık değişiyor. Cevap: Mikro servis mimarisi makul, ama sadece gerçekten bağımsız deploy edilmesi gereken parçalar için.

Senaryo 3: Mevcut bir monolitiniz var ve yavaş yavaş ölüyor, ekleme yapmak çok zorlaştı. Cevap: Önce modülerleştirin, sonra en kritik ve en bağımsız servisi çıkarın, sonra yavaşça devam edin.

Bir servisin mikro servise ayrılmaya hazır olup olmadığını test etmek için şöyle bir kontrol listesi kullanabilirsiniz:

#!/bin/bash
# Servis bağımsızlık kontrol scripti
# Bu script bir modülün bağımlılıklarını analiz eder

MODULE_NAME=$1
SOURCE_DIR="./src/modules/$MODULE_NAME"

echo "=== $MODULE_NAME modülü bağımlılık analizi ==="

# Modül dışına olan import sayısını say
EXTERNAL_IMPORTS=$(grep -r "import" $SOURCE_DIR | grep -v "from './" | grep -v "from "./'" | wc -l)
echo "Dış bağımlılık sayısı: $EXTERNAL_IMPORTS"

# Veritabanı tablo erişimini analiz et
DB_TABLES=$(grep -r "FROM|JOIN|INSERT INTO|UPDATE" $SOURCE_DIR | grep -oP "(?<=FROM |JOIN |INTO |UPDATE )w+" | sort -u)
echo "Erişilen tablolar:"
echo "$DB_TABLES"

# Bu modüle gelen çağrı sayısını bul
INCOMING_CALLS=$(grep -r "$MODULE_NAME" ./src/modules --include="*.go" | grep -v "^$SOURCE_DIR" | wc -l)
echo "Diğer modüllerden gelen referans sayısı: $INCOMING_CALLS"

if [ $EXTERNAL_IMPORTS -gt 50 ] || [ $INCOMING_CALLS -gt 30 ]; then
    echo "SONUÇ: Bu modül henüz mikro servise ayrılmaya hazır değil. Önce bağımlılıkları azaltın."
else
    echo "SONUÇ: Bu modül mikro servise ayrılabilir."
fi

Operasyonel Hazırlık

Mikro servise geçmeye karar verdiyseniz şunu bilin: teknik karar vermek kolay kısım. Asıl iş operasyonel hazırlığa geliyor. Şu bileşenler olmadan mikro servis ortamı işletemezsiniz:

  • Service discovery: Servisler birbirini nasıl bulacak?
  • Distributed tracing: Jaeger veya Zipkin gibi araçlarla request’leri takip etmek
  • Centralized logging: ELK stack veya Loki ile tüm servis loglarını tek yerden görmek
  • Circuit breaker: Bir servis çöktüğünde diğerlerini nasıl koruyacaksınız?
  • API gateway: Dış dünyaya tek giriş noktası
  • Health check ve otomatik yeniden başlatma: Kubernetes bunu hallediyor ama konfigürasyon gerekiyor

Bu altyapıyı kurmak ve bakımını yapmak ciddi bir yatırım. Küçük bir ekip için bu yatırımın geri dönüşü çoğu zaman olmuyor.

# Docker Compose ile lokal mikro servis ortamı kurulumu
# Production'a geçmeden önce lokal ortamda test için

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"  # Jaeger UI
      - "6831:6831/udp"  # Jaeger agent
    
  elasticsearch:
    image: elasticsearch:8.0.0
    environment:
      - discovery.type=single-node
    ports:
      - "9200:9200"
      
  kibana:
    image: kibana:8.0.0
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
      
  api-gateway:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
    depends_on:
      - order-service
      - user-service

Yaygın Tuzaklar

Yıllar içinde gördüğüm en yaygın hatalar şunlar:

  • Nano servisler: Her şeyi çok küçük parçalara bölmek. “Adres servisi”, “telefon numarası servisi” gibi saçmalıklar gerçekten oluyor. Servis granülaritesi iş domainlerine göre belirlenmeli.
  • Dağıtık monolit: Tüm servislerin aynı veritabanını paylaşması. Bu hem monolitin hem de mikro servisin kötü taraflarını alıyorsunuz.
  • Senkron bağımlılık zinciri: A servisi B’yi çağırıyor, B C’yi çağırıyor, C D’yi çağırıyor… Bir halka çöktüğünde tüm zincir çöküyor. Bu durumda asenkron iletişim veya event-driven mimari düşünmek gerekiyor.
  • Versiyonlama ihmal etmek: Servisler arasındaki API kontratları versiyonlanmadan yönetilemiyor.

Sonuç

Mikro servis mi monolitik mi sorusunun tek bir cevabı yok. Ama şunu rahatlıkla söyleyebilirim: çoğu Türkiye’deki startup ve orta ölçekli ekip için modüler monolit ile başlamak, ardından büyüme gerçekleştikçe aşamalı geçiş yapmak en sağlıklı yol.

Mikro servislerin vaat ettiği esneklik gerçek, ama bu esnekliğin bedeli de gerçek. Henüz o bedeli ödeyecek olgunluğa ulaşmamış ekipler için mikro servis bir fırsat değil, operasyonel bir yük haline geliyor.

Kararınızı şu üç soruya vereceğiniz cevaba göre verin: Ekibiniz bu karmaşıklığı yönetecek kapasitede mi? Ürününüzün bağımsız ölçekleme ve deployment ihtiyacı gerçekten var mı? Bu mimariyi kurup ayakta tutmak için harcayacağınız efor, size özellik geliştirmekten daha fazla değer mi sağlayacak?

Cevaplar evet ise mikro servise gidin. Değilse monolitinizi temiz tutmak, modüler yazmak ve gün geldiğinde refactor etmek için kendinizi hazırlamak çok daha akıllıca bir yatırım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir