Sunucu Tarafı G/Ç Performansı: Node vs. PHP vs. Java vs. Go

Yayınlanan: 2022-03-11

Uygulamanızın Giriş/Çıkış (G/Ç) modelini anlamak, maruz kaldığı yükle ilgilenen bir uygulama ile gerçek dünyadaki kullanım durumları karşısında çöken bir uygulama arasındaki fark anlamına gelebilir. Belki uygulamanız küçük olsa ve yüksek yüklere hizmet etmese de, çok daha az önemli olabilir. Ancak uygulamanızın trafik yükü arttıkça, yanlış G/Ç modeliyle çalışmak sizi acı dolu bir dünyaya götürebilir.

Ve birden fazla yaklaşımın mümkün olduğu çoğu durumda olduğu gibi, bu sadece hangisinin daha iyi olduğu meselesi değil, aynı zamanda ödünleşimleri anlama meselesidir. G/Ç manzarasında bir yürüyüşe çıkalım ve neleri gözetleyebileceğimize bir bakalım.

Bu makalede, Node, Java, Go ve PHP'yi Apache ile karşılaştıracağız, farklı dillerin G/Ç'lerini nasıl modellediğini, her modelin avantajlarını ve dezavantajlarını tartışacağız ve bazı temel kıyaslamalarla sonuca varacağız. Bir sonraki web uygulamanızın G/Ç performansıyla ilgili endişeleriniz varsa, bu makale tam size göre.

G/Ç Temelleri: Hızlı Bir Tazeleme

I/O ile ilgili faktörleri anlamak için önce işletim sistemi seviyesindeki kavramları gözden geçirmeliyiz. Bu kavramların çoğuyla doğrudan ilgilenmeniz pek olası olmasa da, bunlarla her zaman uygulamanızın çalışma zamanı ortamı aracılığıyla dolaylı olarak ilgilenirsiniz. Ve detaylar önemlidir.

Sistem Çağrıları

İlk olarak, aşağıdaki gibi tanımlanabilecek sistem çağrılarımız var:

  • Programınız ("kullanıcı arazisinde" dedikleri gibi), işletim sistemi çekirdeğinden onun adına bir G/Ç işlemi gerçekleştirmesini istemelidir.
  • "Sistem çağrısı", programınızın çekirdeğe bir şey yapmasını istediği araçtır. Bunun nasıl uygulandığının özellikleri işletim sistemleri arasında farklılık gösterir ancak temel kavram aynıdır. Kontrolü programınızdan çekirdeğe aktaran bazı özel talimatlar olacak (bir işlev çağrısı gibi ama özellikle bu durumla başa çıkmak için bazı özel soslarla). Genel olarak konuşursak, sistem çağrıları engelliyor, yani programınız çekirdeğin kodunuza geri dönmesini bekliyor.
  • Çekirdek, söz konusu fiziksel aygıtta (disk, ağ kartı vb.) temel G/Ç işlemini gerçekleştirir ve sistem çağrısına yanıt verir. Gerçek dünyada, çekirdeğin isteğinizi yerine getirmek için cihazın hazır olmasını beklemek, dahili durumunu güncellemek vb. dahil olmak üzere bir dizi şey yapması gerekebilir, ancak bir uygulama geliştiricisi olarak bunu umursamıyorsunuz. Bu kernelin işi.

Sistem Çağrıları Şeması

Engelleme ve Engellemeyen Çağrılar

Yukarıda sistem çağrılarının bloke olduğunu söyledim ve bu genel anlamda doğru. Bununla birlikte, bazı çağrılar "engellenmeyen" olarak sınıflandırılır; bu, çekirdeğin isteğinizi aldığı, sıraya koyduğu veya bir yerde arabelleğe aldığı ve ardından gerçek G/Ç'nin gerçekleşmesini beklemeden hemen geri döndüğü anlamına gelir. Bu nedenle, yalnızca çok kısa bir süre için "engellenir", yalnızca isteğinizi sıraya koymaya yetecek kadar uzun bir süre.

