Buğra Şıkel
Apache Kafka’da Veri Sözleşmelerini İhlal Eden 5 Schema Evolution Anti-Pattern’i ve Çözümleri

Giriş
Mikroservis mimarilerinde Apache Kafka bir veri taşıyıcısından (message broker) daha fazlasıdır; servisler arası iletişimde asenkron veri sözleşmelerinin (data contracts) uygulandığı omurgadır. 2021 yılında yönettiğim bir ödeme geçidi (payment gateway) projesinde, upstream ekibinin Avro şemasından tek bir default değerini kaldırması, downstream fraud-detection consumer grubunda 4.2 milyon mesajlık bir lag oluşmasına ve p99 gecikme sürelerinin 120ms’den 18.5 saniyeye fırlamasına neden oldu. Bu kesinti tam 46 dakika sürdü.
Veri sözleşmeleri, tıpkı API sözleşmeleri gibi katı olmalıdır. Confluent Schema Registry (veya AWS Glue, Apicurio) kullanmak tek başına bu sözleşmeleri korumaz. Şema evrimi (schema evolution) kurallarının ihlali, derleme zamanında (compile-time) değil, çalışma zamanında (runtime) ortaya çıkan ve geri alınması (rollback) son derece maliyetli olan consumer çöküşlerine yol açar.
Aşağıda, production ortamında bizzat karşılaştığım, veri sözleşmelerini sessizce bozan 5 temel şema evrimi anti-pattern’ini, çözüm stratejilerini ve trade-off analizlerini detaylandırıyorum.
İçindekiler
- 1. Anti-Pattern: Geriye Dönük Uyumluluğu Bozan Zorunlu Alan (Required Field) Eklemek
- 2. Anti-Pattern: Alan Adı Değişikliklerinde (Rename) Alias Kullanmamak
- 3. Anti-Pattern: Enum Veri Tiplerini Kontrolsüz Genişletmek
- 4. Anti-Pattern: String/JSON Payload İçine Veri Gizlemek (Schema Bypass)
- 5. Anti-Pattern: auto.register.schemas=true Konfigürasyonunu Production’da Açık Bırakmak
- Pratik Öneriler / Production Notları
- Sık Sorulan Sorular
- Sonuç
1. Anti-Pattern: Geriye Dönük Uyumluluğu Bozan Zorunlu Alan (Required Field) Eklemek
Problem
Kafka topic’lerine yazan producer uygulamasına yeni bir iş gereksinimi gelir ve şemaya device_id adında yeni bir alan eklenir. Şema BACKWARD uyumluluk modunda çalışmasına rağmen, bu yeni alana bir varsayılan değer (default value) atanmaz. Yeni producer versiyonu deploy edilir edilmez, eski şemayı kullanan consumer’lar bu yeni mesajları okumaya çalıştıklarında AvroTypeException fırlatarak çökerler.
Çözüm
Şemaya eklenen her yeni alan için mutlak surette mantıklı bir default değer sağlanmalı ve tip tanımı Union (null ve beklenen tip) olarak yapılmalıdır.
// YANLIŞ (Anti-Pattern)
{
"type": "record",
"name": "Transaction",
"fields": [
{ "name": "transaction_id", "type": "string" },
{ "name": "device_id", "type": "string" } // HATA: default yok
]
}
// DOĞRU (Best Practice)
{
"type": "record",
"name": "Transaction",
"fields": [
{ "name": "transaction_id", "type": "string" },
{
"name": "device_id",
"type": ["null", "string"],
"default": null
}
]
}
Ne Zaman Uygulanır?
Veri modeline yeni metrikler, opsiyonel takip ID’leri veya zenginleştirme (enrichment) verileri eklendiğinde. Consumer’ların bu yeni veriyi işlemek için acil bir kod değişikliğine gitmesi gerekmediği durumlarda (BACKWARD compatibility).
Ne Zaman Uygulanmaz?
Regülasyon (örn: GDPR, PCI-DSS) gereği, eklenen alanın kesinlikle işlenmesi zorunlu ise. Bu durumda uyumluluk bilerek kırılır (veya yeni bir topic/versiyon açılır), topic adı v2 olarak güncellenir ve tüm consumer’ların yeni formata geçmesi zorunlu kılınır.
Trade-off Analizi
Union [“null”, “type”] vs Standart Default (örn: “”): String tipler için "default": "" (boş string) kullanmak null pointer exception (NPE) riskini azaltır ancak verinin bilerek mi boş gönderildiği, yoksa eski producer tarafından hiç mi gönderilmediği (null) ayrımını yok eder. Veri mühendisliği (Data Engineering) süreçlerinde analiz tutarlılığı için null kullanmak ve Union tanımlamak her zaman daha güvenlidir.
2. Anti-Pattern: Alan Adı Değişikliklerinde (Rename) Alias Kullanmamak
Problem
Geliştirme ekibi, veritabanı tablolarındaki isimlendirme standartlarını Kafka şemalarına da yansıtmak ister. userId alanı customer_id olarak değiştirilir. Schema Registry bunu teknik olarak userId alanının silinmesi ve customer_id alanının eklenmesi olarak algılar. Eski consumer’lar userId arar, bulamaz ve default değere düşer (veri kaybı yaşanır). Yeni consumer’lar eski mesajları okurken customer_id alanını boş görür.
Çözüm
Avro’nun aliases özelliği kullanılarak, alanın eski adları şema içerisine gömülür. Schema Registry bu sayede iki alanın aynı mantıksal veriyi taşıdığını anlar.
{
"type": "record",
"name": "UserEvent",
"fields": [
{
"name": "customer_id",
"type": "string",
"aliases": ["userId"] // Çözüm noktası
}
]
}
Ne Zaman Uygulanır?
Şirket içi isimlendirme (naming convention) revizyonlarında, legacy sistemlerin modernizasyonunda ve birleşme/satın alma (M&A) sonrası sistem entegrasyonlarında veri modelleri eşitlenirken.
Ne Zaman Uygulanmaz?
Alanın sadece adı değil, iş mantığı da değiştiyse (örneğin userId bir UUID iken, customer_id bir integer değer ise). Bu durumda alias kullanmak tip uyuşmazlığına (TypeMismatchException) neden olur.
Trade-off Analizi
Avro Aliases vs Protobuf Field Tags: Protobuf, alanları isimlerine göre değil numerik tag’lere (string customer_id = 1;) göre eşleştirir. Eğer projenizde sık sık alan adı değişikliği yaşanıyorsa (ki bu mimari bir zafiyettir), Kafka Serialization formatı olarak Avro yerine Protobuf tercih etmek, isim değişikliği kaynaklı kırılmaları temelden çözer.
3. Anti-Pattern: Enum Veri Tiplerini Kontrolsüz Genişletmek
Problem
Sipariş durumunu tutan bir enum (PENDING, COMPLETED) şemada mevcuttur. Producer’a yeni bir REFUNDED durumu eklenir. Şema güncellenir. Consumer’lar henüz bu yeni durumu bilecek şekilde derlenmemiştir (eski şema versiyonunu kullanmaktadırlar). Consumer, REFUNDED değerini okuduğunda, kendi lokal enum tanımında bu değer olmadığı için doğrudan çöker. Kafka topic’inde lag birikmeye başlar.
Çözüm
Avro 1.9.0 ve sonrasında gelen enum default özelliğini kullanarak, bilinmeyen enum değerleri için güvenli bir fallback mekanizması oluşturulur.
{
"type": "record",
"name": "OrderState",
"fields": [
{
"name": "status",
"type": {
"type": "enum",
"name": "StatusEnum",
"symbols": ["UNKNOWN", "PENDING", "COMPLETED", "REFUNDED"],
"default": "UNKNOWN" // Bilinmeyen değerler buraya düşer
}
}
]
}
Ne Zaman Uygulanır?
Durum makineleri (state machines), kategori belirteçleri ve zamanla genişleme ihtimali olan (%99 ihtimalle genişler) herhangi bir kapalı küme (closed-set) veri tipinde.
Ne Zaman Uygulanmaz?
Enum değerinin, finansal bir hesaplama için mutlak doğruluk gerektirdiği durumlarda. (Örn: Vergi tipleri. Bilinmeyen bir vergi tipini “UNKNOWN” yapıp %0 vergi uygulamak felakete yol açar. Bu durumlarda consumer’ın çökmesi (fail-fast), yanlış hesaplama yapmasından daha iyidir).
4. Anti-Pattern: String/JSON Payload İçine Veri Gizlemek (Schema Bypass)
Problem
Geliştiriciler, Schema Registry’nin katı kurallarından ve CI/CD pipeline’larındaki şema doğrulama adımlarından kaçınmak için şemayı aşağıdaki gibi tanımlar. Tüm gerçek veri, payload adındaki bir string alana JSON formatında serileştirilerek gömülür. Bu durum, veri sözleşmesini tamamen anlamsız kılar; CPU üzerinde gereksiz JSON parsing yükü yaratır (p99 süresini 15ms’den 85ms’ye çıkarttığını gözlemledim) ve downstream sistemleri her türlü şema hatasına açık hale getirir.
// KÖTÜ KULLANIM
{
"type": "record",
"name": "Event",
"fields": [
{ "name": "event_id", "type": "string" },
{ "name": "payload", "type": "string" } // Tüm veri burada saklı
]
}
Çözüm
Avro’nun nested records (iç içe kayıtlar) ve karmaşık tipleri (arrays, maps) kullanılarak veri yapısı Schema Registry’ye tam olarak tanıtılmalıdır.
// DOĞRU KULLANIM
{
"type": "record",
"name": "Event",
"fields": [
{ "name": "event_id", "type": "string" },
{
"name": "payload",
"type": {
"type": "record",
"name": "PayloadData",
"fields": [
{ "name": "amount", "type": "double" },
{ "name": "currency", "type": "string" }
]
}
}
]
}
Ne Zaman Uygulanır?
Tüm standart mikroservis iletişimlerinde, veri ambarı (Data Warehouse) aktarımlarında ve stream processing (Kafka Streams / Flink) topolojilerinde.
Ne Zaman Uygulanmaz?
Sistemin sadece bir “pass-through” (geçiş) katmanı olduğu, verinin içeriğiyle kesinlikle ilgilenmediği durumlarda. Örneğin, farklı takımların serbestçe veri attığı ve şemasız depolama yapılan bir S3 raw-data bucket’ına veri taşınırken.
5. Anti-Pattern: auto.register.schemas=true Konfigürasyonunu Production’da Açık Bırakmak
Problem
Kafka producer konfigürasyonlarında yer alan auto.register.schemas parametresi, varsayılan olarak true değerine sahiptir. Bu, uygulamanın her ayağa kalkışında, eğer şemada bir değişiklik varsa bunu otomatik olarak Schema Registry’ye kaydetmeye çalışması anlamına gelir. Production ortamında bu durum; farklı pod’ların yarış durumuna (race condition) girmesine, registry üzerinde yüzlerce gereksiz şema versiyonu oluşmasına ve uyumsuz şemaların test süreçlerinden geçmeden canlıya alınmasına sebep olur.
Çözüm
Production ve Pre-Prod ortamlarında bu parametre kesinlikle false yapılmalı ve client’ın use.latest.version=true parametresini kullanması sağlanmalıdır. Şema kayıt işlemi CI/CD pipeline’ı üzerinden (Örn: schema-registry-maven-plugin veya Terraform kullanılarak) yönetilmelidir.
// Spring Boot / Kafka Client Konfigürasyonu
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class);
props.put("schema.registry.url", "http://schema-registry:8081");
// GÜVENLİK KİLİDİ: Runtime'da şema kaydını engelle
props.put("auto.register.schemas", "false");
props.put("use.latest.version", "true");
Ne Zaman Uygulanır?
Dev, Test veya Local (Docker-compose) ortamları haricindeki tüm üst ortamlarda (Staging, UAT, Production). Sistem kararlılığının öncelikli olduğu her yerde.
Ne Zaman Uygulanmaz?
Geliştiricilerin kendi bilgisayarlarında hızlı iterasyon yaptığı (Local) geliştirme süreçlerinde. Burada manuel şema kaydı zaman kaybı olacağından true bırakılabilir.
Pratik Öneriler / Production Notları
Data contract yönetiminde sadece kodu düzeltmek yetmez, altyapıyı da gözlemlemek gerekir. Production tecrübelerimden süzülen checklist:
- DLQ (Dead Letter Queue) Entegrasyonu: Consumer tarafında
ErrorHandlingDeserializerkullanarak şema uyumsuzluğundan (DeserializationException) kaynaklı hataları ayrı birtopic_name_dlqkuyruğuna atın. Böylece zehirli mesaj (poison pill) tüm partition okumasını durdurmaz. - Monitoring: JMX üzerinden
schema-registry-request-latency-maxveschemas-created-per-secmetriklerini Prometheus/Grafana üzerinde izleyin. Beklenmeyen bir şema yaratım artışı, kontrolsüz bir uygulamanın (auto.register.schemas=true kalmış) sisteme dahil olduğunu gösterir. - CI/CD Kalite Kapısı: Confluent’in Maven/Gradle plugin’lerini kullanarak, build aşamasında
test-compatibilitykomutunu çalıştırın. Şema evrimi kurallarını bozan (compatibility kırılması yaratan) bir commit, daha kod review aşamasındayken pipeline’ı patlatsın.
Sık Sorulan Sorular
FORWARD ve BACKWARD uyumluluk modları arasındaki temel fark nedir?
BACKWARD modunda, eski şemayı kullanan bir consumer, yeni şemayla üretilmiş mesajları okuyabilir (Yeni alanlar için default değer gerekir). FORWARD modunda ise, yeni şemayı kullanan bir consumer, eski şemayla üretilmiş mesajları okuyabilir. Genellikle data pipeline’larında BACKWARD veya FULL (ikisini de kapsar) tercih edilir.
Avro mu Protobuf mu kullanmalıyım?
Eğer Kafka’yı sadece stream processing (KSQL, Flink) ve veri ambarı beslemesi için kullanıyorsanız (Hadoop, Hive uyumluluğu sebebiyle) Avro 1.11.x sürümleri rakipsizdir. Ancak gRPC kullanan yoğun mikroservis mimarileriniz varsa ve alan adı değişiklikleri sıklıkla yaşanıyorsa, numerik tag tabanlı çalışan Protobuf 3.x’e yönelmek uyumluluk krizlerini minimize eder.
Schema Registry çökerse Kafka cluster’ım durur mu?
Hayır. Kafka Producer ve Consumer client’ları, daha önce okudukları/yazdıkları şemaları lokal memory’lerinde (veya disk cache) tutarlar. Schema Registry 7.3.0 ve sonrasında, eğer yeni bir şema kaydedilmeyecekse veya client’ın cache’inde var olan bir ID (örneğin ID: 45) kullanılacaksa, sistem registry kapalı olsa bile p99 hızlarından taviz vermeden çalışmaya devam eder.
DLQ mekanizması şema hatalarında tam olarak nasıl kurgulanmalı?
Spring Kafka kullanıyorsanız ErrorHandlingDeserializer sınıfını key ve value deserializer’ı olarak sarmalayın (wrap). Gerçek deserializer’ı (Örn: KafkaAvroDeserializer) delege edin. Hata durumunda mesaj deserialize edilmeye çalışılmaz, raw (byte[]) hali ile DLQ topic’ine header’larına exception stacktrace’i eklenerek yönlendirilir.
Sonuç
Apache Kafka’da veri sözleşmeleri, yazılım mimarisinin kırılgan olduğu noktalardan biridir. Yukarıda incelediğimiz 5 anti-pattern, çoğu zaman geliştirici dikkatsizliğinden ziyade, şema evriminin getirdiği çalışma zamanı (runtime) etkilerinin öngörülememesinden kaynaklanır. Şema değişikliklerini kod değişiklikleri ile aynı ciddiyette ele almak, veri akış platformlarının sürdürülebilirliği için şarttır.
Hemen bugün, production ortamındaki Kafka client konfigürasyonlarınızı kontrol ederek auto.register.schemas değerinin false olduğundan emin olun ve CI/CD süreçlerinize şema uyumluluk testlerini (compatibility checks) ekleyin. Bu basit aksiyonlar, gecenin bir yarısı alacağınız P1 seviyesindeki bir “Consumer Lag Alert” uyarısını engelleyecektir.
Bunları da beğenebilirsiniz

Monolitik CRUD Mimariden Event Sourcing’e Geçiş: State Mutasyonlarını Immutable Loglara Kademeli Taşıma Rehberi
Production ortamında veri kaybı yaratan UPDATE komutlarından kurtulup, p99 gecikmesini 400ms’den 18ms’ye düşüren Event Sourcing mimarisine aşamalı geçiş.
Devamını Oku

Kurumsal Refactoring İçin Depo Seviyesinde AI Ajanları: İnsan-Döngüde (Human-in-the-Loop) Geri Bildirim Mekanizmaları Tasarlamak
Kurumsal yazılım projelerinde teknik borcu azaltmak için depo seviyesinde otonom ajanların nasıl tasarlanacağını ve insan denetimiyle güvenli refactoring süreçlerinin nasıl işletileceğini inceleyin.
Devamını Oku

WebSocket Yatay Ölçeklemesinde Redis Pub/Sub mu NATS mı? Mesaj İletim Garantisi Karar Matrisi
WebSocket yatay ölçeklemesinde backplane seçimi: Redis 7.2 Pub/Sub ve NATS 2.10 JetStream karşılaştırması, p99 gecikme metrikleri ve karar çerçevesi.
Devamını Oku