Tüm makaleler
Dağıtık Sistemler
Dağıtık Sistemlerde GüvenilirlikBölüm 01

Dağıtık Sistemlerde Idempotency: Bir İsteğin İki Kez İşlenmesi Neden Bu Kadar Kolay?

Bir timeout, başarısız işlem anlamına gelmez. Yeniden deneme mekanizmalarını tasarlarken asıl problem çoğu zaman budur.

6 dk okumaRead in English

Dağıtık sistemlerde karşılaştığımız bazı problemlerin kaynağı karmaşık algoritmalar değildir. Bazen birkaç saniyelik bağlantı kesintisi, oldukça basit görünen bir işlemi ciddi bir tutarlılık problemine dönüştürebilir.

Bir istemci sunucuya istek gönderir. Sunucu isteği alır, işler ve veritabanına kaydeder. Tam yanıt döneceği sırada bağlantı kopar.

İstemci açısından işlem başarısız görünür. Sunucu açısından ise tamamlanmıştır.

Şimdi istemci aynı isteği yeniden gönderirse ne olacak?

İdempotency ihtiyacı tam bu noktada ortaya çıkar.

Başarısız yanıt, başarısız işlem değildir

Bir HTTP isteğinin timeout ile sonuçlanması, karşı taraftaki işlemin gerçekleşmediğini kanıtlamaz.

İstemcinin bildiği tek şey, belirlenen süre içinde beklediği yanıtı alamadığıdır.

Örneğin bir sistemde ödeme alınması, iş emri oluşturulması veya bir cihaz komutunun çalıştırılması gibi işlemler düşünelim.

Bu işlemlerden biri tamamlandığı halde yanıt kaybolursa, otomatik tekrar denemesi aynı işlemin ikinci kez gerçekleştirilmesine neden olabilir.

Burada önemli bir ayrım var:

İsteğin yeniden gönderilmesi ile işin yeniden gerçekleştirilmesi aynı şey olmak zorunda değildir.

İyi tasarlanmış bir sistem aynı mantıksal isteği birden fazla kez alabilir; ancak bunun istenmeyen tekrarlı iş etkileri oluşturmasını önlemelidir.

Idempotency neyi garanti eder?

En temel tanımıyla idempotent bir işlem, tekrarlandığında ilk uygulamadan farklı bir nihai etki oluşturmayan işlemdir.

Ancak pratikte bu tanımın kapsamını doğru belirlemek gerekir.

Bir kaydın durumunu Active olarak ayarlamak çoğu durumda doğal olarak idempotent olabilir. Aynı durumu tekrar yazmak iş sonucunu değiştirmez.

Buna karşılık hesap bakiyesinden 100 birim düşmek veya yeni bir sipariş oluşturmak aynı özellikte değildir. İşlemi ikinci kez çalıştırmak ikinci bir iş etkisi oluşturabilir.

HTTP tarafında PUT metodunun idempotent kabul edilmesi de uygulamanın bütün yan etkilerinin otomatik olarak güvenli olduğu anlamına gelmez. Her çağrıda yeni bir bildirim, olay kaydı veya dış servis işlemi oluşturuyorsak bu etkileri ayrıca değerlendirmemiz gerekir.

Bu nedenle soru yalnızca “Bu endpoint idempotent mi?” olmamalı.

Hangi iş etkisinin, hangi kapsamda ve ne kadar süre boyunca tekrarını engelliyoruz?

Idempotency Key tek başına çözüm değil

Yaygın yöntemlerden biri, istemcinin her mantıksal işlem için benzersiz bir anahtar üretmesidir.

POST /api/payments
Idempotency-Key: 8d9b7c41-...

İstemci aynı ödemeyi ağ hatası nedeniyle tekrar denediğinde aynı anahtarı kullanır.

Sunucu da bu anahtara ait işlemin daha önce başlatılıp başlatılmadığını kontrol eder. Aynı anahtarla farklı bir işlem içeriği gönderilirse bunu da ayrı bir hata olarak ele almak gerekir.