Bazı örnekler (Linux sistem çağrılarından) açıklığa kavuşturmaya yardımcı olabilir: - read() bir engelleme çağrısıdır - ona hangi dosyanın ve okuduğu veriyi nereye ileteceğini belirten bir tanıtıcı iletirsiniz ve veri orada olduğunda çağrı geri döner. Bunun güzel ve basit olma avantajına sahip olduğunu unutmayın. - epoll_create() , epoll_ctl() ve epoll_wait() , sırasıyla dinlemek için bir tutamaç grubu oluşturmanıza, bu gruptan işleyiciler eklemenize/kaldırmanıza ve ardından herhangi bir etkinlik olana kadar engellemenize izin veren çağrılardır. Bu, tek bir iş parçacığı ile çok sayıda G/Ç işlemini verimli bir şekilde kontrol etmenizi sağlar, ancak kendimi aşıyorum. İşlevselliğe ihtiyacınız varsa bu harika, ancak gördüğünüz gibi kullanımı kesinlikle daha karmaşık.

Burada zamanlamadaki farkın büyüklük sırasını anlamak önemlidir. Bir CPU çekirdeği 3GHz'de çalışıyorsa, CPU'nun yapabileceği optimizasyonlara girmeden saniyede 3 milyar döngü (veya nanosaniyede 3 döngü) gerçekleştirir. Engellemeyen bir sistem çağrısının tamamlanması 10'ar döngü veya "nispeten birkaç nanosaniye" alabilir. Şebeke üzerinden bilgi alınmasını engelleyen bir çağrı çok daha uzun sürebilir - örneğin 200 milisaniye (saniyenin 1/5'i) diyelim. Diyelim ki, örneğin, engellemesiz arama 20 nanosaniye sürdü ve engelleme araması 200.000.000 nanosaniye sürdü. İşleminiz, engelleme çağrısı için 10 milyon kat daha uzun süre bekledi.

Engelleme ve Engellemeyen Sistem Çağrıları

Çekirdek, hem G/Ç'yi engelleme ("bu ağ bağlantısından oku ve bana verileri ver") hem de engelleyici olmayan G/Ç ("bu ağ bağlantılarından herhangi birinde yeni veriler olduğunda bana söyle") yapmak için araçlar sağlar. Ve hangi mekanizmanın kullanıldığı, arama sürecini önemli ölçüde farklı süreler boyunca bloke edecektir.

zamanlama

Takip edilmesi kritik olan üçüncü şey, engellemeye başlayan çok sayıda iş parçacığınız veya işleminiz olduğunda ne olduğudur.

Amaçlarımız için, bir iş parçacığı ve süreç arasında büyük bir fark yoktur. Gerçek hayatta, performansla ilgili en göze çarpan fark, iş parçacıklarının aynı belleği paylaştığı ve işlemlerin her birinin kendi bellek alanına sahip olduğu için, ayrı işlemlerin yapılması çok daha fazla bellek kullanma eğiliminde olmasıdır. Ancak zamanlama hakkında konuştuğumuzda, asıl mesele, her birinin mevcut CPU çekirdeklerinde bir dilim yürütme süresi alması gereken şeylerin (aynı şekilde iş parçacıkları ve işlemler) bir listesidir. Çalışan 300 iş parçacığınız ve bunları çalıştırmak için 8 çekirdeğiniz varsa, zamanı her biri kendi payını alacak şekilde bölmeniz gerekir, her bir çekirdek kısa bir süre çalışır ve ardından bir sonraki iş parçacığına geçer. Bu, CPU'nun bir iş parçacığını/işlemi çalıştırmadan diğerine geçmesini sağlayan bir "bağlam anahtarı" aracılığıyla yapılır.

Bu bağlam anahtarlarının kendileriyle ilişkili bir maliyeti vardır - biraz zaman alırlar. Bazı hızlı durumlarda, 100 nanosaniyeden daha az olabilir, ancak uygulama ayrıntılarına, işlemci hızına/mimarisine, CPU önbelleğine vb. bağlı olarak 1000 nanosaniye veya daha uzun sürmesi nadir değildir.

Ve daha fazla iş parçacığı (veya süreç), daha fazla bağlam değiştirme. Binlerce iş parçacığından ve her biri için yüzlerce nanosaniyeden bahsettiğimizde işler çok yavaşlayabilir.

Ancak, engellenmeyen aramalar özünde çekirdeğe "beni yalnızca bu bağlantılardan herhangi birinde yeni veri veya olay olduğunda ara" der. Bu engellemesiz çağrılar, büyük G/Ç yüklerini verimli bir şekilde ele almak ve bağlam geçişini azaltmak için tasarlanmıştır.

