Bloga dön
Mühendislik·24 Mayıs 2026·9 dk okuma

Kunavo yük devretmeyi nasıl yapar: upstream hatası ile istemciniz arasındaki dört katman

Her sayının arkasındaki sabitle birlikte tüm yük devretme yolu: istek gönderilmeden önce arızalı kanalı atlayan devre kesici, tek istek içinde maliyete göre sıralanmış en fazla üç geçiş, hataya dönüşen 200 yanıtını onaylamayı reddeden akış geçidi ve hiç yanıt vermeyen upstream için dört dakikalık son güvence.

Okuduğunuz her ağ geçidi yazısı bir kullanılabilirlik yüzdesi verir. Bu yazı vermiyor; çünkü SLA yayımlamıyoruz ve doğrulayamayacağınız bir sayı yerine kontrol edebileceğiniz bir mekanizmayı açıklamayı tercih ediyoruz. Aşağıda, upstream'in hata döndürmesiyle istemcinizin bu hatayı görmesi arasındaki yolun tamamı yer alır: dört katman, dört sabit ve her sabitin neden o değere sahip olduğu.

Tek bir upstream neden yeterli değildir

Bir model sağlayıcısının yirmi dakika boyunca sorun yaşaması olağandır. Bölgesel kesintiler, hız sınırlayıcının sıkılaştırılması veya yeni kullanıma açılan bir modelde kapasite sıkışıklığı — bunların hiçbiri sıra dışı değildir ve kendi hata oranınız yükselene kadar size görünmez. Uygulamanız tek bir uç noktayla konuşuyorsa kullanılabilirliğiniz o uç noktanın kullanılabilirliğidir; istemci tarafında yeniden denemek dışında yapabileceğiniz bir şey yoktur ve o zamana kadar kullanıcı zaten beklemiştir.

Alternatif, gidebileceğiniz başka bir yerin olmasıdır. Kunavo her model için bir yönlendirme zinciri çözümler — modeli sunabilen, maliyete göre sıralanmış en fazla üç bağımsız kanal — ve aşağıdaki dört katman, herhangi bir yeniden deneme mantığı yazmanıza gerek kalmadan isteğin bu zincir boyunca ilerlemesini sağlar.

Katman 1: devre kesici (istek gönderilmeden önce)

En ucuz yük devretme, gidiş-dönüş maliyetine hiç yol açmayandır. Her ağ geçidi işlemi, kanal başına kayan beş dakikalık bir hata sayısı tutar. Bu pencere içinde üç hata oluştuğunda kanal açık kabul edilir; sonraki altmış saniye boyunca istek gönderilmeden önce zincirden çıkarılır.

provider-health.ts
// Layer 1, simplified from lib/providers/provider-health.ts.
// A channel that has been failing is skipped BEFORE we send anything,
// so the cost of avoiding it is zero — no request, no timeout, no wait.
const WINDOW_MS   = 300_000;  // failures older than 5 minutes are forgotten
const FAIL_THRESHOLD = 3;     // 3 failures inside that window opens the circuit
const COOLDOWN_MS = 60_000;   // and the channel is bypassed for 60 seconds

function isOpen(channel: string, now = Date.now()): boolean {
  const s = state.get(channel);
  return s != null && s.openUntil > now;
}

function recordFailure(channel: string, now = Date.now()) {
  const s = state.get(channel) ?? { failures: [], openUntil: 0 };
  s.failures = s.failures.filter((t) => now - t < WINDOW_MS);
  s.failures.push(now);
  if (s.failures.length >= FAIL_THRESHOLD) s.openUntil = now + COOLDOWN_MS;
}

Gerçek anlamda anlık olan tek katman budur ve nedenini kesin ifade etmek gerekir: hızlı bir ölçüm yapılmıyor. Karar, sizin adınıza hatayı üstlenen ve beş dakika öncesine kadar gerçekleşmiş olabilecek önceki bir istek tarafından verilmiştir. Durum işlem başınadır; dolayısıyla her makine bağımsız olarak öğrenir. Gerçek trafik altında sorunlu bir kanal hepsinde saniyeler içinde devreye girer; yalnızca kısa süreli sorun yaşayan kanal ise soğuma süresi dolduğunda kendiliğinden toparlanır.

Katman 2: istek içi yük devretme (en fazla üç atlama)

Sağlıklı görünen bir kanal yine de hata döndürdüğünde istek zincirdeki bir sonraki kanala geçer. Sınır üç atlamadır, ROUTING_CHAIN_MAX, ve zincir maliyete göre sıralıdır; bu nedenle ilk deneme her zaman modeli sunabilen en ucuz kanalda yapılır.

dispatch.ts
// Layer 2. The routing chain is resolved from the catalog, cost-ordered,
// and capped at ROUTING_CHAIN_MAX hops (3). It is static configuration, not
// a learned weighting: you can read the exact chain for any model in the
// admin UI, and it is the same chain for every customer.
async function dispatch(req: ChatRequest) {
  const chain = routingChain(req.model)          // up to 3 channels, cheapest first
    .filter((c) => !isOpen(c.id));               // layer 1 removes the known-bad

  let lastError: UpstreamError | null = null;
  for (const channel of chain) {
    try {
      return await callUpstream(channel, req);
    } catch (err) {
      if (!isRetryable(err)) throw err;          // a 400 is yours, not ours
      recordFailure(channel.id, Date.now());
      lastError = err;
    }
  }
  throw lastError ?? new Error("all channels exhausted");
}

