Kembali ke blog
Rekayasa·24 Mei 2026·Baca 9 menit

Cara Kunavo melakukan failover: empat lapisan antara kesalahan upstream dan klien Anda

Seluruh jalur failover, dengan konstanta di balik setiap angka: circuit breaker yang melewati kanal yang gagal sebelum permintaan dikirim, hingga tiga lompatan berdasarkan biaya dalam satu permintaan, stream gate yang menolak mengirimkan status 200 yang berubah menjadi kesalahan, dan penyangga empat menit untuk upstream yang tidak pernah menjawab.

Setiap kiriman gateway yang Anda baca mencantumkan persentase ketersediaan. Yang ini tidak, karena kami tidak menerbitkan SLA dan lebih memilih menjelaskan mekanisme yang dapat Anda periksa daripada angka yang tidak dapat Anda verifikasi. Berikut adalah seluruh jalur antara upstream yang mengembalikan error dan klien Anda yang menerimanya: empat lapisan, empat konstanta, serta alasan setiap konstanta memiliki nilai tersebut.

Mengapa satu upstream saja tidak cukup

Penyedia model mengalami dua puluh menit yang buruk adalah hal biasa. Insiden regional, pengetatan rate limiter, atau kekurangan kapasitas pada model yang baru diluncurkan — tidak ada yang eksotis, dan semuanya tidak terlihat oleh Anda sampai tingkat error Anda sendiri berubah. Jika aplikasi Anda berbicara dengan satu endpoint, ketersediaan Anda sama dengan ketersediaan endpoint tersebut dan tidak ada yang dapat Anda lakukan dari sisi klien selain mencoba lagi; saat itu pengguna sudah menunggu.

Alternatifnya adalah memiliki tempat lain untuk berpindah. Kunavo menyelesaikan rantai perutean untuk setiap model — hingga tiga saluran independen yang dapat menyajikannya, diurutkan berdasarkan biaya — dan empat lapisan di bawah ini menjelaskan bagaimana permintaan bergerak melalui rantai tersebut tanpa Anda menulis logika retry.

Lapisan 1: circuit breaker (sebelum permintaan dikirim)

Failover termurah adalah failover yang tidak pernah memerlukan round trip. Setiap proses gateway menyimpan hitungan kegagalan bergulir selama lima menit untuk setiap saluran. Tiga kegagalan dalam jendela tersebut membuat saluran dianggap terbuka: selama enam puluh detik berikutnya, saluran itu dihapus dari rantai sebelum permintaan dikirim sama sekali.

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;
}

Ini satu-satunya lapisan yang benar-benar seketika, dan penting untuk menjelaskan alasannya dengan tepat: tidak ada sesuatu yang diukur secara cepat. Keputusan tersebut dibuat hingga lima menit sebelumnya oleh permintaan terdahulu yang menanggung kegagalan atas nama Anda. Statusnya berlaku per proses, sehingga setiap mesin belajar secara independen — dalam trafik nyata, saluran yang buruk akan terpicu pada semuanya dalam selisih beberapa detik, dan saluran yang hanya bermasalah sebentar akan pulih sendiri saat cooldown berakhir.

Lapisan 2: failover dalam permintaan (hingga tiga lompatan)

Ketika saluran yang tampak sehat tetap mengembalikan error, permintaan diteruskan ke saluran berikutnya dalam rantai. Batasnya adalah tiga lompatan, ROUTING_CHAIN_MAX, dan rantai diurutkan berdasarkan biaya, sehingga percobaan pertama selalu menggunakan saluran termurah yang dapat menyajikan model tersebut.

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");
}

Error klien tidak memicu failover. 400 untuk permintaan yang cacat atau 404 untuk model yang tidak ada akan memberikan jawaban yang sama dari setiap saluran, sehingga mencoba lagi tiga kali hanya akan membuat Anda menunggu tiga kali lebih lama untuk pesan yang sama. Hanya kegagalan upstream yang dapat dicoba ulang yang berjalan menyusuri rantai.

Biaya nyata lapisan ini adalah satu round trip yang gagal untuk setiap lompatan. Jika saluran menerima koneksi Anda lalu mengembalikan 503 setelah dua detik, Anda telah menunggu dua detik itu sebelum percobaan kedua dimulai. Itulah alasan sebenarnya lapisan 1 penting: lapisan ini mencegah saluran yang terus gagal membebankan tol dua detik tersebut pada setiap permintaan.