Şimdiye kadar benimle? Çünkü şimdi eğlenceli kısım geliyor: Bazı popüler dillerin bu araçlarla ne yaptığına bakalım ve kullanım kolaylığı ile performans arasındaki ödünleşimler hakkında bazı sonuçlar çıkaralım… ve diğer ilginç bilgiler.

Not olarak, bu makalede gösterilen örnekler önemsiz (ve yalnızca ilgili bitler gösterilerek kısmi) olsa da; veritabanı erişimi, harici önbelleğe alma sistemleri (memcache, vb.) ve G/Ç gerektiren herhangi bir şey, başlık altında gösterilen basit örneklerle aynı etkiye sahip olacak bir tür G/Ç çağrısı gerçekleştirecek. Ayrıca, G/Ç'nin "engelleme" (PHP, Java) olarak tanımlandığı senaryolar için, HTTP isteği ve yanıtı okuma ve yazma işlemlerinin kendileri çağrıları engeller: Yine, eşlik eden performans sorunlarıyla birlikte sistemde daha fazla G/Ç gizlenir dikkate almak.

Bir proje için bir programlama dili seçmeye giden birçok faktör vardır. Sadece performansı göz önünde bulundurduğunuzda birçok faktör bile var. Ancak, programınızın öncelikle G/Ç tarafından kısıtlanacağından endişe ediyorsanız, projeniz için G/Ç performansı bozuluyor veya bozuluyorsa, bilmeniz gerekenler bunlar.

“Basit Tut” Yaklaşımı: PHP

90'larda birçok insan Converse ayakkabı giyiyor ve Perl'de CGI betikleri yazıyordu. Sonra PHP ortaya çıktı ve bazı insanlar onu kullanmaktan hoşlansa da, dinamik web sayfalarını çok daha kolay hale getirdi.

PHP'nin kullandığı model oldukça basittir. Bunun bazı varyasyonları var ama ortalama PHP sunucunuz şöyle görünüyor:

Bir kullanıcının tarayıcısından bir HTTP isteği gelir ve Apache web sunucunuza ulaşır. Apache, her istek için ayrı bir süreç oluşturur ve yapması gerekenlerin sayısını en aza indirmek için bunları yeniden kullanmak için bazı optimizasyonlar yapar (süreç oluşturma, nispeten yavaştır). Apache PHP'yi çağırır ve ona diskte uygun .php dosyasını çalıştırmasını söyler. PHP kodu, G/Ç çağrılarını yürütür ve engeller. PHP'de file_get_contents() çağırırsınız ve kaputun altında read() sistem çağrıları yapar ve sonuçları bekler.

Ve elbette gerçek kod doğrudan sayfanıza gömülüdür ve işlemler aşağıdakileri engelliyor:

 <?php // blocking file I/O $file_data = file_get_contents('/path/to/file.dat'); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>

Bunun sistemle nasıl bütünleştiğine gelince, şöyle:

G/Ç Modeli PHP

Oldukça basit: istek başına bir işlem. G/Ç çağrıları sadece engeller. Avantaj? Bu basit ve işe yarıyor. dezavantaj? Aynı anda 20.000 istemciyle vurun ve sunucunuz alevler içinde kalsın. Bu yaklaşım iyi ölçeklenmiyor çünkü çekirdek tarafından yüksek hacimli G/Ç (epoll, vb.) ile uğraşmak için sağlanan araçlar kullanılmamaktadır. Ve yaralanmaya hakaret eklemek için, her istek için ayrı bir işlem yürütmek, çok fazla sistem kaynağı, özellikle de böyle bir senaryoda genellikle ilk tükenen şey olan bellek kullanma eğilimindedir.

Not: Ruby için kullanılan yaklaşım PHP'ninkine çok benzer ve bizim amaçlarımız için geniş, genel, elle dalgalı bir şekilde aynı kabul edilebilirler.

Çok Yönlü Yaklaşım: Java

Böylece Java, ilk alan adınızı satın aldığınız sıralarda ortaya çıkıyor ve bir cümleden sonra rastgele "dot com" demek çok güzeldi. Ve Java, (özellikle oluşturulduğunda) oldukça harika olan dilde yerleşik çoklu iş parçacığına sahiptir.

Java web sunucularının çoğu, gelen her istek için yeni bir yürütme dizisi başlatarak ve ardından bu dizide sonunda uygulama geliştiricisi olarak sizin yazdığınız işlevi çağırarak çalışır.