İstemci hatalarında yük devretme yapılmaz. Hatalı biçimlendirilmiş bir istek için 400 veya mevcut olmayan bir model için 404, her kanaldan alınacak aynı yanıttır; bunu üç kez yeniden denemek yalnızca aynı mesaj için üç kat daha uzun beklemenize neden olur. Zincirde yalnızca yeniden denenebilir upstream hataları ilerler.

Bu katmanın gerçek maliyeti, her atlama için bir başarısız gidiş-dönüş isteğidir. Bir kanal bağlantınızı kabul edip iki saniye sonra 503 döndürürse, ikinci deneme başlamadan önce o iki saniyeyi beklersiniz. Katman 1'in önemli olmasının asıl nedeni budur: güvenilir biçimde başarısız olan bir kanalın her isteğe iki saniyelik bu bedeli yüklemesini önler.

Katman 3: akış geçidi (30 saniye)

Akış, yük devretmeyi gözden kaçırılması kolay bir şekilde zorlaştırır. Bir upstream 200 OK döndürebilir, bir akış açabilir ve ancak bundan sonra bir hata olayı gönderebilir. Bu olay ulaştığında saf bir proxy istemcinize zaten bir başlık ve birkaç byte göndermiştir; artık akışın altından geçmek mümkün değildir — istek, yanıt vermeyecek bir kanala bağlanmıştır.

stream-gate.ts
// Layer 3, from lib/providers/stream-gate.ts. An upstream can answer 200 OK
// and then put the error inside the stream. Committing that to your client
// means you get a truncated answer and we cannot fail over any more, so the
// gateway holds the first frames back until it knows which it is.
export const STREAM_GATE_TIMEOUT_MS = 30_000;

// Outcomes:
//   a decisive content frame  -> commit, stream the rest through untouched
//   a decisive error frame    -> discard, fall through to the next channel
//   nothing decisive in 30s   -> commit anyway (a slow model is not an error)

Bu nedenle ağ geçidi durum koduna göre bağlantıyı kesinleştirmez. Açılış çerçevelerini, belirleyici bir şey görene kadar tutar: gerçek içerik gelirse akış kesinleştirilir ve bundan sonra değiştirilmeden iletilir; hata gelirse size hiçbir şey ulaşmamıştır ve istek zincirde ilerlemeye devam eder. Bu karar için üst sınır otuz saniyedir; bu süre, ilk tokenı üretmeden önce gerçekten uzun süre düşünebilen akıl yürütme modelleri içindir. Bu sürenin sonunda bağlantıyı kesinleştiririz, çünkü yavaş bir model bozuk bir model değildir.

Katman 4: üstbilgi zaman aşımı (240 saniye)

Son katman, kimsenin sevmediği katmandır. Bir üst akış bağlantıyı kabul eder ve ardından hiçbir yanıt üstbilgisi döndürmezse istek takılır; isteği sona erdiren tek şey zaman aşımıdır — UPSTREAM_HEADERS_TIMEOUT_MS, dört dakika.

Dört dakika beklemek uzun bir süredir ve bu kasıtlıdır. Video ve görsel oluşturma işlemleri meşru olarak dakikalar sürebilir; sohbette duyarlı hissettirecek kadar kısa bir zaman aşımı, başarılı olması beklenen video işlerini iptal ederdi. Takılan bir üst akış da yüksek sesle hata veren bir üst akışa kıyasla nadirdir; yüksek sesle hata veren durumlar zaten 1. ila 3. katmanlar tarafından ele alınır. Yaygın durumda yanlış davranmaktansa nadir durumda yavaş olmayı tercih ederiz.

Kendi uygulamanız için bundan daha sıkı bir sınır istiyorsanız istemci tarafında bir zaman aşımı ayarlayın. Bizimki bir güvenlik ağıdır, gecikme hedefi değildir.

İddia etmediğimiz şeyler

  • Milisaniye cinsinden yük devretme süresi yok. Katman 1 gidiş-dönüş maliyeti oluşturmaz; çünkü karar isteğinizden önce verilir. Katman 2 ila 4, başarısız olan üst akışın başarısız olması için geçen süre kadar sürer. Bu iki değerden hiçbirini pazarlama sayfasına dürüstçe yazabileceğimiz bir sayı olarak sunamayız.
  • Kullanılabilirlik yüzdesi yok. Bir SLA ve kredi programı bulunmadığından, böyle bir sayı yalnızca süs olurdu. Durum sayfası, iyi olmadığı dönemler de dahil olmak üzere fiilen gözlemlediğimiz başarı oranını gösterir.
  • Modelin sessizce değiştirilmesi yok. Yük devretme, bir isteği aynı modeli sunan kanallar arasında taşır. İstediğinizden daha ucuz bir modelle asla yanıt vermeyiz — bu, herhangi bir ağ geçidine doğrudan sormaya değer bir uygulamadır; çünkü yanıttan anlaşılmaz.

Bunun üzerine uygulama geliştirirken anlamı

Çoğunlukla bir yeniden deneme döngüsünü silebileceğiniz anlamına gelir. Yeniden denenebilir üst akış hataları, siz bir hata görmeden önce bağımsız kanallar arasında zaten yeniden denenir; dolayısıyla bizden gelen 5xx yanıtında istemci tarafında yeniden deneme yapmak, üç kez denenmiş bir şeyi yeniden denemektir. Zaman aşımınızı koruyun, geri çekilmeyi kaldırın.

Kendiniz ele almaya değer tek durum 402 insufficient balance hatasıdır; bu bir yük devretme durumu değildir — ne kadar yeniden denerseniz deneyin cüzdanın bakiyesi dolmaz. Hangi durum kodlarının ikinci bir denemeye değer, hangilerinin kesin olduğunu görmek için hata referansına bakın.