Basit bir uygulama dört adımdan oluşuyor gibi görünür: anahtarı kontrol et, işlemi gerçekleştir, sonucu kaydet ve tekrar gelirse önceki sonucu döndür.

Fakat üretim ortamında problem çoğunlukla bu adımların arasındadır.

Aynı istek eşzamanlı gelirse?

İki istek aynı anahtarla neredeyse aynı anda gelebilir.

Her ikisi de veritabanında henüz kayıt olmadığını görebilir ve işlem başlatmaya çalışabilir.

Bu nedenle yalnızca önce kontrol edip sonra kayıt oluşturmak yeterli değildir.

İşlem sahipliğinin atomik biçimde belirlenmesi gerekir. Benzersiz veritabanı kısıtları, koşullu kayıt oluşturma veya uygun transaction mekanizmaları bunun için kullanılabilir.

Yine de bir isteğin işlem hakkını kazanması, dış sistemdeki işin güvenli biçimde tamamlandığı anlamına gelmez.

İşlem tamamlandı, sonuç kaydedilemedi

Bir ödeme sağlayıcısının işlemi başarıyla gerçekleştirdiğini düşünelim.

Uygulama hemen ardından çöküyor ve ödeme sonucunu kendi veritabanına yazamıyor.

Sistem yeniden başladığında elinde sonucu kesinleşmemiş gibi görünen bir işlem bulunuyor.

Böyle bir durumda işlemi körlemesine tekrar etmek ikinci ödeme riskini doğurabilir. Hiç tekrar etmemek ise süreci belirsiz durumda bırakabilir.

Dolayısıyla idempotency kaydı kadar işlem durumunun nasıl uzlaştırılacağı da önemlidir.

Ödeme sağlayıcısı idempotency anahtarlarını destekliyorsa aynı anahtarın yeniden kullanılması önemli bir koruma sağlayabilir. Desteklemiyorsa işlem kimliği üzerinden sorgulama, mutabakat veya telafi mekanizmaları gerekebilir.

Aynı veritabanındaki iki değişikliği tek transaction içinde yönetebiliriz. Fakat bağımsız bir dış sistemdeki işlemle kendi veritabanımızdaki kaydı tek bir yerel transaction ile atomik hale getiremeyiz.

Endüstriyel sistemlerde başka bir boyut

Endüstriyel haberleşme sistemlerinde benim özellikle önemsediğim ayrımlardan biri, iletişim başarısı ile operasyonel başarı arasındaki fark. Bir komuta yanıt alınması kadar, komut sonrasında cihazın hangi durumda olduğunun doğrulanması da önemli. İletişimin kesildiği veya geciktiği koşullarda, tekrar deneme kararının yalnızca yazılım tarafındaki timeout sonucuna bağlanmaması gerektiğini düşünüyorum.

Bir gateway üzerinden uzak bir cihaza komut gönderildiğini düşünelim. İletişim kesintisi nedeniyle yanıt alınamadığında, komutun uygulanıp uygulanmadığı her zaman bilinemeyebilir.

Burada komutun türü belirleyicidir. Bir parametreyi belirli bir değere ayarlamak ile bir işlemi tetiklemek aynı tekrar davranışına sahip değildir.

Üstelik komutun kabul edilmesi, gerçekleştirilmesi ve sonuç durumunun doğrulanması birbirinden farklı aşamalardır.

Bu nedenle endüstriyel sistemlerde retry politikasını yalnızca timeout süresi veya tekrar sayısıyla belirlemek yeterli değildir. Komutun semantiğini, cihazın mevcut durumunu ve doğrulama imkânlarını birlikte değerlendirmek gerekir.

Bazı durumlarda en doğru yaklaşım komutu yeniden göndermek değil, önce cihazın mevcut durumunu sorgulamaktır.

Mesaj kuyruklarında aynı problem

HTTP istekleri için ele aldığım belirsizlik, mesaj tabanlı sistemlerde de karşımıza çıkar.

Bir tüketici mesajı alır, iş verisini günceller; fakat mesajı başarıyla işlediğini kuyruğa bildirmeden bağlantısı kesilir.

