Sipariş iki kez düştü, stok tutmuyor: entegrasyonlarda veri senkronizasyonu neden bozulur?
Entegrasyonlar bozulur, çünkü ağ üzerinden mesaj teslimi en iyi ihtimalle "en az bir kez" çalışır ve entegrasyonların çoğu bu gerçeğe göre tasarlanmaz. Aynı olay iki kez gelir, olaylar üretildikleri sıradan başka bir sırada gelir, bazıları hiç gelmez. Üç belirtinin de ayrı çözümü var: her olay için bir idempotency anahtarı ve o anahtar üzerinde veritabanı seviyesinde benzersiz kısıt, artırma yerine mutlak değer yazan bir işleyici, gönderen tarafta outbox tablosu, iki sistemi düzenli olarak karşılaştıran bir mutabakat işi. Bu dördü yoksa entegrasyon aylarca çalışıyor gibi görünür, sonra ayın en yoğun gününde tutmaz.
"Tam bir kez teslimat" diye bir şey yok
Göndericinin çözemediği bir belirsizlik var. İsteği yolladı, cevap gelmedi. İstek hiç ulaşmadı mı, yoksa ulaştı, işlendi ve cevap mı kayboldu? Bu ikisini dışarıdan ayırt etmenin yolu yok. Gönderici tek mantıklı şeyi yapar, tekrar dener. Siz de aynı olayı ikinci kez alırsınız.
Bu bir arıza değil, ilan edilmiş davranış. Stripe kendi dokümanında açıkça yazıyor: uç noktanız zaman zaman aynı olayı birden fazla kez alabilir. Shopify aynı şeyi söylüyor, mükerrer teslimatı en aza indirdiklerini ama uygulamanızın aynı webhook'u birden fazla kez alabileceğini belirtiyor. Yani ikinci sipariş kaydı sizin altyapınızın kalitesiyle değil, protokolün doğasıyla ilgili. Sektörün üzerinde uzlaştığı çözüm de şu: en az bir kez teslimat artı idempotent işleme, pratikte tam bir kezin yerini tutar.
Sıra garantisi de yok, bu daha az bilinir
Mükerrer kayıt en azından göze batar. Sıra bozukluğu sessizce yanlış veri üretir. Stripe olayların üretildikleri sırada teslim edileceğini garanti etmediğini yazıyor ve kendi örneğini veriyor: bir abonelik oluşturulduğunda customer.subscription.created, invoice.created, invoice.paid ve charge.created olayları çıkar, ama bunların size geliş sırası farklı olabilir. Shopify tarafında bir ürünün products/update bildirimi, products/create bildiriminden önce düşebiliyor.
Pratikte anlamı şu: 10:00'da üretilen "stok 40" mesajı, 10:05'te üretilen "stok 37" mesajından sonra elinize geçerse, stoku geri sararsınız. Sistem hata vermez, log temizdir, sadece rakam yanlıştır. Bu tür bir sapma genelde haftalar sonra, biri elle sayım yaptığında fark edilir.
Delta yazmayın, mutlak durum yazın
Bu iki sorunun tek satırlık panzehiri var. stok = 40 yazan bir işleyici aynı mesajı iki kez alsa bile doğru sonucu verir. stok = stok - 3 yazan bir işleyici ikinci teslimatta veriyi bozar. Aynısı bakiye, puan, sayaç ve durum alanları için de geçerli: mümkün olan her yerde artırma yerine son değeri yazın.
Sıra sorunu için de kaynak sistemin sürüm bilgisini taşıyın. Kaydınızda kaynak_surum (ya da kaynağın ürettiği güncelleme zamanı) tutun ve gelen mesajın sürümü elinizdekinden küçükse yazmayı reddedin. Kritik nokta şu: karşılaştırmayı mesajın size ulaştığı ana göre değil, kaynağın olayı ürettiği ana göre yapın. Varış sırası zaten güvenilmez olan şeydir.
Idempotency anahtarı ve gerçek garantiyi veren şey
Anahtar olarak kendi ürettiğiniz bir değeri değil, göndericinin teslimat kimliğini kullanın. Stripe olay kimliği (evt_...), Shopify X-Shopify-Webhook-Id başlığı, GitHub X-GitHub-Delivery başlığı gönderiyor. Hiçbiri yoksa, yükün değişmeyen alanları üzerinden kararlı bir özet (hash) hesaplayın.
Asıl mesele bu anahtarı nasıl kontrol ettiğiniz. Yaygın hata, önce SELECT ile "bu kimlik var mı" diye bakıp sonra işlemek. İki kopya aynı anda geldiğinde ikisi de "yok" cevabını alır ve ikisi de işler. Gerçek garantiyi veren şey benzersiz kısıttır: anahtarı bir tabloya UNIQUE indeksle yazın, yazma işlemini iş mantığıyla aynı transaction içine alın, çakışma hatası aldığınızda sessizce başarı dönün. Kontrolü uygulama katmanına değil veritabanına yaptırın.
Saklama süresini de göndericinin yeniden deneme penceresine göre seçin. Shopify başarısız çağrıları dört saat içinde sekiz kez deniyor. Stripe canlı modda üç güne kadar, artan bekleme süreleriyle deniyor. Dedup kayıtlarını bir saat sonra silen bir sistem, üç günlük yeniden deneme politikasına sahip bir gönderici karşısında er geç mükerrer kayıt üretir. Bir de şu detay: eğer webhook sizin tarafınızda başka bir API'yi tetikliyorsa (ödeme, fatura, kargo etiketi), kendi olay kimliğinizi o API'ye Idempotency-Key olarak geçirin. Bu başlık hâlâ IETF'te taslak aşamasında, RFC değil, o yüzden ismini ve davranışını her sağlayıcıda ayrıca doğrulayın.
Beş saniyeniz var ve bu sandığınızdan önemli
Shopify'ın webhook için bir saniye bağlantı, toplam beş saniye zaman aşımı var. Bu süre içinde cevap dönmezseniz teslimat başarısız sayılır, yani yeniden denenir, yani mükerrer olur. Fark ettiniz mi: yavaş işleyici mükerrer kaydın kurbanı değil, sebebidir.
Doğru kalıp basit. İşleyici üç şey yapar: imzayı doğrular, yükü bir kuyruğa ya da tabloya yazar, 200 döner. ERP çağrısı, e-posta, PDF üretimi, hepsi bu cevaptan sonraki asenkron adımda çalışır. Stripe da aynısını söylüyor, zaman aşımına yol açabilecek karmaşık mantıktan önce başarılı durum kodunu dönmenizi ve olayları asenkron kuyrukla işlemenizi öneriyor. Bunun ikinci bir faydası var: ayın ilk günü tüm abonelikler yenilendiğinde gelen ani yığın, senkron çalışan bir uç noktayı devirir, kuyruğa yazan bir uç noktayı devirmez.
Bir uyarı daha. Shopify'da 24 saat içinde tekrarlanan başarısızlıklardan sonra webhook aboneliği tamamen kaldırılıyor. Cuma akşamı yapılan kötü bir dağıtım, pazartesi sabahı "hiç bildirim gelmiyor" olarak karşınıza çıkabilir ve bunun sebebi kod değil, silinmiş bir aboneliktir.
Gönderen taraftaki ikiz sorun: çift yazma
Şimdiye kadar alıcı taraftaydık. Gönderen tarafta simetrik bir tuzak var. Kodunuz siparişi veritabanına yazar, sonra ERP'nin API'sini çağırır. İkinci adım hata verirse iki sistem farklı şey söyler. Sırayı ters çevirirseniz bu sefer henüz kesinleşmemiş, sonradan geri alınacak bir işlem için olay göndermiş olursunuz. AWS'nin desen kılavuzunda buna çift yazma (dual write) problemi deniyor: tek bir iş adımı iki ayrı sisteme yazıyorsa, birinin başarısız olması tutarsızlık bırakır.
Çözüm outbox deseni. İş kaydını ve gönderilecek olayı aynı yerel transaction içinde, aynı veritabanına yazarsınız. Ayrı bir aktarıcı süreç outbox tablosunu okur, mesajı kuyruğa basar ve satırı temizler. Transaction geri alınırsa outbox satırı da geri alınır, yani hiç gönderilmemiş olay için tutarsızlık oluşmaz. AWS'nin aynı kılavuzdaki uyarısını atlamayın: aktarıcı mesajı yine birden fazla kez gönderebilir, dolayısıyla tüketici tarafı idempotent olmak zorunda. Outbox mükerrer teslimatı çözmez, kayıp olayı çözer. Yazan koda hiç dokunamıyorsanız alternatif, değişiklik yakalama (CDC) ile veritabanı günlüğünden olay üretmektir.
Yeniden deneme politikası, arızayı büyütmenin en kolay yolu
ERP zaten yük altında yanıt vermiyorsa, üzerine gönderdiğiniz yeniden denemeler yükü artırır ve sistemi büsbütün kilitler. Amazon'un mühendislik kütüphanesindeki formülasyon net: yeniden denemeler bağımlı sistemdeki yükü çoğaltabilir, bu yüzden üstel geri çekilmeye (exponential backoff) bir üst sınır konur ve araya rastgele bir gecikme (jitter) eklenir. Jitter olmazsa tüm istemciler aynı anda geri gelir ve dalga tekrar eder.
İkinci kural daha çok ihlal ediliyor: yığının her katmanında yeniden denemeyin. HTTP istemcisi üç kez, kuyruk beş kez, üstteki iş katmanı iki kez denerse, tek bir başarısız istek otuz isteğe dönüşür. Amazon'un pratiği yığında tek bir noktada yeniden denemek. Bir de biten yerin ne olduğunu tanımlayın: N denemeden sonra mesaj bir ölü mektup kuyruğuna (dead letter queue) düşmeli ve oradaki sayı bir insanın gördüğü ekranda görünmeli. Log dosyasına düşen hata, düşmemiş hatadır.
Kısmi başarı, sessiz veri kaybının bir numaralı sebebi
Toplu API'lerde çok yaygın bir tuzak. 200 ürün gönderirsiniz, 197'si geçer, 3'ü doğrulamadan döner. Sunucu size HTTP 200 verir, çünkü istek başarıyla işlendi; hatalar cevabın gövdesinde, satır satır listelenmiştir. Durum koduna bakıp geçen entegrasyon o üç ürünü sessizce kaybeder ve kimse aylarca fark etmez.
Kural şu: toplu uçlarda durum kodu yetmez, cevap gövdesindeki başarı ve hata sayılarını okuyun, gönderdiğiniz kayıt sayısıyla karşılaştırın, uyuşmazlığı hata olarak işaretleyin.
Her sapma teslimat hatası değil, bazıları anlam farkı
Bazı tutarsızlıkların hiçbir yeniden deneme politikasıyla ilgisi yoktur. İki sistem aynı isimli alandan farklı şey anlıyordur. "Mevcut stok" fiziksel stok mu, rezerve düşülmüş kullanılabilir stok mu, yolda olan da dahil mi? Fiyat KDV dahil mi, yuvarlama hangi basamakta ve hangi yöne yapılıyor? Tarih alanı hangi saat diliminde? Bu farklar tek seferde patlamaz, sürekli küçük bir sapma olarak sızar.
Çaresi teknik değil, yazılı olmalı: alan alan bir eşleme tablosu ve her alan için tek bir sahip sistem. Bir alanın kayıt kaynağı ERP ise, e-ticaret tarafı o alanı asla yazmaz, sadece okur. Sahiplik belirsiz kalırsa iki sistem birbirinin güncellemesini yankılar ve durmayan bir ping pong başlar. Bu, entegrasyon projelerinde en çok emek yakan ve en az dokümante edilen kısımdır. Arayüz sözleşmelerini baştan tasarlamanın diğer faydalarını API-first yaklaşımı yazımızda anlattık.
Mutabakat işi opsiyonel değil
Yukarıdaki her şeyi doğru yapsanız bile bir olay kaybolur. Sunucu yeniden başlar, abonelik silinir, bir dağıtım penceresinde beş dakika kesinti olur. Shopify bu konuda beklenmedik ölçüde açık sözlü: uygulamanızın webhook'lardan veri almaya bel bağlamaması gerektiğini, kaçırdığınız veriyi API üzerinden periyodik olarak çeken bir mutabakat işi kurmanın yaygın pratik olduğunu yazıyorlar.
Pratik kurulum: gecelik bir iş son 7 günün kayıtlarını iki taraftan çeker, kimlikleri ve birkaç kritik alanı karşılaştırır, farkları listeler ve eksikleri geri doldurur. Tek şart, bu işin çıktısının bir sayıya dönüşmesi. "Dün 3 sipariş sapmış" bir metriktir, izlenir, eşik konur, alarm üretir. Aynı bilginin log dosyasında durması hiçbir işe yaramaz, çünkü işin çalışmadığı günle sıfır fark bulduğu gün birbirinden ayırt edilemez. Sapma sayısını ve mutabakat işinin son çalışma zamanını izleme ekranınıza koyun; log toplayıp kimsenin bakmaması burada da geçerli bir tuzak.
Bu haftaya sığan ilk adım
En yoğun entegrasyonunuzu seçin ve dört soruyu tek cümleyle cevaplamayı deneyin. Bir: bu entegrasyonda idempotency anahtarı nedir ve üzerinde benzersiz indeks var mı? İki: gönderici kaç kez ve kaç saat boyunca yeniden deniyor, siz dedup kaydını ne kadar saklıyorsunuz? Üç: bir mutabakat işi var mı, dün çalıştı mı, kaç fark buldu? Dört: ortak alanların her birinin sahibi hangi sistem?
Hangi soruya tek cümleyle cevap veremiyorsanız, bir sonraki olayınız oradan çıkacak. Yeni entegrasyonun ucunu tasarlarken kimlik doğrulama ve yetki tarafını da atlamamak için API güvenliği yazımıza, eski bir sistemi kademeli taşırken bu desenlerin nasıl birleştiğine ise eski yazılımı yenileme yazımıza bakabilirsiniz.
Bu konuda yardıma mı ihtiyacınız var?