omeryanbas.com

Ömer Yanbaş

Genel Müdür, Ticofab Yazılım

EntegrasyonMesajlaşma

Hız sınırını kalıcı hata saymanın bedeli

429 ve protokol seviyesindeki throttle yanıtı sonra gel demektir. Bunu kalıcı hata yazan istemci, parası ödenmiş işi çöpe atar ve kapasiteyi gizler.

Bir kuyruk normalden hızlı boşaldı ve kayıtların önemli bir kısmı başarısız işaretiyle geri geldi. Hiçbirinde bir sorun yoktu. Karşı taraf "çok fazla istek" anlamına gelen cevabı vermiş, istemci de bunu bozuk bir isteğle aynı kovaya yazmıştı. Parası çoktan ödenmiş işler gitmişti ve panelde bir kalite sorunu görünüyordu; oysa asıl sorun anlaşmanın izin verdiğinden hızlı göndermemizdi.

Gerçekte ne oluyor

Aynı anlama gelen iki sinyal var, farklı protokollerde.

HTTP tarafında bu, çoğu zaman saniye veya tarih taşıyan bir Retry-After başlığıyla gelen 429 durumudur. SMPP tarafında throttling durum kodlu bir submit yanıtıdır, bir de karşı kuyruğun dolduğunu söyleyen komşusu vardır. İkisi de açıkça geçicidir: istek doğruydu, zamanlama değildi.

Naif bir istemcide iki kova vardır. Başarılı olmayan her şey başarısızdır. Yol şöyle işler:

  1. Kayıt kuyruktan alınır ve gönderilir.
  2. Cevap throttle'dır.
  3. İstemci kayda REJECTED yazar ve bu terminal bir durumdur.
  4. Tekrar deneme mantığı yalnızca tekrar denenebilir durumdaki kayıtlara bakar, bu yüzden onu bir daha hiç görmez.
  5. Kuyruk daha erken boşalır ve kapasite grafiği yükselir.

Bu son madde, hatanın neden uzun süre fark edilmediğini açıklar. İşi çöpe atmak, hızlanmak gibi görünür. Saatlik deneme sayısı artar, kuyruk boşalır ve kaybın göründüğü tek yer, kimsenin bir sebebe bağlayamadığı bir hata sayacıdır.

Müşteri tarafında biriken bir bedel daha var. Gönderim başarısız yazıldığı için müşteri raporunda sebebi açıklanamayan bir kayıp görür, destek ekibi de elinde bir açıklama olmadığından sistemi savunmak zorunda kalır. Oysa kayıtların hiçbirinde bir sorun yoktur, yalnızca yanlış saniyede gönderilmişlerdir.

Kaybolan kayıtlardan daha uzun yaşayan bir bedel daha var. Düz bir hata sayısı sinyalin içindeki bilgiyi yok eder. Anlaşılan hızın üstünde geçen on bir dakika ile gerçekten bozulmuş bir entegrasyonda geçen on bir dakika, aynı grafikte aynı sayıyı üretir. Kodu mu düzelteceğinizi, kapasite mi alacağınızı, yoksa yavaşlamanız mı gerektiğini ayırt edemezsiniz, çünkü durduğunuz yerden üçü de aynı görünür.

Nasıl görülür

Kendi yazdığınıza göre değil, karşı tarafın söylediğine göre sayın. Bu ikisi aynı kolonsa hata zaten oradadır:

SELECT provider_code,
       internal_status,
       COUNT(*) AS n
FROM send_attempts
WHERE created_at >= UTC_TIMESTAMP() - INTERVAL 1 HOUR
GROUP BY 1, 2
ORDER BY n DESC;
provider_code  internal_status      n
200            SUBMITTED        41230
429            REJECTED          4812
0x58           REJECTED          1907
400            REJECTED            58

Zamanlama yüzünden terminal duruma düşmüş yaklaşık yedi bin kayıt, isteğin kendisiyle ilgili gerçek bir sorun yüzünden düşmüş elli sekiz kayıt. Sonra şeklin zaman içinde nasıl göründüğüne bakın, dakika dakika:

SELECT DATE_FORMAT(created_at, '%H:%i') AS minute,
       SUM(CASE WHEN provider_code IN ('429', '0x58') THEN 1 ELSE 0 END) AS throttled,
       COUNT(*) AS attempts
