sgcWebSockets HTTP Sunucusundan Büyük Dosyaları Akış Olarak Sunma

· Bileşenler

Bir müşteri, sonradan tam isabet olduğu ortaya çıkan basit bir gözlemle bize yazdı: HTTP sunucusundan dosya sunmak çok fazla bellek kullanıyordu. TsgcWebSocketHTTPServer DocumentRoot üzerinden büyük bir dosya sunduğunda, her bağlantı için tüm dosya RAM'e okunuyordu. Aynı 1 GB'lık dosyayı indiren yüz istemci, kabaca 100 GB RAM anlamına geliyordu. Bu sürüm bunu düzeltiyor. Dosyalar artık diskten akış olarak sunuluyor ve dosya boyutundan ve kaç istemcinin bağlı olduğundan bağımsız olarak sunucu belleği sabit kalıyor. Bu, sgcWebSockets 2026.6.0 ile geliyor.

Sorun: bağlantı başına tam bir kopya

Her iki statik dosya yolu da, HTTP/1.x için DoResponseHTTP ve HTTP/2 için DoHTTP2DocumentRoot, yanıtı aynı şekilde oluşturuyordu. Bir TMemoryStream oluşturuyor, LoadFromFile çağırıyor ve bu akışı yanıta veriyorlardı. LoadFromFile tüm dosyayı tek seferde belleğe okur, dolayısıyla istek başına maliyet tam dosya boyutuydu ve her eşzamanlı indirme kendi kopyasını tutuyordu.

// Old behaviour, simplified
oStream := TMemoryStream.Create;
oStream.LoadFromFile(vDocument);   // entire file into RAM
AResponseInfo.ContentStream := oStream;

Bu, tepe bellek kullanımını kabaca dosya boyutu × eşzamanlı bağlantı yapar. Bir video, bir kurulum dosyası, bir disk imajı veya bir veritabanı yedeğini çeken bir avuç istemci, bir sunucuyu takas (swap) durumuna veya bir bellek yetersizliği (out-of-memory) çökmesine itmek için yeterlidir.

Yavaş okuma çarpanı

Aynı müşteri ikinci bir senaryo tarif etti ve bu gerçek bir saldırı: her biri yanıtı çok yavaş, saniyede bir bayt mertebesinde okuyan çok sayıda bağlantı. Bu, Slowloris'in bir çeşidi olan bir yavaş okuma saldırısıdır. Okumalarını damla damla yapan bir istemci, bağlantıyı çok uzun süre açık tutar ve eski kodla bu bağlantıların her biri aynı zamanda dosyanın bellekteki tam bir kopyasını da sabitliyordu. Mütevazı bir dosyanın birkaç bin yavaş okuyucusu, neredeyse hiçbir şey aktarmadan gigabaytları rehin tutabilirdi.

Nasıl ölçtük

Küçük bir test düzeneği kurduk: bir DocumentRoot ve büyük bir test dosyası olan bir sunucu, artı iki modlu bir istemci. Bir "hızlı" mod N eşzamanlı normal indirme açar ve bir "yavaş" mod, geçerli bir istek gönderen ve ardından gövdeyi saniyede bir bayt hızında okuyan M ham bağlantı açar. Sunucunun çalışma kümesini (working set) 100 MB'lık bir dosyayla, düzeltmeden önce ve sonra örnekledik.

Senaryo (100 MB dosya)Düzeltmeden önceDüzeltmeden sonra
5 eşzamanlı indirme~511 MB~11 MB
10 eşzamanlı indirme~1.012 MB~11 MB
20 eşzamanlı indirme~1.7 GB~12 MB
100 yavaş okuma (1 byte/sec) bağlantısı~1.67 GB sabit tutuldu~19 MB
Dosya bütünlüğüyokSHA-256 bayt-tam, doğru Content-Length

Düzeltmeden önce, bellek neredeyse tam olarak dosya boyutu çarpı bağlantı sayısını takip ediyordu. Düzeltmeden sonra, dosya ne kadar büyük olursa olsun veya kaç istemci bağlanırsa bağlansın onlarca megabayt mertebesinde kalıyor.

Çözüm: diskten akış sunma

