Apache Kafka’da Dağıtım Stratejileri ve Tutarlılık/Erişebilirlik Tercihleri
Merhaba VBO okuyucuları,
Bu yazımda öncelikle Apache Kafka’nın kullandığı mesajlaşma modellerinden ve mesaj dağıtım stratejilerinden bahsedeceğim. CAP teoremine göre dağıtık sistemler tutarlılık (Consistency), erişebilirlik (Availability) ve bölünebilme toleransı (Partition tolerance) özelliklerinden aynı anda en fazla iki tanesine sahip olabilir. Kafka’da da tutarlılık ve erişebilirlik arasında bir seçim (tradeoff) vardır. Olası senaryolara göre sisteminizin tutarlılığının veya erişebilirliliğinin yüksek olmasını isteyebilirsiniz ancak ikisine aynı anda sahip olamazsınız. Yazının ilerleyen bölümünde bu konudan ve bununla ilgili konfigürasyon ayarlarından bahsedeceğim. Kafka’yla ilgili bilgilerinizi tazelemek için ilk yazıya buradan göz atabilirsiniz.
Mesajlaşma Modelleri ve Kafka
Mesajlaşma sistemleriyle ilgili iki geleneksel model vardır. Bunlar kuyruk (queue) ve yayın/abone (publish/subscribe) modelleridir. Her modelin avantajı ve dezavantajı vardır.
Kuyruk modeli uçtan uca bir mesaj iletim modelidir. Veriler bir kuyruk yapısında birikir, consumer veya consumer’lar kuyruktaki veriyi alır. Her bir veri tek consumer’a gider. Birden çok consumer olması durumunda dağıtım işlemini ölçeklendirebilmesi (scalability) avantajıdır. Ancak, veriler kaynak sunucuda (kuyrukta) sıralı bir şekilde tutulmasına rağmen, consumerlara dağıtım işlemi senkronize bir şekilde yapılamadığı için verilerin consumer’lara sıralı bir şekilde ulaşamaması problemi ortaya çıkar. Bu problemi birçok sistem, ayrıcalıklı consumer “exclusive consumer” denen tek bir sunucudan veri çeken tek consumer ile çözmeye çalışmıştır. Bu sefer de tek bir consumer olduğu için paralel işlem gücü kullanılamamaktadır. Kuyruk modelinin bir diğer dezavantajı da verinin birden çok okunamamasıdır (multi-subscriber olmaması). Veri bir kez consumer tarafından okununca kuyrukta tutulmaz, bu yüzden tekrar başka consumerlar tarafından okunamaz.
Klasik yayın/abone (publish/subscribe) modelinde mesajlar bir topic’e gönderilir. Bu topic’e abone olan tüm consumerlara mesaj iletilir. Mesaj bir kez iletilince topic’ten uçmaz, verinin ne kadar süre tutulacağıyla ilgili stratejiler vardır. Veri farklı abonelere tekrar tekrar iletilebildiği için multi-subscriber’dır. Ancak tüm veri olduğu gibi bütün abonelere iletildiği için ölçeklendirme yapılamamaktadır.
Kafka’da consumer group kavramı sayesinde iki modeli de etkin bir şekilde kullanabilmektedir. Kuyruk kullanımında Kafka bir topic’deki verileri aynı consumer grupta bulunan consumer’lara ölçeklendirerek dağıtabilmektedir. Yayın/abone kullanımındaysa Kafka farklı consumer gruplara multi-subscriber şekilde aynı mesajları iletebilmektedir. Kuyruk modelinde birden çok consumer kullanılması durumunda sıralamayı garanti edememesi ve tek consumer olması durumunda da paralel işlem kabiliyetini yitirmesi problemi Kafka’da her partition’a birer consumer atandığı senaryolarla çözülmüştür. Böylelikle hem sıralı bir şekilde veri iletimini garanti eder hem de paralel veri aktarmı yaparak hız problemini çözer.
Consumer Offset ve Mesaj Dağıtım Stratejileri
Kafka’da consumer’lar partition’dan en son okudukları offset’leri işlerler. Bu consumer’ın bulunduğu sunucunun çökmesi veya yeniden başlaması gibi durumlarda kaldığı yerden devam etmesi açısından önemlidir. Kafka bunun için bir consumer grupta bulunan tüm consumer offset’leri saklar. Daha önceleri zookeeper’da tutulan offset’ler, artık Kafka’da group cordinator denilen bir broker’da saklanır. Bu offset’ler group cordinator’de __consumer_offsets denilen bir topic’de tutulur. Consumer’lar bu topic’e kaldıkları offset’leri işlerler ve okuma sırası kendilerine geldiği zaman yine bu topic’e giderek en son kaldıkları offset’i kontrol ederek bir sonraki veriyi alırlar.
Consumer offset’lerin işlenmesi üç farklı dağıtım stratejisine göre farklılık gösterir.
- At Most Once: En fazla bir kez anlamına gelen bu stratejide consumer mesajı veya mesaj yığınını alır almaz offseti log’a kaydeder. Ancak consumer mesajı alır almaz (mesajı işlemeden ve kaydetmeden önce) offseti işlediği için, işlem sırasında bir hata olursa consumer en son işlediği offset’ten mesajları almaya devam edeceği için mesaj kaybolur. Mesaj ya bir kez iletilir ya da kaybolur, iletilemez bu yüzden en fazla bir kez denir. Daha hızlı ve daha az maliyetli bir yöntem olmasına rağmen mesajın kaybolma riski vardır. Mesela terabyte’larca yapısal olmayan log verimizi analiz etmek için Kafka’yla Spark’a aktarıyorsak ve arada kaybolabilecek loglar bizim analizimizi çok etkilemeyecekse bununla birlikte hızı da önemsiyorsak bu strateji kullanılabilir.
- At Least Once: En az bir kez anlamına gelen bu stratejide consumer mesajı işlendikten/kaydettikten sonra offseti işler. Mesaj yığınını okurken çıkabilecek (restart gibi) problemlerde önceki işlediği offset’ten verileri okumaya devam eder. Ancak işlem bittikten sonra offset’i işlediği için tam restart olmadan önce yığından almış olduğu ve henüz hedefe ulaşmadığı için offset’i de işlenmemiş verileri tekrar alabilir. Böyle durumlarda aynı mesaj iki kez alınmış (duplicate) olabilir. Bundan dolayı süreç çiftlerden etkilenmeden mesajlar tekmiş gibi devam edebiliyor (idempotent) durumda olmalıdır. Mesela Kafka’yla ilişkisel veritabanımıza yapısal veri aktarımı yapıyorsak, her bir veri önemliyse, veri kaybına tahammülümüz yoksa ve verimiz çift kayıtlardan etkilenmeyecek bir şekildeyse (örneğin her veri bir primary keye sahipse) bu strateji kullanılabilir.
- Exactly Once: Her mesajın aktarılmasını ve bir kez aktarılmasını garanti eder. Bunun için Kafka’ya 0.11 sürümünden itibaren gelen transactional producer ve consumer kullanılır. Transactional süreçlerde ya tüm veriler olduğu gibi iletilir ya da hiçbiri iletilmez. Bu teknikte veriler bir Kafka topic’ten consume edilir ve başka bir topic’e (çıkış topic’ine) produce edilir. Consumer’ın güncel pozisyonu farklı bir topic’de tutulur. Çıkış topic’inin offset’ine veri yazılırken bu pozisyonların olduğu topic’le aynı transaction’a tabi tutulur. Transaction’da işler yolunda gitmezse consumer’ın pozisyonu başlangıç değerine geri getirilir, çıkış topic’indeki veriler de hedef sunucunun consumer’larına iletilmez. Bu tekniğin uygulanabilmesi için hedef sistemin de bu konuda Kafka’yla işbirliğine girebilecek uygun yapıda olması gerekmektedir. Veri aktarımında “tam olarak bir kez aktarım” ibaresi herkesin kulağına hoş gelse de, oldukça maliyetli olduğu için streaming gibi işler dışında veri kaybı hassasiyeti olan senaryolarda bu teknik yerine at least once‘ın kullanılması daha uygulanabilir görülmektedir.
Kafka’nın Leader Seçim Stratejisi
Kafka’da topic’lerin her partition’u birden fazla sunucuda saklanır. Bu sunucular arasında bir leader bulunur. Leader’le bire bir senkron olan, aynı verileri içeren, leader’e bir şey olması durumunda yerine geçen replica’larına ISR (in-sync replica) denir. Leader’in ISR olmayan normal replica’ları da vardır. Kafka’nın veri kaybına karşı garantisi, en az bir ISR’nin ayakta kaldığı durumlarda geçerlidir. Eğer bir partition’un tüm replica’ları offline duruma düşerse bu garanti geçerliliğini yitirir. Bununla birlikte tutarlı bir sistem tüm replica’ların offline olması (çökmesi, kapanması vs.) gibi durumları da düşünmek zorundadır. Kafka’da böyle durumlarda iki yoldan birini izlenir:
- ISR’ler içindeki bir replica’nın yeniden online olmasını beklenir ve tüm datalara sahip olacağı düşüncesiyle leader olarak atanır.
- İlk online olan replica, ISR olup olmadığına bakılmaksızın leader olarak atanır.
Bu aslında tutarlılık ile erişebilirlik arasında bir ikilemdir. Eğer ISR’lerden birini beklemeyi seçersek, in-sync replica yeniden ayağa kalkana kadar erişilemez durumda oluruz. ISR çöktüyse veya bir şekilde verilerinden bir kısmını kaybettiyse, yeniden leader olduğunda bu verileri kalıcı olarak kaybetmiş oluruz. Yani ISR’leri beklemeyi tercih etsek bile veri kaybetme ihtimali sıfıra inmez. Diğer yandan ISR olmayan bir replica leader olarak seçilirse, tüm verileri içermemesine rağmen, süreç bu replica’nın verileri ve offset’leri üzerinden devam edecektir. Tutarlı bir yöntem olmayacaktır ama normal replica daha önce ayağa kalkarsa, sistem daha hızlı bir şekilde erişebilir olacaktır.
Kafka 0.11 sürümünden itibaren, tutarlı bir replica’yı bekleme stratejisini izlediği için default olarak ilk seçenek seçilidir. Ancak erişebilirlik süresinin tutarlılığa tercih edildiği senaryolarda ikinci yol, konfigürasyon ayarlarında bulunan unclean.leader.election.enable özelliği true olarak değiştirilerek seçilebilmektedir.
Tutarlılık/Erişebilirlik Seçimi
Kafka’ya veri gönderirken producer’lar, broker’a verinin ulaşıp ulaşmadığına dair teyit bilgisi isteyip istemediğini acks 0,1 ve -1 (all) ile seçebiliyorlardı. Ack parametresinin “all” olduğu zaman gelen teyit tüm replica’ların veriyi aldığı anlamına gelmez; in-sync olan replica’ların veriyi aldığını gösterir. ISR olmayan replica’lar için garanti yoktur. Örneğin bir partition, iki in-sync replica (ISR) ile konfigüre edildiyse ve bir replica offline olduysa sorun olmayacaktır. Ancak diğer replica’mız da çökerse veri kaybı olacaktır. Bu partition’da maximum availability sağlansa da, veri dayanıklılığını erişebilirliliğe tercih eden kullanıcılar bu davranış biçimini tercih etmeyeceklerdir. Kafka mesaj dayanıklılığını erişebilirliliğe tercih edilen senaryolar için iki konfigürasyon ayarı sunar:
- unclean.leader.election.enable: Yukarıda da bahsettiğim bu özelliği true yapmak tüm replica’ların erişilemez oldukları durumlarda, veri kaybetme riskini göze alarak ilk ayağa kalkan replica’yı ISR olup olmadığına bakmaksızın leader olarak atar. Bu özellik false yapılarak (Kafka 0.11’den itibaren default olarak false’dur) veri kaybetme riskine karşı erişilemez olmayı göze alınabilir.
- transaction.state.log.min.isr: Minimum kaç ISR’miz olması gerektiğini tanımlarsak, topic partition’umuza veri produce etmek ancak en az o sayıda ISR’miz varsa mümkün olabilecektir. En azından belirttiğimiz kadar ISR’nin veriyi alacağını garanti eder. Bu ayarı etkinleştirmek için tüm ISR’lerden verinin alındığına dair teyit gönderilebilmesi için öncelikle acks parametremizi all yapmalıyız. Bu ayar da tutarlılık ile erişebilirlik arasında bir seçim sunar. Minimum ISR sayımız ne kadar fazla olursa Kafka o kadar tutarlılık vaat eder çünkü ISR sayısı arttıkça veri kaybı ihtimali azalacaktır. Bununla birlikte erişebilirlik azalacaktır çünkü in-sync replica’lar minimum sayının altına düşerse, yeniden aktif oluncaya kadar veri aktarım işlemi duracaktır.
Kafka’la ilgili tüm konfigürasyon ayarları bunlarla sınırlı değildir. Çok daha ileri seviye ayarlar bulunmaktadır. Benim de yazımda faydalandığım Kafka’nın resmi dökümantasyonunda Kafka’yla ilgili çok yönlü bilgiye ulaşabilirsiniz.
Yeni yazılarda görüşmek ümidiyle.