Dağıtık akış sistemlerinde veri bütünlüğünü sağlamak, modern mimarinin en zorlu yönlerinden biridir. Apache Kafka varsayılan olarak en az bir kez (at-least-once) ve en fazla bir kez (at-most-once) teslimatı sağlarken, finansal işlemler, envanter yönetimi ve sağlık verisi işleme gibi kritik uygulamalar için tam bir kez semantiğini (exactly-once semantics) sağlamak esastır. Bu kılavuzda, ağ arızaları veya uygulama çökmeleri durumunda bile mesajların tam olarak bir kez işlendiğinden emin olmak için Kafka'nın işlemsel çerçevesini ve idempotent üreticileri nasıl kullanacağımızı inceleyeceğiz.
Problemi Anlama
Dağıtık sistemlerde, ağ bölünmeleri, tüketici çökmeleri ve üretici yeniden denemeleri, yinelenen mesajlara veya veri kaybına yol açabilir. Uygun önlemler alınmadığında, bir tüketici aynı kaydı iki kez işleyebilir ve bu da tutarsız bir duruma neden olabilir. Örneğin, bir bankacılık uygulaması iki adet aynı "100 $ çek" mesajını alırsa, kullanıcının hesap bakiyesi yanlışlıkla 200 $ düşer. Tam bir kez semantiği, her kaydın aşağı akış durumuna tam olarak bir kez uygulandığından emin olarak bu belirsizliği ortadan kaldırır.
Temel Bileşenler: Idempotent Üreticiler ve İşlemler
Tam bir kez semantiğini sağlamak için, birlikte çalışan iki temel mekanizmaya ihtiyacınız vardır:
- Idempotent Üretici: Aynı mesajın birden fazla örneğinin üretilmesini önler. Kafka broker'ları, aynı üretici oturumundan gönderilen mesajları ayırt etmek için üretici kimliklerini ve sıra numaralarını takip eder.
- İşlemler (Transactions): Birden fazla üretim ve tüketim işlemini tek bir atomik iş birimi olarak gruplamanıza olanak tanır. İşlemin herhangi bir kısmı başarısız olursa, tüm işlem geri alınır.
Idempotent Bir Üretici Uygulama
Öncelikle, Kafka üretici yapılandırmanızda idempotence'ı (tekrarlanabilirliği) etkinleştirin. Bu, enable.idempotence=true değerini ayarlayarak yapılır. Not: Idempotence etkinleştirildiğinde, mesajların onaylanmadan önce tüm senkronize replikalara (in-sync replicas) yazıldığından emin olmak için acks değerinin all olarak ayarlanması gerekir.
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class IdempotentProducerExample {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// Idempotence'ı etkinleştir
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.ACKS_CONFIG, "all");
KafkaProducer producer = new KafkaProducer<>(props);
// Mesajları gönder...
producer.close();
}
}
Tam Bir Kez İşleme İçin İşlemleri Kullanma
Uçtan uca tam bir kez işleme için, idempotent bir üreticiyi aynı işlemde yer alan bir tüketiciyle birleştirmelisiniz. Buradaki anahtar nokta, tüketici grubunu belirli bir işlemsel oturuma bağlamak için transactional.id yapılandırmasını kullanmaktır.
Adım 1: Üreticiyi İşlemsel Kimlikle Yapılandırma
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id");
producer.initTransactions();
Adım 2: Bir İşlem Başlatma, Veri İşleme ve Taahhüt Etme (Commit)
producer.beginTransaction();
// Mesajları tüket
ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord record : records) {
// Kaydı işle (örn: dış sistemi güncelle)
process(record);
// Sonucu aşağı akış konusuna (topic) üret
producer.send(new ProducerRecord<>("output-topic", record.key(), record.value()));
}
// İşlemi atomik olarak taahhüt et (commit)
producer.commitTransaction();
// Offset taahhüdü burada isteğe bağlıdır, çünkü Kafka offset'leri işlem günlüğü üzerinden yönetir
consumer.commitSync();
Pratik Düşünceler
- Zaman Aşımları (Timeouts): İşlemlerin erken sona ermesini önlemek için
transaction.timeout.msvesession.timeout.msdeğerlerini uygun şekilde ayarlayın. - Tüketici Grubu Koordinasyonu: Ölçeklendirme (scale-out) senaryolarında, üretici-tüketici çiftinin tüm örneklerinde aynı
transactional.iddeğeri kullanılmalıdır. Kafka, her işlemsel kimlik için aynı anda yalnızca bir örneğin aktif olduğundan emin olur. - Aşağı Akış Sistemleri: Kafka'daki tam bir kez semantiği, mesajın Kafka'ya yalnızca bir kez yazıldığını garanti eder. Aşağı akış sisteminiz (örn: bir veritabanı) idempotent değilse, tüketici seviyesinde de ayırt etme (deduplication) mantığı uygulamanız gerekir.
Sonuç
Apache Kafka'da tam bir kez semantiğini uygulamak, idempotent üreticilerin, işlemsel API'lerin ve doğru yapılandırmanın dikkatli bir kombinasyonunu gerektirir. Kafka'nın yerleşik işlemsel desteğinden yararlanarak, yüksek derecede dağıtık ortamlarda bile veri tutarlılığını koruyan, sağlam ve arızalara dayanıklı akış uygulamaları geliştirebilirsiniz. Kurulum, basit en az bir kez teslimatından daha karmaşık olsa da, veri bütünlüğü garantisi, misyon kritik sistemler için bunu değerli bir yatırım haline getirir.