Eylül 19 2026

Legacy Sistemlerde Modernizasyon: ‘Strangler Fig’ Modeli ile Göç Stratejileri

Bir sabah uyandığınızda yönetimin “Artık bu monolit bizi taşımıyor, her şeyi sıfırdan yazıyoruz!” dediğini duyduysanız, soğuk terler dökmüşsünüzdür. Legacy bir altyapıyı bir gecede rafa kaldırmaya çalışmak (namıdiğer Big Bang rewrite), havacılıkta uçak havadayken motor değiştirmeye değil, uçağı havada patlatıp parçalarından jet yapmaya benzer. Başarılı bir modernizasyon süreci için ihtiyacımız olan şey cesur bir intihar girişimi değil, kontrollü bir yazılım mimarisi evrimidir. İşte tam bu noktada, monoliti boğarak öldüren mikroservis dönüşüm yaklaşımı: Strangler Fig (Boğucu İncir) paterni devreye giriyor.

Martin Fowler’ın Avustralya’daki asalak incir ağaçlarından esinlenerek adlandırdığı bu model, yeni sistemi eski sistemin etrafına adım adım sarıp, zamanla eskiyi işlevsiz bırakarak çürütme prensibine dayanır. Peki bu teoriyi prodüksiyonda 500k RPM trafik altında çalışan bir Kubernetes kümesinde nasıl hayata geçiririz? Gelin, lafı uzatmadan işin mutfağına inelim.

1. Keşif ve Sınırların Çizilmesi: Nereden Başlamalı?

Strangler Fig yaklaşımında yapılan en büyük hata, doğrudan sistemin kalbi sayılan domain’e (örneğin e-ticaretteki ödeme veya sipariş akışı) saldırmaktır. İlk adayınız şu üç kriteri sağlamalı:

  • Düşük Bağımlılık (Low Coupling): Monolitin veri tabanındaki 40 farklı tabloya JOIN atmayan bir context.
  • Yüksek Değer / Hızlı Kazanım: Hata yaptığınızda şirketi batırmayacak ama çalıştığında ekibe “bu iş oluyor” güvenini verecek bir modül (Örn: Bildirim servisi, PDF/Fatura oluşturucu, Statik içerik sunumu).
  • Net API Kontratı: Girdisi ve çıktısı belirgin olan işlevler.

Sınırları belirledikten sonra mimarinin en kritik bileşenini devreye alıyoruz: Interception (Yakalama) Katmanı.

2. Interception Katmanı: Akıllı Yönlendirme (Routing)

Strangler Fig modelinin kalbi API Gateway ya da Reverse Proxy katmanıdır. İstemciler (web, mobil, 3rd party API) asla monolitle mi yoksa yeni mikroservisle mi konuştuklarını bilmemelidir. Trafik önce bu katmana çarpar ve yönlendirme kurallarına göre hedefe dağıtılır.

Bu aşamada sadece path-based routing yetmez; feature flag’ler, canary dağıtımlar ve cookie/header bazlı yönlendirmeler gerekir. NGINX kullanarak bu katmanı nasıl kurabileceğimize bakalım:

# /etc/nginx/conf.d/strangler_router.conf

