Migrasi ke tier Large Language Model (LLM) yang lebih murah sering kali menjadi prioritas rekayasa untuk menekan inference cost. Namun, penyedia tier berbiaya rendah (seperti shared multi-tenant cluster atau model kuantisasi terdistribusi) sering mengalami trade-off performa: variansi Time-to-First-Token (TTFT) yang tinggi, antrean inference yang padat, dan response chunk streaming yang korup akibat pemotongan koneksi TCP agresif di upstream.

Panduan ini memaparkan langkah konkret menangani degradasi performa selama migrasi: mulai dari telemetri metrik streaming, canary routing berbasis bobot, hingga automated rollback di layer gateway tanpa perlu melakukan redeployment pod/container.

Akar Masalah: Karakteristik Latensi dan Kegagalan Streaming LLM

Berbeda dari REST API tradisional yang berbasis permintaan-respons utuh, metrik latensi HTTP standar (seperti Time to First Byte / TTFB) tidak mencerminkan pengalaman pengguna LLM. Pada koneksi Server-Sent Events (SSE):

  • TTFB vs TTFT: HTTP status 200 OK dikirim seketika saat koneksi terbuka, tetapi first token baru di-generate setelah proses prompt evaluation/prefill selesai di GPU. TTFT mencatat waktu hingga payload token pertama benar-benar diterima gateway.
  • Queueing Delay & Kuantisasi: Provider murah mengandalkan batching agresif (vLLM/TGI continuous batching) dengan antrean tinggi. Saat volume token input besar, P99 TTFT melonjak signifikan dari 400ms ke lebih dari 5000ms.
  • Malformed Chunk: Kegagalan alokasi VRAM pada dynamic batching sering memicu upstream server menutup koneksi di tengah jalan, menghasilkan chunk JSON yang terpotong tanpa penutup ([DONE]) atau format SSE rusak (data: {"content": ... tidak valid).

1. Instrumentasi Metrik Telemetri pada Gateway

Gateway harus mengukur latensi streaming secara presisi sebelum meneruskan stream ke client. Berikut implementasi middleware reverse proxy (contoh Go) untuk mengekstrak metrik TTFT, parsing error, dan stream drop rate ke Prometheus:

package main

import (
	"bufio"
	"bytes"
	"net/http"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
)

var (
	ttftHistogram = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "llm_time_to_first_token_seconds",
			Help:    "Durasi dari request dikirim hingga token pertama diterima gateway.",
			Buckets: []float64{0.2, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0},
		},
		[]string{"model_tier"},
	)
	chunkParsingErrors = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "llm_streaming_chunk_errors_total",
			Help: "Jumlah chunk SSE yang tidak valid atau terpotong.",
		},
		[]string{"model_tier"},
	)
)

func ProxyHandler(targetURL, tier string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		reqStart := time.Now()
		firstTokenReceived := false

		outReq, _ := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
		outReq.Header = r.Header.Clone()

		resp, err := http.DefaultClient.Do(outReq)
		if err != nil {
			http.Error(w, "Upstream failure", http.StatusBadGateway)
			return
		}
		defer resp.Body.Close()

		w.Header().Set("Content-Type", "text/event-stream")
		flusher, _ := w.(http.Flusher)
		reader := bufio.NewReader(resp.Body)

		for {
			line, err := reader.ReadBytes('\n')
			if err != nil {
				break
			}

			// Deteksi token pertama pada event SSE
			if !firstTokenReceived && bytes.HasPrefix(line, []byte("data:")) && !bytes.Contains(line, []byte("[DONE]")) {
				ttft := time.Since(reqStart).Seconds()
				ttftHistogram.WithLabelValues(tier).Observe(ttft)
				firstTokenReceived = true
			}

			// Validasi keutuhan payload JSON minimal
			if bytes.HasPrefix(line, []byte("data: ")) {
				payload := bytes.TrimSpace(bytes.TrimPrefix(line, []byte("data: ")))
				if !bytes.Equal(payload, []byte("[DONE]")) {
					if !bytes.HasPrefix(payload, []byte("{")) || !bytes.HasSuffix(payload, []byte("}")) {
						chunkParsingErrors.WithLabelValues(tier).Inc()
					}
				}
			}

			w.Write(line)
			flusher.Flush()
		}
	}
}

2. Strategi Canary Deployment via Weighted Traffic Routing