FROM send_attempts
WHERE created_at >= UTC_TIMESTAMP() - INTERVAL 30 MINUTE
GROUP BY 1 ORDER BY 1;

Ani bir sıçrama değil de düz bir plato görüyorsanız bir tavanın üstünde oturuyorsunuz demektir. Bu, hız ayarının imzasıdır ve genelde birinin worker havuzunu büyüttüğü hafta ortaya çıkar.

Çözüm

İki kovayı üçle değiştirin, sonra sınıflandırma durum makinesini yönetsin.

Kalıcı, isteğin bu haliyle asla başarılı olmayacağı anlamına gelir: geçersiz alıcı, kapalı hedef, bozuk içerik, reddedilen gönderici, başarısız kimlik doğrulama. Başarısız işaretleyin, sebebi yazın, durun.

Tekrar denenebilir, isteğin doğru ama zamanlamanın yanlış olduğu durumdur: throttle, dolu karşı kuyruk, 5xx, kopan bağlantı, cevapsız timeout.

Bilinmeyen ise henüz sınıflandırmadığınız her şeydir. Düşük bir deneme sınırıyla tekrar denenebilir sayın ve bir iki sürüm içinde doğru sınıfa geçecek kadar gürültülü log'layın.

const TERMINAL = new Set([400, 401, 403, 404, 422]);

function classify(res) {
  if (res.status === 429) return { kind: 'retry', afterMs: retryAfterMs(res) };
  if (res.status >= 500) return { kind: 'retry', afterMs: null };
  if (TERMINAL.has(res.status)) return { kind: 'terminal', reason: res.body?.code };
  if (res.status === 200) return { kind: 'ok' };
  return { kind: 'unknown' };
}

// protokol seviyesindeki karsiliklari, ayni uc sinif
const THROTTLE_STATUSES = new Set([0x58, 0x14]);

Sonra gecikme. Karşı taraf Retry-After gönderiyorsa ona uyun, çünkü elinizdeki tek yetkili sayı odur. Göndermiyorsa tam jitter kullanın: pencerenin ucunu değil, mevcut backoff penceresinin içinden rastgele bir noktayı seçin.

function nextDelayMs(attempt, afterMs) {
  if (afterMs) return afterMs;
  const window = Math.min(60000, 500 * 2 ** attempt);  // 0,5s 1s 2s ... tavan 60s
  return Math.floor(Math.random() * window);           // tam jitter
}

Rastgelelik olmazsa aynı saniyede throttle yiyen her kayıt aynı saniyede geri döner ve dalgayı yeniden kurar. Tam bu yüzden yirmi dakika boyunca izdiham ile boş bağlantı arasında gidip gelen sistemler gördüm.

Durum makinesi yazmaya değecek kadar küçük:

QUEUED     -> IN_FLIGHT
IN_FLIGHT  -> SUBMITTED   karsi taraf kabul etti
IN_FLIGHT  -> PENDING     tekrar denenebilir; attempts += 1, next_attempt_at yazilir
PENDING    -> QUEUED      next_attempt_at gelince
PENDING    -> DEFERRED    deneme tavani asildi; insan bekler
IN_FLIGHT  -> FAILED      yalnizca kalici, sebebi kayitli

next_attempt_at de diğerleri gibi zamanlanmış bir andır. Onu UTC saklayın ve UTC saatle karşılaştırın, yoksa kayıtları kuyrukta bekleten saat dilimi hatasını yeni bir yerde yeniden keşfedersiniz.

Bir parça daha var ve genelde atlanan parça bu. Throttle yalnızca kaydı değil göndericiyi de değiştirmeli. Her kayıt kibarca geri çekilirken havuz aynı hızla göndermeye devam ediyorsa tavanda kalırsınız ve her şey birikir. Throttle oranını hız ayarına geri besleyin: oran küçük bir eşiğin üstüne çıktığında token hızını yarıya indirin, oran temiz kaldıkça küçük adımlarla yükseltin. Artırırken yavaş, azaltırken hızlı.

Çalıştığını nasıl doğrularsınız

Aynı yoğunluktaki bir saati tekrarlayın ve tek sayıya değil üç sayıya bakın:

                        once       sonra
denemeler              48007       44113
throttle                6719        1204
kalici hatalar          6777          61
teslim edilen          41230       44052