Bir Java Servlet'inde G/Ç yapmak, aşağıdaki gibi görünme eğilimindedir:

 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }

Yukarıdaki doGet yöntemimiz tek bir isteğe karşılık geldiği ve kendi iş parçacığında çalıştığı için, her istek için kendi belleğini gerektiren ayrı bir işlem yerine, ayrı bir iş parçacığımız var. Bunun bazı güzel avantajları vardır, birbirlerinin belleğine erişebildikleri için iş parçacıkları arasında durumu, önbelleğe alınmış verileri vb. paylaşabilmek gibi, ancak programla nasıl etkileşime girdiği üzerindeki etkisi hala PHP'de yapılanla neredeyse aynıdır. örnek daha önce. Her istek yeni bir iş parçacığı alır ve istek tamamen işlenene kadar bu iş parçacığının içindeki çeşitli G/Ç işlemleri bloğunu alır. İş parçacıkları, onları oluşturma ve yok etme maliyetini en aza indirmek için birleştirilir, ancak yine de binlerce bağlantı, zamanlayıcı için kötü olan binlerce iş parçacığı anlamına gelir.

Önemli bir dönüm noktası, Java 1.4 sürümünde (ve 1.7'de yine önemli bir yükseltme) engellenmeyen G/Ç çağrıları yapma yeteneği kazanmasıdır. Çoğu uygulama, web ve başka türlü, kullanmaz, ancak en azından kullanılabilir. Bazı Java web sunucuları bundan çeşitli şekillerde yararlanmaya çalışırlar; ancak, konuşlandırılan Java uygulamalarının büyük çoğunluğu yukarıda açıklandığı gibi çalışmaya devam eder.

G/Ç Modeli Java

Java bizi daha da yakınlaştırıyor ve kesinlikle G/Ç için kullanıma hazır bazı iyi işlevlere sahip, ancak yine de, yoğun bir şekilde G/Ç'ye bağlı bir uygulamanız olduğunda ne olduğu sorununu gerçekten çözmüyor. binlerce engelleme iş parçacığı ile zemin.

Birinci Sınıf Vatandaş olarak Engellemeyen G/Ç: Düğüm

Daha iyi I/O söz konusu olduğunda bloktaki popüler çocuk Node.js'dir. Node'a en kısa bir giriş yapmış olan herkese, bunun "engellemesiz" olduğu ve G/Ç'yi verimli bir şekilde gerçekleştirdiği söylendi. Ve bu genel anlamda doğrudur. Ancak şeytan ayrıntılardadır ve bu büyücülüğe nasıl ulaşıldığı performans söz konusu olduğunda önemlidir.

Esasen Node'un uyguladığı paradigma kayması, esasen "isteği işlemek için kodunuzu buraya yazın" yerine "isteği işlemeye başlamak için buraya kod yazın" demeleridir. G/Ç içeren bir şey yapmanız gerektiğinde, istekte bulunursunuz ve bittiğinde Node'un çağıracağı bir geri arama işlevi verirsiniz.

Bir istekte bir G/Ç işlemi yapmak için tipik Düğüm kodu şu şekildedir:

 http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });

Gördüğünüz gibi, burada iki geri arama işlevi var. Birincisi, bir istek başladığında çağrılır ve ikincisi, dosya verileri mevcut olduğunda çağrılır.

Bunun yaptığı temel olarak Node'a bu geri aramalar arasındaki G/Ç'yi verimli bir şekilde işleme fırsatı vermektir. Daha da alakalı olacağı bir senaryo, Düğüm'de bir veritabanı çağrısı yaptığınız yerdir, ancak örnekle uğraşmayacağım çünkü bu tamamen aynı ilkedir: Veritabanı çağrısını başlatır ve Düğüme bir geri arama işlevi verirsiniz. I/O işlemlerini engellemeyen aramaları kullanarak ayrı ayrı gerçekleştirir ve ardından istediğiniz veriler mevcut olduğunda geri arama işlevinizi başlatır. Bu G/Ç çağrılarını sıraya koyma ve Node'un işlemesine izin verme ve ardından bir geri arama alma mekanizmasına “Olay Döngüsü” adı verilir. Ve oldukça iyi çalışıyor.

G/Ç Modeli Node.js