Dosya artık bir TMemoryStream yerine salt okunur, paylaşımlı bir TFileStream ile sunuluyor. İşlem hattındaki başka hiçbir şeyin değişmesi gerekmedi, çünkü yazma tarafı zaten akış halinde çalışıyor: HTTP/1.x sunucusu içerik akışını 32 KB'lık parçalar halinde gönderir ve HTTP/2 sunucusu bunu kabaca 64 KB'lık DATA çerçeveleri halinde yayar. Yani sunucu diskten küçük bir parça okur, gönderir ve devam eder, bu da bağlantı başına bellek maliyetini tüm dosyadan birkaç on kilobayta düşürür.

// New behaviour, simplified
oStream := TFileStream.Create(vDocument, fmOpenRead or fmShareDenyWrite);
oStream.Position := 0;
AResponseInfo.ContentStream := oStream;

Content-Length hâlâ doğrudur, çünkü TFileStream.Size'dan gelir ve akış, yanıttan sonra tıpkı önceden olduğu gibi serbest bırakılır. İndirmelerin bir SHA-256 karşılaştırmasıyla kaynak dosyayla bayt-tam aynı olduğunu ve başlığın doğru uzunluğu bildirdiğini doğruladık. Hiçbir API değişikliği ve yapılandırılacak bir şey yok: DocumentRoot ayarlamak her zaman çalıştığı gibi çalışır, sadece artık dosyayı tamponlamıyor.

oServer := TsgcWebSocketHTTPServer.Create(nil);
oServer.Port := 80;
oServer.DocumentRoot := 'C:\inetpub\files';  // large files now stream from disk
oServer.Active := True;

.NET sürümü düzeltmeyi otomatik olarak devralır, çünkü DocumentRoot dosya sunumu, Delphi bileşenlerinin kullandığı aynı derlenmiş çekirdekte çalışır.

Yavaş okuyucuları yavaşlatma

Diskten akış sunmak bellek büyütmesini ortadan kaldırır, dolayısıyla bir yavaş okuyucu artık tüm bir dosyayı RAM'de sabitleyemez. Geriye kalan bağlantının kendisidir ve bunun için bir gönderme zaman aşımı vardır. Options.WriteTimeOut soket yazmalarına bir son tarih koyar, böylece veri kabul etmeyi durduran bir istemci, bir sunucu iş parçacığını sonsuza dek engellemek yerine düşürülür. Bu sürümde bu zaman aşımı yalnızca Windows'ta değil, Linux ve diğer POSIX platformlarında da çalışır. POSIX soket API'si milisaniye tamsayısı yerine bir timeval bekler ve bu artık doğru şekilde ele alınıyor.

oServer := TsgcWebSocketHTTPServer.Create(nil);
oServer.DocumentRoot := 'C:\inetpub\files';
oServer.Options.WriteTimeOut := 15000;   // drop a client that stalls for 15s, Windows and POSIX
oServer.Active := True;

Herkese açık bir sunucu için üç kaldıracı birleştirmenizi öneririz: durmuş yazmaların düşürülmesi için Options.WriteTimeOut ayarlayın, tek bir adresin kaç bağlantı açabileceğini sınırlamak için güvenlik duvarının RateLimit.MaxConnectionsPerIP özelliğini kullanın ve bir yavaş bağlantı seli her biri bir iş parçacığını tüketemesin diye bir iş parçacığı havuzu zamanlayıcısı veya IOCP ve EPOLL işleyicilerini değerlendirin.

Yükseltme

Akış düzeltmesi doğrudan takılabilir niteliktedir. Güncellediğiniz anda, DocumentRoot dosyaları diskten sabit bellekle, herhangi bir kod değişikliği olmadan sunar. Değişiklik Delphi 7'den 13'e kadar, HTTP/1.x ve HTTP/2 yollarında doğrulandı ve .NET derlemesi bunu paylaşılan çekirdek aracılığıyla alır. Büyük statik dosyalar sunuyorsanız, bu, bellekte anlamlı bir azalma ve yavaş okuma istismarına karşı yararlı bir adımdır.

sgcWebSockets indirme sayfasından güncelleyin veya GetIt üzerinden ya da kayıtlı hesabınızla edinin.

Sorular, geri bildirim veya geçiş yardımı mı? Bizimle iletişime geçin, kodu yazan kişilerden bir yanıt alacaksınız.