Kalıcı hatalar gerçekten sorunlu kayıt sayısına iner ve deneme sayısı düşmüş olmasına rağmen teslim edilen artar, çünkü artık denemeler çöpe atılan işe harcanmıyor. Throttle sayısının sıfıra inmesi gerekmez. O bir kontrol sinyalidir; küçük ve istikrarlı bir sayı, hızın tavanın hemen altında oturduğunu söyler, zaten olması gereken de budur.

Nelere dikkat etmeli

  • Tekrar deneme, ancak işlem idempotent ise güvenlidir. Her denemede kendi ürettiğiniz sabit kimliği gönderin ve tekrarı karşı taraf tanısın; aksi halde cevapsız bir timeout iki kez ödemeye dönüşür.
  • Kalıcı hataları tekrar denemeyin. Ulaşılamayan bir hedefi altı kez denemek altı kat maliyet ve aynı cevap demektir, üstelik gerçek işi kuyrukta geriye iter.
  • Kayıtları beklemede tutmak, kuyruk derinliğinin artık bir ilerleme çubuğu olmadığı anlamına gelir. Uçuşta, bekleyen ve biten sayılarını ayrı gösterin; yoksa backoff işini her yaptığında destek ekibi kuyruk takıldı diye haber verir.
  • Deneme sınırını hem kayıt başına hem zaman penceresi başına koyun. Sonsuza kadar denenen bir kayıt başka türden bir kesintidir ve fark etmesi daha zordur, çünkü hiçbir şey başarısız işaretlenmez.
  • Alarmı toplam hata sayısına değil throttle oranına ve en eski bekleyen kaydın yaşına kurun. Doğru hızda mısınız ve gerçekten takılan bir şey var mı, bunu söyleyen iki sayı bunlardır.

Daha geniş alışkanlık, karşı sistemin cevaplarını geçti ya da kaldı diye değil bir sözlük olarak okumaktır. Her entegrasyonun en az üç çeşit hayırı vardır: asla olmaz, böyle olmaz, şimdi olmaz. Bunları tek kovaya indirmek, ne yapacağınızı söyleyen yegâne bilgiyi kaybetmektir; her kaydın bir maliyeti olan bir sistemde ise kaydın kendisini de kaybetmektir. Sınıflandırmayı tek bir yere yazın, tekrar denenebilir olanları kuyrukta tutun ve size yavaşla denme oranını bilerek izlediğiniz bir sayıya çevirin.

Sorular ve cevaplar

429 yanıtı bir hata mıdır?
Geçici bir ret cevabıdır, isteğin kendisi hakkında verilmiş bir karar değildir. Aynı istek birkaç saniye sonra hiç değiştirilmeden gönderildiğinde genelde başarılı olur; kalıcı hatanın tanımı ise tam tersidir. Bunu bozuk bir istekle veya tanımsız bir alıcıyla aynı kovaya yazmak hem işi çöpe atar hem de anlaşılan hızın üstünde çalıştığınızı gizler.
Hız sınırına takılan bir isteği kaç kez tekrar denemeliyim?
Normal bir aşırı yüklenmeyi karşılayacak kadar, fazlası değil; pratikte bir dakika civarında tavanlanmış üstel gecikmeyle beş altı deneme yeterlidir. Bu, birkaç dakikalık bir olayı örter. Tavandan sonra kaydı başarısız işaretlemek yerine insan müdahalesi bekleyen bir duruma alın; böylece hiçbir şey kaybolmaz ve hiçbir şey sonsuza kadar denenmez.
Jitter nedir, backoff'un ona neden ihtiyacı var?
Jitter, tekrar denemeden önceki gecikmeye eklenen rastgeleliktir. Olmadığında aynı saniyede throttle yiyen bütün kayıtlar aynı saniyede geri döner, bu da throttle'a sebep olan dalgayı yeniden üretir ve döngü sürer. Tam jitter, yani sıfır ile mevcut backoff penceresi arasında rastgele bir gecikme, denemeleri yayar ve sistem çok daha hızlı yatışır.
Tekrar denemeyi nasıl güvenli hale getiririm?
Her kayda kendi ürettiğiniz sabit bir kimlik verin ve bunu her denemede gönderin; karşı taraf tekrarı tanıyıp işi ikinci kez yapmak yerine ilk sonucu dönebilsin. Bu olmadan, cevapsız kalan bir timeout'tan sonraki tekrar deneme aynı kayda iki kez para ödemek anlamına gelebilir. Kimliği bellekte değil kaydın yanında saklayın ki yeniden başlatmadan sağ çıksın.