Lapisan 3: gerbang stream (30 detik)

Streaming membuat failover lebih sulit dengan cara yang mudah terlewatkan. Upstream dapat mengembalikan 200 OK, membuka stream, lalu baru mengeluarkan event error. Saat itu tiba, proxy naif sudah mengirim header dan beberapa byte ke klien Anda, sehingga tidak ada lagi cara untuk meneruskan permintaan ke saluran lain — permintaan sudah terikat pada saluran yang tidak akan menjawabnya.

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)

Karena itu, gateway tidak berkomitmen berdasarkan kode status. Gateway menahan frame pembuka sampai melihat sesuatu yang menentukan: konten nyata, yang berarti stream dikomit dan sejak saat itu diteruskan tanpa perubahan; atau error, yang berarti belum ada apa pun yang sampai kepada Anda dan permintaan berlanjut ke rantai berikutnya. Tiga puluh detik adalah batas atas keputusan tersebut, dan batas ini ada untuk model penalaran yang secara wajar berpikir lama sebelum mengeluarkan token pertama. Setelah itu kami berkomitmen, karena model yang lambat belum tentu rusak.

Lapisan 4: timeout header (240 detik)

Lapisan terakhir adalah lapisan yang tidak disukai siapa pun. Jika upstream menerima koneksi lalu sama sekali tidak mengembalikan header respons, permintaan akan menggantung, dan satu-satunya hal yang mengakhirinya adalah timeout — UPSTREAM_HEADERS_TIMEOUT_MS, empat menit.

Empat menit adalah waktu yang lama untuk menunggu, dan itu disengaja. Pembuatan video dan gambar secara wajar memerlukan waktu beberapa menit; timeout yang cukup singkat untuk terasa responsif pada chat akan membatalkan pekerjaan video yang sebenarnya akan berhasil. Upstream yang menggantung juga jarang dibandingkan upstream yang gagal secara jelas, dan kasus yang jelas tersebut sudah ditangani oleh lapisan 1 hingga 3. Kami lebih memilih lambat dalam kasus yang jarang daripada salah dalam kasus yang umum.

Jika Anda menginginkan batas yang lebih ketat untuk aplikasi Anda sendiri, tetapkan timeout di sisi klien. Timeout kami adalah pengaman, bukan target latensi.

Hal yang tidak kami klaim

  • Tidak ada angka failover dalam milidetik. Lapisan 1 tidak memerlukan round trip karena keputusan tersebut dibuat sebelum permintaan Anda. Lapisan 2 hingga 4 berlangsung tepat selama waktu yang diperlukan upstream yang gagal untuk gagal. Tidak satu pun dari keduanya merupakan angka yang dapat kami tampilkan secara jujur di halaman pemasaran.
  • Tidak ada persentase ketersediaan. Tidak ada SLA dan tidak ada skema kredit, sehingga angka hanya akan menjadi hiasan. Halaman status menampilkan tingkat keberhasilan yang benar-benar kami amati, termasuk periode ketika hasilnya tidak baik.
  • Tidak ada substitusi model secara diam-diam. Failover memindahkan permintaan antar-saluran yang menyajikan model yang sama. Kami tidak pernah menjawab dengan model yang lebih murah daripada yang Anda minta — praktik yang patut Anda tanyakan langsung kepada gateway mana pun karena tidak terlihat dari respons.

Maknanya saat Anda membangun di atasnya

Sebagian besar, artinya Anda dapat menghapus loop retry. Error upstream yang dapat dicoba ulang sudah dicoba ulang melalui saluran independen sebelum Anda melihat kegagalan, sehingga retry sisi klien pada 5xx dari kami berarti mencoba ulang sesuatu yang sudah dicoba tiga kali. Pertahankan timeout Anda, hapus backoff.

Satu hal yang layak Anda tangani sendiri adalah 402 insufficient balance, yang sama sekali bukan kasus failover — berapa pun percobaan ulang tidak akan mengisi ulang dompet. Lihat referensi error untuk mengetahui status mana yang layak dicoba lagi dan mana yang final.