Ancak bu modelde bir yakalama var. Kaputun altında, bunun nedeninin, V8 JavaScript motorunun (Chrome'un Node tarafından kullanılan JS motoru) nasıl uygulandığıyla 1 her şeyden çok daha fazla ilgisi var. Yazdığınız JS kodunun tamamı tek bir iş parçacığında çalışır. Bir an için bunu düşünün. Bu, G/Ç'nin verimli engelleme olmayan teknikler kullanılarak gerçekleştirilirken, CPU'ya bağlı işlemler yapan JS'nizin tek bir iş parçacığında çalıştığı ve her bir kod öbeğinin bir sonrakini engellediği anlamına gelir. Bunun ortaya çıkabileceği yaygın bir örnek, bunları istemciye göndermeden önce bir şekilde işlemek için veritabanı kayıtları üzerinde döngü yapmaktır. İşte bunun nasıl çalıştığını gösteren bir örnek:

 var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };

Node, G/Ç'yi verimli bir şekilde ele alırken, yukarıdaki örnekteki for döngüsü, tek ve tek ana iş parçacığınız içinde CPU döngüleri kullanıyor. Bu, 10.000 bağlantınız varsa, ne kadar sürdüğüne bağlı olarak bu döngünün tüm uygulamanızı bir taramaya getirebileceği anlamına gelir. Her istek, ana iş parçacığınızda birer birer bir zaman dilimi paylaşmalıdır.

Tüm bu kavramın dayandığı öncül, G/Ç işlemlerinin en yavaş kısım olduğudur, bu nedenle, diğer işlemleri seri olarak yapmak anlamına gelse bile, bunları verimli bir şekilde ele almak çok önemlidir. Bu, bazı durumlarda doğrudur, ancak hepsinde değil.

Diğer bir nokta ise, ve bu sadece bir fikir olsa da, bir sürü iç içe geçmiş geri arama yazmak oldukça yorucu olabilir ve bazıları bunun kodu takip etmeyi önemli ölçüde zorlaştırdığını iddia eder. Düğüm kodunun derinliklerinde dört, beş veya daha fazla düzeyde iç içe geçmiş geri aramalar görmek nadir değildir.

Tekrar takaslara döndük. Ana performans sorununuz G/Ç ise Düğüm modeli iyi çalışır. Bununla birlikte, aşil topuğu, bir HTTP isteğini işleyen ve CPU yoğun kod koyan ve dikkatli olmazsanız her bağlantıyı bir taramaya getiren bir işleve girebilmenizdir.

Doğal olarak Engellemeyen: Git

Go bölümüne geçmeden önce, bir Go fanboyu olduğumu açıklamam uygun olur. Birçok projede kullandım ve verimlilik avantajlarının açıkça savunucusuyum ve kullandığım zaman bunları işimde görüyorum.

Bununla birlikte, G/Ç ile nasıl başa çıktığına bakalım. Go dilinin en önemli özelliklerinden biri, kendi zamanlayıcısını içermesidir. Tek bir işletim sistemi iş parçacığına karşılık gelen her yürütme iş parçacığı yerine, "goroutinler" kavramıyla çalışır. Ve Go çalışma zamanı, bir OS iş parçacığına bir goroutin atayabilir ve bu goroutinin ne yaptığına bağlı olarak yürütmesini veya askıya almasını ve bir OS iş parçacığıyla ilişkilendirilmesini sağlayabilir. Go'nun HTTP sunucusundan gelen her istek ayrı bir Goroutine'de işlenir.

Zamanlayıcının nasıl çalıştığının şeması şöyle görünür:

G/Ç Modeli Git

Kaputun altında, bu, Go çalışma zamanında, yazma/okuma/bağlanma/vb. isteğinde bulunarak G/Ç çağrısını uygulayan, mevcut goroutini uyku moduna geçiren ve goroutini geri uyandıracak bilgilerle birlikte uygulanan çeşitli noktalar tarafından gerçekleştirilir. daha fazla önlem alınabileceği zaman.

Gerçekte, Go çalışma zamanı, Node'un yaptığından çok farklı olmayan bir şey yapıyor, ancak geri arama mekanizmasının G/Ç çağrısının uygulanmasına dahil edilmesi ve zamanlayıcı ile otomatik olarak etkileşime girmesi dışında. Ayrıca, tüm işleyici kodunuzun aynı iş parçacığında çalıştırılması zorunluluğunun getirdiği kısıtlamadan da etkilenmez, Go, Goroutine'lerinizi, zamanlayıcısındaki mantığa göre uygun gördüğü kadar çok işletim sistemi iş parçacığına otomatik olarak eşler. Sonuç şöyle bir koddur:

 func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }

