Nasıl test ettik
Node tek thread’lidir ve bu benchmark, CPU’ya bağlı bir istek event loop ile karşılaştığında ne olduğuyla ilgilidir. Her şeyi dedicated bir Hetzner 2 çekirdekli VPS’te (AMD EPYC, Ubuntu 26.04), Node 26.5.0 ve Express 5.2.1 ile çalıştırdık. İki uç nokta:
/fast:fibonacci(30), yaklaşık 12 ms saf CPU. Normal istek./slow:fibonacci(37), yaklaşık 380 ms saf CPU. Ağır, bloklayan istek.
Her yapılandırma aynı şekilde ölçüldü: uç nokta başına 2 bağlantıyla autocannon, 15 saniyelik ısınma, ardından 30 saniyelik sadece-fast fazı, sonra da iki uç noktanın aynı anda yüklendiği 75 saniyelik karma faz. Yüzdelikler her bir istek süresinden tam olarak hesaplandı ve sunucu kendi CPU ve RSS değerlerini her 200 ms’de bir örnekledi.
Minimal iki uç noktalı bir benchmark’ta eşzamanlılığı nasıl yönettiğin sonucu 28× değiştirdi.
Eşzamanlılık ve Express’e etkisi
Bir thread, iki tür iş ve ikincisinin her şeyi kırdığı yer.
Node, JavaScript’i tek bir thread üzerinde çalıştırır. Bir Express route veritabanına ulaştığında sorgu işletim sistemine gider ve loop diğer istekleri karşılamak için serbest kalır. Tek bir sürecin binlerce isteğe hizmet etmesini sağlayan asenkron model budur. Loop ancak bir şey senkron CPU işi yaptığında meşgul kalır.
Yukarıdaki animasyonun simüle ettiği durum tam olarak bu. 380 ms CPU tüketen bir handler teslim olmaz. Arkasındaki her istek sıraya girer ve birkaç yüz milisaniyede bir gelen tek bir /slow isteği loop’u sürekli meşgul etmeye yeter.
İkinci ve daha sessiz bir sınır daha var: tek bir süreç yalnızca bir çekirdeği kullanabilir. Bu yüzden Node’daki eşzamanlılık iki bağımsız soruya ayrılır ve benchmark’taki her yapılandırma bunlardan birini yanıtlar:
Her yapılandırmanın yeri
İki soru, dört kurulum. Karar için bir çeyreğin üzerine gel.
Aşağıdaki grafiklerdeki her bar aynı Express uygulaması. Tek fark, doğru çeyrekte hangi mekanizmanın durduğu.
Cluster: ne zaman kullanmalısın?
Cluster, tek bir portu paylaşan bağımsız Node süreçleri oluşturur. Her worker’ın kendi belleği, kendi heap’i, kendi event loop’u ve kendi çekirdeği vardır. “HTTP için daha fazla çekirdek kullan” sorununun standart üretim cevabıdır.
- Ne zaman kullan: uygulaman çoğunlukla I/O’ya bağlıysa (veritabanları, harici API’ler) ve birden fazla çekirdeğe ihtiyacın varsa. Her üretim Node sunucusu için varsayılan budur.
- Bilmen gereken: her süreç belleği kopyalar ve bellekteki durum (oturumlar, önbellekler) worker başına düşer. Ortak durum Redis veya veritabanında yaşar.
- Çözmediği şey: bloklayan bir handler. Her worker hâlâ tek bir event loop çalıştırır, bu yüzden cluster aşağıdaki p99 grafiğinde görüldüğü gibi verimi artırır ama gecikmeyi koruyamaz.
Worker threads: ne zaman kullanmalısın?
Worker thread’ler sürecin içinde yaşar. Aynı bellek alanı, ayrı bir V8 isolate ve ana thread ile mesajlar üzerinden konuşurlar, tıpkı tarayıcıdaki Web Worker’lar gibi. Ana thread HTTP sunmaya devam eder. Worker’lar CPU’ya yoğun kodu çalıştırır ve sonucu geri gönderir.
Kritik nokta şu: worker_threads HTTP’nin kendisini hızlandırmaz. Event loop her isteği hâlâ kendisi karşılar ve HTTP hâlâ tek çekirdekte çalışır. Tek süreçli bir worker-thread uygulaması, sadece-fast grafiğinde görüldüğü gibi verimi tıpkı plain gibi ölçeklendirir. Worker thread’lerin yaptığı şey loop’u boş tutmak. Bu da farklı bir yerde görünür: kuyruk gecikmesinde.
p99 eksenini oku. plain’de en yavaş %1’lik hafif istek, ağır işin arkasında kuyruğa girip 1,6 saniye bekledi. cluster biraz yardım ediyor ama her worker’ın loop’u hâlâ teker teker kilitleniyor. worker_threads bunu 85 ms’ye indiriyor. Cluster ile birleşince 48 ms.
- Ne zaman kullan: bir handler gerçek CPU işi yapıyorsa: görsel yeniden boyutlandırma, kripto, video kodlama, devasa payload’ları ayrıştırma. Bunu dışarı aktar, event loop diğer herkese hizmet etmeye devam etsin.
- Bilmen gereken: istek başına yeni worker yerine baştan küçük bir havuz oluşturulur. Thread oluşturmanın bedeli yok sayılacak gibi değil.
- Çözmediği şey: HTTP için daha fazla çekirdek. Bunun için hâlâ cluster gerekiyor.
Cluster çekirdek ekler. worker_threads loop’u korur. Farklı sorunları çözerler ve biri diğerinin yerini tutmaz.
Senaryolar, yan yana
Tek tablo, dört yapılandırma, benchmark’taki her sayı.
Karma, ağır uç noktanın tüm pencere boyunca çalıştığı anlamına geliyor. Sadece-fast ise çalışmadığı.
| metrik (karma faz) | plain | cluster | Worker threads | cluster + wt |
|---|---|---|---|---|
| /fast verim (RPS) | 2,9 | 5,1 | 59,6 | 81,8 |
| /fast p99 (ms) | 1627 | 1049 | 85 | 48 |
| /fast p50 (ms) | 619 | 321 | 29 | 22 |
| /slow verim (RPS) | 2,7 | 5,4 | 3,5 | 2,7 |
| /fast verim (sadece-fast) | 78,8 | 136,6 | 82,3 | 134,7 |
| CPU (karma, tüm çekirdeklerin %’si) | 100 | 99 | 196 | 99 |
| RSS boşta (MB) | 65 | 128 | 86 | 151 |
İki sayı dikkat etmeye değer. Birincisi, /slow verimi her yerde hemen hemen aynı, yaklaşık 2,7 ile 5,4 RPS arasında, çünkü iş yükü CPU’ya bağlı ve iki çekirdek var. worker_threads ağır işi hızlandırmaz. Ağır işin herkesi cezalandırmasını durdurur. İkincisi, cluster’ın maliyetinin nerede yaşadığına dikkat: boştaki RSS ikiye katlanıyor, 65 MB’a karşı 128 MB, çünkü artık iki tam Node süreci çalışıyor. Tek bir süreci paylaşan thread’ler yalnızca yaklaşık 20 MB ekliyor.
Sonuç
Önceki yazıda Express’in yavaş olmadığını gördük. Yavaş olan Node sürümündü. Aynı ders burada farklı bir biçimde tekrarlıyor: Express yavaş değil. Senin eşzamanlılık kurulumun yavaş. Tek süreçli, tek loop’lu bir Express uygulaması, bir handler 380 ms CPU yakmaya karar verene kadar son derece hızlıdır. O anda tüm sunucu takılır ve başka hiçbir yerdeki istek verimi bunu düzeltmez.
Çözüm farklı bir framework değil. Her aracın ne yaptığını bilmek:
- HTTP’yi çekirdeklere yaymak için cluster kullan. Üretim Node’u için varsayılan budur.
- Bir handler’ın senkron olarak yaptığı ve birkaç milisaniyeden uzun süren CPU işleri için küçük bir havuz üzerinden worker_threads kullan.
- İkisini birden kullan. bcrypt route’u olan üretim Express’i tam olarak böyle görünür.
Minimal iki uç noktalı bir benchmark’ta bu kombinasyon hafif uç noktayı saniyede 2,9’dan 81,8 isteğe, yani 28×’e taşıdı ve p99 gecikmesini 1,6 saniyeden 48 ms’ye indirdi. Hiçbir route değişmedi. Hiçbir middleware değişmedi. Sadece alttaki mekanizma.
Express 5.2.1, Node.js 26.5.0 ile ölçüldü. İki çekirdekli AMD EPYC Hetzner VPS (Ubuntu 26.04). autocannon, uç nokta başına 2 bağlantı. 15 sn ısınma. 30 sn sadece-fast + 75 sn karma faz. Yüzdelikler istek başına gecikmelerden hesaplandı. CPU/RSS 200 ms’de bir örnekleme. İş yükü: senkron CPU’nun fibonacci(30) ≈ 12 ms ve fibonacci(37) ≈ 380 ms’i.