upstream legacy_monolith {
    server 10.0.1.10:8080 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

upstream new_notification_service {
    server 10.0.2.20:3000 max_fails=2 fail_timeout=5s;
    keepalive 16;
}

# Feature flag kontrolü: Header veya Cookie bazlı canary
map $http_x_use_microservice $notification_backend {
    default legacy_monolith;
    "true"  new_notification_service;
}

server {
    listen 80;
    server_name api.sirket.internal;

    # Henüz dokunulmamış tüm trafik doğrudan monolite
    location / {
        proxy_pass http://legacy_monolith;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Strangled endpoint: Kademeli göç edilen servis
    location /api/v1/notifications {
        proxy_pass http://$notification_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Request-ID $request_id;
        
        # Olası mikroservis çökmesinde monolite failover mekanizması
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

Buradaki kritik detay proxy_next_upstream kullanımıdır. Yeni mikroservisiniz beklediğiniz yükü kaldıramaz veya 502/503 dönerse, istek anında monolite yönlendirilerek SLA ihlali önlenir. Tabii ki bu fallback state-mutating (POST/PUT/DELETE) isteklerde idempotentlik garantisi yoksa dikkatli kullanılmalıdır.

3. En Büyük Karın Ağrısı: Veri Bütünlüğü ve CDC (Change Data Capture)

İş mantığını ayırmak kolaydır; veri tabanını ayırmak ise tam bir mayın tarlasıdır. Legacy uygulamalarda genellikle tek bir devasa MySQL veya Oracle veri tabanı bulunur. Mikroservis mimarisinde ise her servisin kendi veri tabanı olmalıdır (Database-per-Service).

Bunu çözmek için kesinlikle monolit içinden “hem eski DB’ye yazayım hem yeni servisin DB’sine yazayım” (Dual Writing) gafletine düşmeyin. Ağ gecikmeleri, connection pool tükenmesi ve dağıtık transaction problemleri başınızı ağrıtır. Bunun yerine Change Data Capture (CDC) kullanarak veri akışını asenkron hale getirin.

Debezium ve Apache Kafka ikilisi, legacy veri tabanının transaction log’larını (binlog / redo log) dinleyerek olay bazlı (event-driven) veri aktarımı sağlar:

# Debezium MySQL Source Connector Konfigürasyonu (Kafka Connect REST API)
curl -i -X POST -H "Accept:application/json" -H "Content-Type:application/json" \
  http://kafka-connect.infra:8083/connectors/ \
  -d '{
    "name": "legacy-mysql-cdc-connector",
    "config": {
      "connector.class": "io.debezium.connector.mysql.MySqlConnector",
      "tasks.max": "1",
      "database.hostname": "legacy-db-primary.internal",
      "database.port": "3306",
      "database.user": "debezium_cdc",
      "database.password": "VaultGuardedSecret123!",
      "database.server.id": "184054",
      "database.server.name": "legacy_core",
      "database.include.list": "core_production",
      "table.include.list": "core_production.notifications,core_production.user_preferences",
      "database.history.kafka.bootstrap.servers": "kafka-cluster:9092",
      "database.history.kafka.topic": "schema-changes.legacy.core",
      "decimal.handling.mode": "double",
      "time.precision.mode": "connect"
    }
  }'

Bu yapı sayesinde, monolit eski tabloya yazmaya devam ederken Debezium bu değişiklikleri anında Kafka’ya fırlatır. Yeni mikroservisiniz de bu event’leri tüketerek (consume) kendi izole PostgreSQL veya MongoDB veritabanını besler (Eventual Consistency).

4. Trafik Kademelendirme: Shadowing (Dark Launching)

Yeni yazdığınız mikroservisi prodüksiyon trafiğiyle ilk kez karşılaştırmak istemezsiniz. Yük testleri sentetiktir; gerçek kullanıcı davranışlarını simüle edemez. Bu yüzden Traffic Shadowing (Trafik Aynalama) tekniğini kullanmalısınız.

Trafik Gateway katmanına geldiğinde hem monolite hem de mikroservise gönderilir. Monolitin cevabı kullanıcıya dönülürken, mikroservisin cevabı arka planda loglanır veya karşılaştırılır (semantic diffing). İstemci mikroservisten haberdar bile olmaz.

Kubernetes üzerinde Envoy tabanlı bir Ingress (örneğin Emissary-ingress veya doğrudan Envoy proxy) kullanıyorsanız, shadowing tanımı şu kadar basittir:

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
  name: notification-shadow-mapping
  namespace: production
spec:
  hostname: api.sirket.internal
  prefix: /api/v1/notifications/send
  service: legacy-monolith-service.production:8080
  shadow: true
  shadow_service: new-notification-microservice.modern-apps:3000
  timeout_ms: 2500

Shadowing devredeyken neyi izlemelisiniz?

  • HTTP Status Kodu Farkları: Monolit 200 dönerken mikroservis 500 mü veriyor? Neden?
  • p99 ve p95 Latency: Yeni servis gerçekten hedeflenen performansı sunuyor mu?
  • Payload Uyuşmazlığı: Yanıt gövdeleri arasındaki farklar neler?

5. Monoliti Boğmak: Eski Kodu Temizlemek

Mikroservisiniz shadowing aşamasını başarıyla geçti, veri senkronizasyonu stabil, canary deployment ile trafiğin %100’ünü yeni servise aktardınız. Tebrikler, servisi göç ettiniz!

Ama işiniz bitmedi. Strangler Fig paterninin son ve en çok ihmal edilen adımı: Eski kodun monolit içinden sökülüp atılmasıdır.

Geliştiriciler genelde “ne olur ne olmaz, kod kalsın” diyerek eski methodları ve endpoint’leri monolitte bırakır. Bu durum, monoliti zamanla yaşayan bir zombi kod yığınına çevirir. Bir sonraki göç adımında bu zombiler ayağınıza dolanır.

Yapılacaklar net:

  1. Monolitteki eski endpoint’e bir log alert’i koyun (artık 0 istek alıyor olmalı).
  2. Monolit kod tabanından ilgili controller, service ve data access katmanlarını silin.
  3. CDC konfigürasyonundan o tabloyu çıkartın ve monolit veri tabanındaki eski tabloyu READ-ONLY moda alıp birkaç hafta sonra arşivleyin.

Özet ve SRE Çıkarımları

Bir legacy mimariyi modernize etmek yazılım mühendisliğinden çok bir cerrahi operasyondur. Strangler Fig yaklaşımı bu operasyonu lokal anestezi altında, hasta yürümeye devam ederken yapmanızı sağlar.

Bu yolculukta aklınızdan çıkarmamanız gereken altın kurallar şunlar:

  • Önce Gateway katmanını güçlendirin; yönlendirme kabiliyetiniz yoksa göçü yönetemezsiniz.
  • Veritabanı senkronizasyonunu CDC gibi asenkron ve güvenilir araçlarla çözün.
  • Prodüksiyon trafiğini aynalayarak (traffic mirroring) riskinizi sıfırlayın.
  • Göçü tamamlanan her parçadan sonra monolitteki ölü kodu acımadan temizleyin.

Bir sonraki sprint planlamasında masaya “Her şeyi sıfırdan yazalım” fikri gelirse, bu makaleyi Slack kanalına usulca bırakın ve monoliti boğmaya odaklanın.

Category: Genel | LEAVE A COMMENT
Mart 7 2025

Mikroservis Labirentinde Kaybolmamak: OpenTelemetry ve Jaeger ile Dağıtık İzleme

Modern bir devops ekibinin kabusu, gecenin üçünde gelen bir PagerDuty alarmıyla başlar. Sisteminizdeki 10’larca mikroservis birbirine zincirlenmişken, ödeme adımındaki bir gecikmenin ya da hatanın kaynağını bulmak samanlıkta iğne aramaya benzer. Geleneksel log analiz araçları (ELK, Loki) bu kaotik ortamda yetersiz kalır; çünkü size korelasyonu değil, sadece bağımsız olayları sunarlar. Tam da bu yüzden, modern sistem mimarilerinde uçtan uca observability sağlamak ve hata tespitini dakikalara indirmek için opentelemetry ve jaeger ikilisi fiili endüstri standardı haline geldi. Bu rehberde, lafı hiç uzatmadan, production ortamında çalışan servisleriniz için dağıtık izleme (distributed tracing) altyapısını nasıl kuracağımızı ve karmaşık darboğazları nasıl analiz edeceğimizi göreceğiz.

Neden APM Ajanları Değil de OpenTelemetry?

Geçmişte New Relic, Datadog veya Dynatrace gibi kapalı kaynak APM (Application Performance Monitoring) ajanlarını uygulamaya gömmek kolay bir kaçış yoluydu. Ancak bu yaklaşım beraberinde ciddi bir “vendor lock-in” (tedarikçiye bağımlılık) ve kontrol edilemez maliyet artışları getiriyor. OpenTelemetry (OTel), CNCF çatısı altında geliştirilen satıcıdan bağımsız (vendor-agnostic) açık bir standarttır. Yarın öbür gün Jaeger yerine başka bir backend’e (örneğin Grafana Tempo) geçmek isterseniz, uygulama kodunuzda tek bir satır bile değiştirmeden sadece konfigürasyon seviyesinde bu değişikliği yapabilirsiniz.

Peki, arka planda bu iş nasıl dönüyor? İşin sırrı W3C Trace Context standardında saklıdır. Servisler arası HTTP veya gRPC çağrıları yapılırken, istek başlıklarına (headers) benzersiz bir traceparent eklenir:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# Format: sürüm-traceId-spanId-traceFlags

Bu başlık sayesinde, istek hangi servise giderse gitsin, o servisin ürettiği loglar ve span’ler aynı üst kimlik (Trace ID) altında birleşir.

Mimariyi Doğru Tasarlamak: Collector Neden Şart?

Uygulamalarınızdan trace verilerini doğrudan Jaeger’a göndermek ilk bakışta cazip görünebilir. Ancak bu yaklaşım production ortamında intihardır. Jaeger geçici olarak ulaşılamaz olduğunda uygulamanızın bellek (heap) tüketimi tavan yapabilir veya ağ trafiğiniz optimize edilmemiş paketlerle dolabilir.

Doğru yaklaşım, her Kubernetes node’unda bir DaemonSet olarak veya merkezi bir Gateway olarak OpenTelemetry Collector konumlandırmaktır. Uygulamalar trace verilerini localhost üzerindeki Collector’a gönderir (çok düşük gecikme ile), Collector ise veriyi tamponlar (buffer), sıkıştırır (batch) ve asenkron olarak Jaeger’a iletir.

Adım Adım Kurulum: Production-Ready OTel Collector Konfigürasyonu

Aşağıda, yük altında ezilmeyecek, memory limiter ve batching mekanizmaları aktif edilmiş örnek bir otel-collector-config.yaml dosyası yer alıyor. Bu konfigürasyonu Kubernetes ortamında ConfigMap olarak tanımlayabilirsiniz:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 15
  batch:
    send_batch_size: 8192
    timeout: 5s
    send_batch_max_size: 10240

exporters:
  otlp/jaeger:
    endpoint: "jaeger-collector.observability.svc.cluster.local:4317"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/jaeger]

Neden bu işlemcileri (processors) kullandık?

  • memory_limiter: Collector’ın bellek tüketimi belirlenen limitin (%75) üzerine çıktığında, çökmeyi (OOMKilled) önlemek için yeni gelen verileri dropping moduna alır. Güvenli limanda kalmanızı sağlar.
  • batch: Her trace span’ini tek tek ağ üzerinden göndermek yerine, bunları gruplayarak gönderir. CPU ve network overhead’ini dramatik ölçüde düşürür.

Uygulama Seviyesinde Context Propagation (Go Örneği)

OTel SDK’sını uygulamanıza entegre ederken en kritik nokta, bağlamın (context) kaybolmamasını sağlamaktır. Eğer bir HTTP çağrısı yapıyorsanız, HTTP istemcinizi OTel transport katmanı ile sarmalamanız gerekir. Aşağıdaki Go kod bloğu, gelen isteğin trace bağlamını alıp bir sonraki servise nasıl güvenli bir şekilde aktaracağınızı gösterir:

package main

import (
	"context"
	"net/http"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
)

func handleCheckout(w http.ResponseWriter, req *http.Request) {
	// Gelen istekten trace context'i çıkar ve yeni bir span başlat
	ctx := req.Context()
	tracer := otel.Tracer("checkout-service")
	ctx, span := tracer.Start(ctx, "ProcessPayment", trace.WithSpanKind(trace.SpanKindServer))
	defer span.End()

	// Ödeme servisine yapılacak HTTP çağrısını sarmala
	client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
	
	nextReq, _ := http.NewRequestWithContext(ctx, "POST", "http://payment-service/charge", nil)
	resp, err := client.Do(nextReq)
	if err != nil {
		span.RecordError(err)
		span.SetStatus(500, "Payment initiation failed")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	w.Write([]byte("Checkout successful"))
}

Bu kodda otelhttp.NewTransport, arka planda W3C standartlarına uygun traceparent header’ını giden HTTP isteğine otomatik olarak enjekte eder. Manuel müdahaleye gerek kalmaz.

Production SRE Pratikleri: Sampling Rate (Örnekleme) Ayarı

Saniyede 10.000 istek alan bir sistemde her bir isteğin trace verisini saklamak hem Jaeger depolama alanınızı (Elasticsearch/Cassandra) saniyeler içinde doldurur hem de ciddi bir maliyet kalemi oluşturur. Çözüm: Head-based veya Tail-based sampling uygulamaktır.

Uygulama seviyesinde (Head-based) sadece başarılı olan isteklerin %1’ini, hatalı (error status) olanların ise %100’ünü saklamak mantıklıdır. Ancak uygulamanın bir isteğin hata vereceğini en baştan bilmesi imkansızdır. Bu yüzden Collector seviyesinde Tail-based sampling yapılandırmak en profesyonel çözümdür. Collector, trace tamamlanana kadar veriyi belleğinde tutar; eğer trace içinde bir hata kodu veya yüksek gecikme saptanırsa trace’in tamamını saklar, aksi takdirde belirlediğiniz oranda eler.

Bunun için Collector konfigürasyonunuza şu işlemciyi ekleyebilirsiniz:

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 10000
    expected_new_traces_per_sec: 2000
    policies:
      - name: filter_errors
        type: status_code
        status_code: { status_codes: [ ERROR ] }
      - name: filter_latency
        type: latency
        latency: { threshold_ms: 500 } # 500ms üzerindeki tüm trace'leri sakla
      - name: probabilistic_sample
        type: probabilistic
        probabilistic: { sampling_percentage: 5.0 } # Normal trafikten %5 örnek al

Jaeger Arayüzünde Darboğaz Analizi: Nereden Başlamalı?

Her şey kuruldu ve Jaeger UI’a girdiniz. Önünüzde yüzlerce span içeren karmaşık bir trace ağacı duruyor. SRE bakış açısıyla analiz yaparken şu üç altın kuralı unutmayın:

  1. Gap Analizi (Boşluklar): İki ardışık span arasında büyük bir zaman boşluğu varsa, bu durum ağ gecikmesine, kuyrukta bekleyen (message queue) mesajlara veya uygulama içindeki CPU-bound kilitlemelere (mutex contention) işaret eder.
  2. Database Span’leri (N+1 Sorgu Problemi): Eğer tek bir HTTP isteğinin altında yüzlerce ardışık SQL sorgu span’i görüyorsanız, yazılımcılarınız ORM kütüphanesini yanlış kullanmış ve N+1 query tuzağına düşmüş demektir.
  3. Baggage vs Span Attributes Ayrımı: Trace akışı boyunca tüm downstream servislere taşınmasını istediğiniz kritik meta verileri (örneğin tenant_id veya user_tier) “Baggage” olarak ekleyin. Sadece o servise ait verileri (örneğin SQL query string) ise “Span Attribute” olarak tutun.

Sonuç: Kör Noktaları Yok Edin

OpenTelemetry ve Jaeger yatırımı, ilk kurulumda kod değişikliği ve konfigürasyon yükü getirse de, production’da yaşanacak ilk büyük krizde kendini amorti eder. Servislerinizin birbiriyle nasıl konuştuğunu tahmin etmek yerine, onları canlı olarak izleyin. Unutmayın; ölçemediğiniz sistemi yönetemezsiniz.

Category: Genel | LEAVE A COMMENT