Yukarıda görebileceğiniz gibi, yaptığımız şeyin temel kod yapısı, daha basit yaklaşımlarınkine benzer, ancak kaputun altında engellenmeyen G/Ç'ye ulaşır.

Çoğu durumda, bu “her iki dünyanın da en iyisi” olur. Engellemeyen G/Ç, tüm önemli şeyler için kullanılır, ancak kodunuz engelliyor gibi görünür ve bu nedenle anlaşılması ve bakımı daha basit olma eğilimindedir. Gerisini Go planlayıcı ile OS planlayıcı arasındaki etkileşim halleder. Bu tam bir sihir değildir ve eğer büyük bir sistem kurarsanız, nasıl çalıştığı hakkında daha fazla ayrıntıyı anlamak için zaman ayırmaya değer; ama aynı zamanda, "kullanıma hazır" olduğunuz ortam oldukça iyi çalışır ve ölçeklenir.

Go'nun hataları olabilir, ancak genel olarak konuşursak, I/O'yu işleme şekli bunlar arasında değildir.

Yalanlar, Lanet Yalanlar ve Ölçütler

Bu çeşitli modellerle ilgili bağlam değiştirme konusunda kesin zamanlamalar vermek zordur. Sizin için daha az yararlı olduğunu da iddia edebilirim. Bunun yerine, size bu sunucu ortamlarının genel HTTP sunucu performansını karşılaştıran bazı temel testler vereceğim. Tüm uçtan uca HTTP istek/yanıt yolunun performansında birçok faktörün rol oynadığını ve burada sunulan sayıların, temel bir karşılaştırma yapmak için bir araya getirdiğim bazı örnekler olduğunu unutmayın.