Kuyruk aynı mesajı tekrar teslim edebilir.

Bu, at-least-once delivery yaklaşımında beklenen bir davranıştır.

Çözümlerden biri, işlenen mesajın kimliğini ilgili iş verisini değiştiren transaction içinde saklamaktır. Aynı mesaj tekrar geldiğinde bu kayıt ikinci güncellemeyi önleyebilir.

Ancak transaction dışında gönderilen bir e-postayı, yapılan HTTP çağrısını veya çalıştırılan cihaz komutunu aynı mekanizma otomatik olarak geri alamaz.

Bir mesajın tekrar işlenmemesi ile onun tetiklediği bütün dış etkilerin yalnızca bir kez gerçekleşmesi farklı problemlerdir.

Her işlem için aynı strateji gerekli mi?

Hayır.

Bir durumu son bilinen değerine eşitlemekle para transferi yapmak aynı risk seviyesinde değildir.

Ben böyle bir mekanizmayı değerlendirirken önce şu soruların yanıtlarını aramayı tercih ederim:

  • Tekrarlandığında hangi iş etkileri oluşur?
  • İstemci isteğin sonucunu hangi yollarla doğrulayabilir?
  • Aynı anahtarla eşzamanlı istek gelirse ne olur?
  • Uygulama işlem sırasında çökerse nasıl toparlanır?
  • Anahtarın geçerlilik süresi dolduğunda ne olur?
  • Dış sistem veya fiziksel cihaz söz konusuysa gerçekleşen etki nasıl doğrulanır?

Bunların yanıtları kullanılacak mekanizmayı belirler.

Bazı işlemler doğal olarak idempotent tasarlanabilir. Bazılarında benzersiz iş kimlikleri ve transaction yeterli olabilir. Bazılarında ise kalıcı işlem durumları, uzlaştırma ve gerektiğinde insan müdahalesi gerekir.

Sonuç: Retry yalnızca teknik bir hata yönetimi değildir

Dağıtık sistemlerde retry mekanizmaları çoğu zaman teknik bir dayanıklılık özelliği olarak ele alınır.

Oysa tekrar denenen işlem bir iş etkisi oluşturuyorsa retry politikasının kendisi de iş mantığının parçasıdır.

Ne zaman yeniden deneyeceğimiz kadar, neyi tekrar etmeye izin verdiğimizi de tanımlamak gerekir.

Idempotency bu yüzden yalnızca bir HTTP başlığı, veritabanı kaydı veya middleware özelliğinden ibaret değildir.

İşlemin kimliğini, tekrarın kapsamını, arıza anındaki davranışı ve toparlanma yöntemini birlikte ele alan bir tasarım kararıdır.

Amaç her isteğin yalnızca bir kez ulaşmasını sağlamak değil; tekrar gelen isteklerin sistemde istenmeyen tekrarlı iş etkilerine dönüşmesini engellemektir.

Uygulamalı deney: Aynı mesaj, farklı iş sonucu

Bu konuyu Distributed Reliability Lab kapsamında çalışan bir deneyle de inceledim.

E02 — Duplicate Delivery deneyinde dört iş emrine ait mesajlar, hem normal teslimat hem de kontrollü çökme sonrasında yeniden teslimat senaryolarında tüketiliyor.

Aynı mesajları işleyen iki tüketiciden biri, tekrarlı teslimatları doğrudan işliyor. Diğeri ise işlenen mesaj kimliklerini rezervasyon ve stok güncellemesiyle aynı veritabanı transaction’ında kaydediyor.

Deney sonunda idempotent tüketici doğru stok miktarını korurken, naif tüketici aynı iş emirleri için iki fazla rezervasyon oluşturuyor.

Bu sonuç, idempotency’nin yalnızca teknik bir tekrar kontrolü değil, iş verisinin doğruluğunu koruyan bir mekanizma olduğunu somut biçimde gösteriyor.

E02 — Duplicate Delivery deneyini GitHub’da inceleyin

#idempotency#retry#mesajlaşma#endüstriyel-sistemler