Pengalihan trafik tidak boleh langsung 100%. Gunakan pembagian trafik bertahap: 5% → 25% → 50% → 100%. Gateway harus membagi trafik berdasarkan konfigurasi dinamis yang dapat diubah secara runtime tanpa me-restart proses.

Contoh konfigurasi weighted cluster pada reverse proxy berbasis Envoy (konfigurasi perutean statis awal):

route_config:
  name: llm_canary_route
  virtual_hosts:
    - name: llm_service
      domains: ["*"]
      routes:
        - match:
            prefix: "/v1/chat/completions"
          route:
            weighted_clusters:
              clusters:
                - name: llm_tier_premium
                  weight: 95
                - name: llm_tier_cheap
                  weight: 5
            timeout: 60s

Bobot ini memungkinkan sistem observabilitas mengumpulkan data P99 TTFT dari 5% volume trafik riil tanpa mengorbankan Service Level Objective (SLO) mayoritas pengguna aplikasi.

3. Prosedur Automated Rollback Tanpa Redeploy Pod

Redeployment container membutuhkan waktu 1 hingga 5 menit untuk rollout bertahap. Ketika model tier murah mengalami lonjakan TTFT hingga 8 detik atau chunk error rate melewati ambang batas 1%, pemulihan harus terjadi dalam orde detik.

Arsitektur Dynamic Fallback

Pisahkan state perutean dari binary gateway. Simpan metadata bobot atau status kill-switch di memory store terdistribusi (seperti Redis) atau gunakan dynamic dynamic discovery service (Envoy RDS/Cluster Discovery Service). Saat alert latency menyala, kontroler otomatis menurunkan bobot tier murah ke 0.

Eksekutor Webhook Rollback

Ketika Prometheus Alertmanager mendeteksi breach pada rule:

histogram_quantile(0.99, sum(rate(llm_time_to_first_token_seconds_bucket{model_tier="cheap"}[2m])) by (le)) > 2.0

Alertmanager memicu webhook ke API internal yang langsung memperbarui routing flag:

curl -X POST http://gateway-admin.internal/v1/routing/override \
  -H "Content-Type: application/json" \
  -d '{"target_tier": "cheap", "weight": 0, "fallback_tier": "premium"}'

Gateway membaca flag ini pada setiap siklus inisialisasi request (atau via subscriber pattern) dan langsung mengalihkan 100% traffic kembali ke tier premium tanpa restart pod.

4. Guardrail: Prefill Timeout & Real-time Upstream Failover

Masalah paling sulit pada streaming LLM adalah menangani kegagalan setelah header dikirim ke client. Jika koneksi terputus di tengah token ke-50, gateway tidak bisa lagi mengubah HTTP status code.

Solusinya adalah mengimplementasikan First-Token Buffering pada gateway:

  1. Klien mengirim request. Gateway menahan pengiriman HTTP 200 OK ke client.
  2. Gateway mengirim request ke tier murah dengan batas timeout TTFT ketat (misal: 2500ms).
  3. Jika upstream murah menghasilkan chunk token pertama sebelum timeout, gateway segera mem-forward status 200 dan memulai streaming.
  4. Jika upstream murah timeout (prefill macet) atau mengembalikan error non-200, gateway secara transparan memutus koneksi upstream tersebut dan mengulang request (retry) ke tier premium sebelum client menerima respons pembuka.
Batasan: Teknik first-token buffering hanya melindungi TTFT. Jika tier upstream mati di tengah proses generasi token (mid-stream truncation), client harus memiliki logika penanganan retry-after-incomplete di sisi application layer.

5. Format Postmortem & Langkah Preventif

Setelah insiden terselesaikan, dokumentasikan postmortem dengan ringkas:

  • Root Cause: Provider tier murah mengalami degradasi P99 TTFT dari 800ms menjadi 6.200ms akibat antrean prefill GPU yang melebihi kapasitas concurrency. Gateway memotong koneksi karena timeout internal 5s, menyebabkan peningkatan malformed JSON chunk.
  • Downtime/Impact: 5% trafik pengguna canary mengalami error/latensi tinggi selama 45 detik sebelum webhook automated rollback mengeksekusi override ke tier premium.
  • Preventive Action: Terapkan concurrency rate-limiting berbasis token-per-minute (TPM) di edge proxy dan aktifkan first-token buffering failover secara permanen.