· kaynak dev.to (home feed)
Nginx'in varsayılan proxy_buffering ayarı SSE ilk token gecikmesini 0,7 saniyeden 3,4 saniyeye çıkardı
Bir dev.to yazısı, nginx'in varsayılan proxy_buffering davranışının SSE token akışlarını toplu halde teslim ederek medyan ilk token süresini 0,71 saniyeden 3,4 saniyeye çıkardığını — ve ilk düzeltme girişiminin neden sessizce başarısız olduğunu gösteriyor.

Bir geliştiricinin Claude destekli sohbet uygulaması dizüstü bilgisayarda akıcı çalışıyor, production'da ise ciddi şekilde takılıyordu. dev.to'daki ayrıntılı bir yazıya göre suçlu, nginx'in varsayılan proxy_buffering davranışıydı; bu davranış token token akan bir server-sent events (SSE) akışını sessizce toplu teslimata dönüştürüyordu — ve bariz görünen çözüm, dahil edilen bir dosyada gizli ikinci bir nginx kuralı yüzünden başlangıçta hiçbir işe yaramadı.
Kurulum neye benziyordu
Söz konusu uygulama küçük bir iç sohbet aracıydı: Anthropic API'sini streaming modunda çağıran ve her metin deltasını bir SSE olayı olarak yeniden yayınlayan bir endpoint'e sahip FastAPI servisi. Yerelde uvicorn bunu 8000 portunda sunuyordu ve streaming çalışıyordu; ilk kelime bir saniyenin altında görünüyordu. Production'da nginx TLS'yi sonlandırıyor ve /api/ yolunu uvicorn'a proxy'liyordu — ve aynı uygulama orada, paragrafın tamamını birden boşaltmadan önce üç ila dokuz saniye boyunca yanıp sönen bir imleçte bekliyordu.
Ölçümler
Yazar, kendi sohbet geçmişinden 200 prompt'u tek tek yeniden oynattı ve HTTP yanıtının ilk byte'ını değil, gerçek metin içeren ilk ayrıştırılmış SSE veri olayını zamanladı; çünkü header'lar erken gelebilirken gövde takılı kalabilir. Ayrıca her yanıt gövdesinin kaç ayrı ağ okumasında geldiğini saydılar — bu da en açıklayıcı metrik olduğunu kanıtladı: sağlıklı bir akış düzinelerce okumada, tamponlanmış bir akış ise bir avuç ya da tek okumada gelir.
Sonuçlar çarpıcıydı. Doğrudan uvicorn'a gidildiğinde medyan ilk token süresi 0,71 saniye, p95 ise 1,32 saniyeydi ve hiçbir yanıt tek seferde gelmedi. Varsayılan ayarlarla nginx arkasında medyan ilk token süresi 3,4 saniyeye fırladı, p95 9,6 saniyeye ulaştı ve 200 yanıtın 58'i tek seferde geldi. Tek seferde gelen vakalar, SSE gövdesinin tamamı nginx'in tamponlarına sığan kısa yanıtlardı; dolayısıyla kullanıcı modeli bitene kadar hiçbir şey görmüyordu. Daha uzun yanıtlar dört ya da beş parça halinde geliyordu; yazar bunu başlangıçta kötü Wi-Fi sanmıştı.
Tamponlama token akışlarını neden bozuyor
Yazıda açıklandığı gibi nginx, upstream yanıtını bellek tamponlarına okur ve istemciye tampon boyutunda parçalar halinde iletir. Sıradan web trafiği için bu makul bir varsayılandır: nginx yavaş bir uygulama sunucusundan yanıtı hızla çekebilir, worker'ı serbest bırakabilir ve byte'ları yavaş bir istemciye kendi hızında akıtabilir. Ancak bir token akışı, amacı tamamıyla ilk kelimeyi anında göstermek olan yüzlerce küçük yazma işlemidir; bunları birleştirmek tam tersi davranıştır.
Uygulanmayan çözüm
İlk denenen çözüm, streaming endpoint'i için proxy_buffering off, proxy_cache off ve gzip off ayarlarını içeren özel bir prefix location bloğuydu. Reload sonrası ölçüm sonuçları değişmedi.
Sebep: nginx location eşleştirmesi. nginx önce en uzun eşleşen prefix location'ı bulur ve aklında tutar, sonra regex location'ları yapılandırmada görünme sırasına göre kontrol eder — ve herhangi bir regex eşleşirse regex kazanır. Aylar önce CORS header'ı ayarlamak için eklenmiş ve yazarın unuttuğu bir include dosyasında yaşayan, ^/api/ ile eşleşen eski bir regex location, isteği yeni prefix bloğu hizmet verebilmeden yakalıyordu.
Hata ayıklama tekniği basit ve etkiliydi: her location bloğuna ayırt edici bir yanıt header'ı eklemek, endpoint'i curl ile istemek ve hangi header'ın geri döndüğüne bakmak. Beklenen header görünmediğinde isteğin açıkça başka bir yerde işlendiği anlaşıldı. Birleştirilmiş yapılandırmayı dökmek için nginx -T çalıştırmak ve location için grep yapmak, unutulan regex bloğunu ortaya çıkardı.
Güvenilir çözüm
Nihai çözüm iki katmanlı. Birincisi, streaming location'ı, nginx'e bu en uzun eşleşen prefix ise regex'lerin hiç kontrol edilmemesi gerektiğini söyleyen ^~ değiştiricisini kullanıyor. Blok ayrıca proxy_http_version 1.1 ayarlıyor, Connection header'ını temizliyor ve tamponlamayı, cache'lemeyi ve gzip'i devre dışı bırakıyor. İkincisi, uygulamanın kendisi streaming yanıtında X-Accel-Buffering: no gönderiyor; nginx'in bu yanıt için tamponlamayı devre dışı bırakmak üzere dikkate aldığı bir header'dır bu — proxy_ignore_headers ile yok sayılması söylenmedikçe. Yazar bu header'ı tercih ediyor, çünkü streaming kodunun yanında yaşıyor ve gelecekteki nginx yapılandırma değişikliklerinden etkilenmiyor.
Yazının altını çizdiği bir pürüz: regex bloğu streaming yolunu eşleştirmeyi bıraktığında, o yol için yaptığı her şey de sona eriyor; dolayısıyla CORS header'ının yeni bloğa kopyalanması gerekti.
Gerçek çözümden sonra ilk token süresi medyan 0,76 saniye ve p95 1,41 saniye oldu, tek seferde gelen yanıt kalmadı — nginx'e atfedilebilir ek gecikme kabaca 50 milisaniye.
Neden önemli
Streaming yanıtlar artık AI sohbet ürünlerinde varsayılan kullanıcı deneyimi ve kullanıcıların fiilen hissettiği gecikme ilk token süresi. Dağıtımların çoğu, varsayılanları token akışları için değil sayfa sunumu için ayarlanmış bir reverse proxy arkasında duruyor. nginx üzerinden SSE ya da streaming LLM endpoint'i yayınlayan herkes önce tamponlamadan şüphelenmeli, gecikmeyi ilk byte değil ayrıştırılmış olay düzeyinde ölçmeli ve ayarın kodla birlikte yolculuk etmesi için yanıt header'ı çözümünü düşünmeli. Hikaye ayrıca nginx location önceliği hakkında kompakt bir ders: bir include dosyasına eklenen regex, sessizce daha spesifik bir prefix bloğunu geçersiz kılabilir ve bunu discover etmenin en hızlı yolu nginx -T ve blok başına hata ayıklama header'larıdır.
- #nginx
- #sse
- #streaming
- #latency
- #debugging
- #deployment