Bu ortamların her biri için, rastgele baytlarla 64k bir dosyada okumak için uygun kodu yazdım, üzerinde N sayıda SHA-256 karma çalıştırdım (URL'nin sorgu dizesinde N belirtildi, ör., .../test.php?n=100 ) ve elde edilen karmayı onaltılı olarak yazdırın. Bunu seçtim çünkü aynı testleri bazı tutarlı G/Ç ile çalıştırmanın çok basit bir yolu ve CPU kullanımını artırmanın kontrollü bir yolu.

Kullanılan ortamlar hakkında biraz daha ayrıntı için bu kıyaslama notlarına bakın.

İlk olarak, bazı düşük eşzamanlılık örneklerine bakalım. 300 eşzamanlı istek ve istek başına yalnızca bir karma (N=1) ile 2000 yineleme çalıştırmak bize şunu verir:

Tüm eşzamanlı isteklerde bir isteği tamamlamak için geçen ortalama milisaniye sayısı, N=1

Süreler, tüm eşzamanlı isteklerde bir isteği tamamlamak için geçen ortalama milisaniye sayısıdır. Alçak daha iyi.

Sadece bu grafikten bir sonuç çıkarmak zor, ama bana öyle geliyor ki, bu bağlantı ve hesaplama hacminde, dillerin kendilerinin genel uygulamasıyla daha çok ilgili zamanları görüyoruz. G/Ç. "Komut dosyası dilleri" olarak kabul edilen dillerin (gevşek yazma, dinamik yorumlama) en yavaş performans gösterdiğini unutmayın.

Ancak yine de 300 eşzamanlı istekle N'yi 1000'e çıkarırsak ne olur - aynı yük ancak 100 kat daha fazla karma yineleme (önemli ölçüde daha fazla CPU yükü):

Tüm eşzamanlı isteklerde bir isteği tamamlamak için geçen ortalama milisaniye sayısı, N=1000

Süreler, tüm eşzamanlı isteklerde bir isteği tamamlamak için geçen ortalama milisaniye sayısıdır. Alçak daha iyi.

Birdenbire, Düğüm performansı önemli ölçüde düşer, çünkü her istekteki CPU yoğun işlemler birbirini engeller. Ve ilginç bir şekilde, PHP'nin performansı (diğerlerine göre) çok daha iyi hale geliyor ve bu testte Java'yı geçiyor. (PHP'de SHA-256 uygulamasının C ile yazıldığını ve şu anda 1000 karma yineleme yaptığımız için yürütme yolunun bu döngüde çok daha fazla zaman harcadığını belirtmekte fayda var).

Şimdi 5000 eşzamanlı bağlantı deneyelim (N=1 ile) - veya buna olabildiğince yakın. Ne yazık ki, bu ortamların çoğu için başarısızlık oranı önemsiz değildi. Bu grafik için saniye başına toplam istek sayısına bakacağız. Ne kadar yüksek olursa o kadar iyi :

Saniyedeki toplam istek sayısı, N=1, 5000 req/sn

Saniyedeki toplam istek sayısı. Daha yüksek daha iyidir.

Ve resim oldukça farklı görünüyor. Bu bir tahmin, ancak yüksek bağlantı hacminde, yeni süreçlerin ortaya çıkmasıyla ilgili bağlantı başına ek yük ve PHP+Apache'de bununla ilişkili ek bellek baskın bir faktör haline geliyor ve PHP'nin performansını etkiliyor gibi görünüyor. Açıkça, burada kazanan Go, ardından Java, Node ve son olarak PHP.

Genel veriminizle ilgili faktörler çoktur ve ayrıca uygulamadan uygulamaya büyük ölçüde değişiklik gösterirken, kaputun altında neler olup bittiğinin cesaretini ve ilgili ödünleri ne kadar çok anlarsanız, o kadar iyi olursunuz.

Özetle

Yukarıdakilerin tümü ile, diller geliştikçe, çok sayıda G/Ç yapan büyük ölçekli uygulamalarla başa çıkma çözümlerinin de geliştiği oldukça açıktır.

Adil olmak gerekirse, hem PHP hem de Java, bu makaledeki açıklamalara rağmen, web uygulamalarında kullanılabilen engellemeyen G/Ç uygulamalarına sahiptir. Ancak bunlar, yukarıda açıklanan yaklaşımlar kadar yaygın değildir ve bu tür yaklaşımları kullanan sunucuların bakımına ilişkin operasyonel ek yükün dikkate alınması gerekir. Kodunuzun bu tür ortamlarda çalışacak şekilde yapılandırılması gerektiğinden bahsetmiyorum bile; "normal" PHP veya Java web uygulamanız genellikle böyle bir ortamda önemli değişiklikler yapılmadan çalışmayacaktır.

Karşılaştırma olarak, performansı ve kullanım kolaylığını etkileyen birkaç önemli faktörü göz önünde bulundurursak, şunu elde ederiz:

Dilim Konular ve Süreçler Engellemeyen G/Ç Kullanım kolaylığı
PHP süreçler Numara
Java İş Parçacığı Mevcut Geri Arama Gerektirir
Node.js İş Parçacığı Evet Geri Arama Gerektirir
Gitmek Konular (Goroutinler) Evet Geri Aramaya Gerek Yok


İş parçacıkları genellikle işlemlerden çok daha verimli olacaktır, çünkü işlemler aynı bellek alanını paylaşır, ancak işlemler yapmaz. Bunu engellemeyen G/Ç ile ilgili faktörlerle birleştirirsek, en azından yukarıda ele alınan faktörlerle, listede aşağı indikçe G/Ç ile ilgili genel kurulumun geliştiğini görebiliriz. Yani yukarıdaki yarışmada bir kazanan seçmem gerekirse, kesinlikle Go olurdu.

Yine de pratikte, uygulamanızı oluşturacağınız bir ortam seçmek, ekibinizin söz konusu ortama aşinalığı ve bununla elde edebileceğiniz genel üretkenlik ile yakından bağlantılıdır. Bu nedenle, her takımın Node veya Go'da web uygulamaları ve hizmetleri geliştirmeye başlaması mantıklı olmayabilir. Aslında, geliştiriciler bulmak veya kurum içi ekibinizin aşinalığı, genellikle farklı bir dil ve/veya ortam kullanmamanın ana nedeni olarak gösterilir. Bununla birlikte, son on beş yılda zaman çok değişti.

Umarım yukarıdakiler, kaputun altında neler olup bittiğinin daha net bir resmini çizmeye yardımcı olur ve uygulamanız için gerçek dünya ölçeklenebilirliği ile nasıl başa çıkılacağı konusunda size bazı fikirler verir. Mutlu giriş ve çıkışlar!