← Ana sayfa English →
Blog · Türkçe

Express üzerinde Cluster ve Worker threads

Aynı Express uygulaması, aynı iki route. Sadece alttaki eşzamanlılık mekanizması değişti ve ağır bir CPU görevi çalışırken hafif uç nokta saniyede 2,9’dan 82 isteğe çıktı.

Event loop’u izleyin. Aynı Express uygulamasını çalıştırmanın iki yolu.
Mavi kareler hafif istekler. Düz şeritte tek bir turuncu blok tek işçiyi meşgul ediyor, bu yüzden hafif istekler kuyruğa giriyor. İkinci yarıda iki süreç yan yana çalışıyor: hafif istekler ikisinden de geçiyor, ağır CPU işi ise ortak bir worker thread’e iniyor ve bitince loop’a geri dönüyor.
28×
ağır iş çalışırken hafif uç noktanın verimi, plain vs cluster + worker threads
34×
aynı pencerede hafif uç noktanın p99 gecikmesindeki iyileşme
2
tüm testin kullandığı vCPU çekirdeği. Her yerde aynı iki çekirdek

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:

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.

Worker threads Ağır CPU işi thread’lere aktarılır. Gecikme düşük kalır ama HTTP tek çekirdekte çalışır. cluster + wt HTTP için çekirdekler, ağır handler’lar için thread’ler. Üretim şekli. plain Tüm handler’lar hızlıyken sorun yok. Tek bir ağır uç nokta tüm sunucuyu kilitler. cluster Daha fazla süreç, daha fazla çekirdek, yaklaşık 2× HTTP verimi. Her loop hâlâ ağır iş tarafından kilitlenir. route’larda ağır CPU işi route’larda ağır CPU işi yok tek çekirdek yeterli daha fazla çekirdek gerekli

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.

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.

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)plainclusterWorker threadscluster + wt
/fast verim (RPS)2,95,159,681,8
/fast p99 (ms)162710498548
/fast p50 (ms)6193212922
/slow verim (RPS)2,75,43,52,7
/fast verim (sadece-fast)78,8136,682,3134,7
CPU (karma, tüm çekirdeklerin %’si)1009919699
RSS boşta (MB)6512886151

İ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.

Dürüst özet
plain, loop’a hiçbir zaman ağır bir iş gelmediği sürece doğru seçimdir. Tek bir ağır uç nokta onu bozar. sadece cluster, iş yükü I/O’ya bağlıysa ve boş çekirdeklerin varsa doğrudur. Bloklayan CPU işine karşı işe yaramaz. sadece worker_threads, loop kilitlenmeye başladığında ama HTTP için tek çekirdek yeterliyse doğrudur. cluster + worker_threads üretim şeklidir: HTTP için çekirdekler, ağır handler’lar için küçük bir havuz.

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:

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.