Node.js'yi Hızlı Tutmak: Yüksek Performanslı Node.js Sunucuları Yapmak İçin Araçlar, Teknikler ve İpuçları

Yayınlanan: 2022-03-10
Hızlı özet ↬ Düğüm çok yönlü bir platformdur, ancak baskın uygulamalardan biri ağ bağlantılı süreçler oluşturmaktır. Bu makalede, bunlardan en yaygın olanı olan HTTP web sunucularının profilini çıkarmaya odaklanacağız.

Node.js ile yeterince uzun süredir herhangi bir şey inşa ediyorsanız, beklenmedik hız sorunlarının acısını yaşadığınızdan şüpheniz olmasın. JavaScript, olaylı, eşzamansız bir dildir. Bu, açıkça görüleceği gibi, performans hakkında akıl yürütmeyi zorlaştırabilir. Node.js'nin artan popülaritesi, sunucu tarafı JavaScript'in kısıtlamalarına uygun araç, teknik ve düşünce ihtiyacını ortaya çıkardı.

Performans söz konusu olduğunda, tarayıcıda neyin işe yaradığının mutlaka Node.js'ye uyması gerekmez. Peki, bir Node.js uygulamasının hızlı ve amaca uygun olduğundan nasıl emin olabiliriz? Uygulamalı bir örnek üzerinden gidelim.

Araçlar

Düğüm çok yönlü bir platformdur, ancak baskın uygulamalardan biri ağ bağlantılı süreçler oluşturmaktır. Bunlardan en yaygın olanı olan HTTP web sunucularının profilini çıkarmaya odaklanacağız.

Performansı ölçerken çok sayıda istek içeren bir sunucuyu patlatabilecek bir araca ihtiyacımız olacak. Örneğin, AutoCannon'ı kullanabiliriz:

 npm install -g autocannon

Diğer iyi HTTP kıyaslama araçları arasında Apache Bench (ab) ve wrk2 bulunur, ancak AutoCannon Node'da yazılmıştır, benzer (veya bazen daha yüksek) yük basıncı sağlar ve Windows, Linux ve Mac OS X'e kurulumu çok kolaydır.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Temel bir performans ölçümü oluşturduktan sonra, sürecimizin daha hızlı olabileceğine karar verirsek, süreçle ilgili sorunları teşhis etmenin bir yoluna ihtiyacımız olacak. Çeşitli performans sorunlarını teşhis etmek için harika bir araç, npm ile de kurulabilen Node Clinic'tir:

 npm install -g clinic

Bu aslında bir takım araçlar yükler. Devam ederken Clinic Doctor ve Clinic Flame (0x civarında bir sarmalayıcı) kullanacağız.

Not : Bu uygulamalı örnek için Düğüm 8.11.2 veya üstü gerekir.

kod

Örnek durumumuz, tek bir kaynağa sahip basit bir REST sunucusudur: /seed/v1 bir GET yolu olarak gösterilen büyük bir JSON yükü. Sunucu, bir package.json dosyasından ( restify 7.1.0 'a bağlı olarak), bir index.js dosyasından ve bir util.js dosyasından oluşan bir app klasörüdür.

Sunucumuz için index.js dosyası şöyle görünür:

 'use strict' const restify = require('restify') const { etagger, timestamp, fetchContent } = require('./util')() const server = restify.createServer() server.use(etagger().bind(server)) server.get('/seed/v1', function (req, res, next) { fetchContent(req.url, (err, content) => { if (err) return next(err) res.send({data: content, url: req.url, ts: timestamp()}) next() }) }) server.listen(3000)

Bu sunucu, istemci tarafından önbelleğe alınan dinamik içerik sunmanın yaygın durumunu temsil eder. Bu, içeriğin en son durumu için bir ETag başlığı hesaplayan etagger ara yazılımı ile sağlanır.

util.js dosyası, böyle bir senaryoda yaygın olarak kullanılacak uygulama parçaları, ilgili içeriği bir arka uçtan alma işlevi, etag ara yazılımı ve dakika dakika zaman damgaları sağlayan bir zaman damgası işlevi sağlar:

 'use strict' require('events').defaultMaxListeners = Infinity const crypto = require('crypto') module.exports = () => { const content = crypto.rng(5000).toString('hex') const ONE_MINUTE = 60000 var last = Date.now() function timestamp () { var now = Date.now() if (now — last >= ONE_MINUTE) last = now return last } function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } } function fetchContent (url, cb) { setImmediate(() => { if (url !== '/seed/v1') cb(Object.assign(Error('Not Found'), {statusCode: 404})) else cb(null, content) }) } return { timestamp, etagger, fetchContent } }

Bu kodu hiçbir şekilde en iyi uygulamalara örnek olarak almayın! Bu dosyada birden fazla kod kokusu var, ancak uygulamayı ölçüp profilini çıkarırken bunları bulacağız.

Başlangıç ​​noktamızın tam kaynağını almak için yavaş sunucu burada bulunabilir.

profil oluşturma

Profil oluşturmak için, biri uygulamayı başlatmak için, diğeri ise yük testi için olmak üzere iki terminale ihtiyacımız var.

Bir terminalde, app içinde çalıştırabileceğimiz klasör:

 node index.js

Başka bir terminalde şöyle profilleyebiliriz:

 autocannon -c100 localhost:3000/seed/v1

Bu, 100 eşzamanlı bağlantı açacak ve sunucuyu on saniye boyunca isteklerle bombalayacaktır.

Sonuçlar aşağıdakine benzer bir şey olmalıdır (10s testi @ https://localhost:3000/seed/v1 — 100 bağlantı çalıştırılıyor):

durum Ort. Stdev Maks.
Gecikme (ms) 3086.81 1725.2 5554
İstek/Sn 23.1 19.18 65
Bayt/Sn 237,98 kB 197,7 kB 688.13 kB
10 saniyede 231 istek, 2.4 MB okuma

Sonuçlar makineye bağlı olarak değişecektir. Ancak, bir "Merhaba Dünya" Node.js sunucusunun, bu sonuçları üreten makinede saniyede otuz bin istekte bulunabileceğini düşünürsek, ortalama gecikme süresi 3 saniyeyi aşan saniyede 23 istek yetersizdir.

teşhis

Sorun Alanını Keşfetmek

Clinic Doctor'un –on-port komutu sayesinde uygulamaya tek komutla teşhis koyabiliyoruz. app klasörü içinde çalıştırıyoruz:

 clinic doctor --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

Bu, profil oluşturma tamamlandığında tarayıcımızda otomatik olarak açılacak bir HTML dosyası oluşturacaktır.

Sonuçlar aşağıdaki gibi görünmelidir:

Clinic Doctor bir Event Loop sorunu tespit etti
Klinik Doktor sonuçları

Doktor bize muhtemelen bir Event Loop sorunumuz olduğunu söylüyor.

Kullanıcı arayüzünün üst kısmındaki mesajın yanı sıra Event Loop grafiğinin kırmızı olduğunu ve sürekli artan bir gecikme gösterdiğini de görebiliriz. Bunun ne anlama geldiğini daha derinlemesine incelemeden önce, teşhis edilen sorunun diğer metrikler üzerindeki etkisini anlayalım.

İşlem sıraya alınmış istekleri işlemek için çok çalıştığından CPU'nun sürekli olarak %100'de veya üzerinde olduğunu görebiliriz. Düğümün JavaScript motoru (V8), makine çok çekirdekli olduğundan ve V8 iki iş parçacığı kullandığından, bu durumda aslında iki CPU çekirdeği kullanır. Biri Etkinlik Döngüsü için, diğeri Çöp Toplama için. Bazı durumlarda CPU'nun %120'ye kadar arttığını gördüğümüzde, süreç işlenen isteklerle ilgili nesneleri topluyor.

Bunun Bellek grafiğinde ilişkili olduğunu görüyoruz. Bellek grafiğindeki düz çizgi, Kullanılan Yığın ölçümüdür. CPU'da herhangi bir artış olduğunda, Kullanılan Yığın satırında, belleğin serbest bırakıldığını gösteren bir düşüş görüyoruz.

Etkin Tutamaçlar, Olay Döngüsü gecikmesinden etkilenmez. Etkin tanıtıcı, G/Ç'yi (soket veya dosya tanıtıcısı gibi) veya zamanlayıcıyı ( setInterval gibi) temsil eden bir nesnedir. AutoCannon'a 100 bağlantı ( -c100 ) açması talimatını verdik. Etkin tanıtıcılar tutarlı bir 103 sayısı olarak kalır. Diğer üçü STDOUT, STDERR için tanıtıcılar ve sunucunun kendisi için tanıtıcıdır.

Ekranın alt kısmındaki Öneriler paneline tıklarsak aşağıdaki gibi bir şey görmeliyiz:

Klinik Doktor tavsiyeleri paneli açıldı
Soruna özel önerileri görüntüleme

Kısa Vadeli Azaltma

Ciddi performans sorunlarının kök neden analizi zaman alabilir. Canlı olarak dağıtılan bir proje durumunda, sunuculara veya hizmetlere aşırı yük koruması eklemeye değer. Aşırı yük koruması fikri, (diğer şeylerin yanı sıra) olay döngüsü gecikmesini izlemek ve bir eşik aşılırsa "503 Hizmet Kullanılamıyor" ile yanıt vermektir. Bu, bir yük dengeleyicinin diğer örneklere yük devretmesine izin verir veya en kötü durumda, kullanıcıların yenilemesi gerekeceği anlamına gelir. Aşırı yük koruma modülü bunu Express, Koa ve Restify için minimum ek yük ile sağlayabilir. Hapi çerçevesi, aynı korumayı sağlayan bir yük yapılandırma ayarına sahiptir.

Sorun Alanını Anlamak

Clinic Doctor'daki kısa açıklamanın açıkladığı gibi, Olay Döngüsü gözlemlediğimiz düzeye kadar gecikirse, bir veya daha fazla işlevin Olay Döngüsü'nü “engelliyor” olması çok muhtemeldir.

Node.js'de bu birincil JavaScript özelliğini tanımak özellikle önemlidir: Eşzamansız olaylar, o anda yürütülmekte olan kod tamamlanmadan gerçekleşemez.

Bu nedenle bir setTimeout kesin olamaz.

Örneğin, aşağıdakileri bir tarayıcının DevTools veya Node REPL'sinde çalıştırmayı deneyin:

 console.time('timeout') setTimeout(console.timeEnd, 100, 'timeout') let n = 1e7 while (n--) Math.random()

Ortaya çıkan zaman ölçümü asla 100ms olmayacaktır. Muhtemelen 150ms ila 250ms aralığında olacaktır. setTimeout eşzamansız bir işlem planladı ( console.timeEnd ), ancak şu anda yürütülmekte olan kod henüz tamamlanmadı; iki satır daha var. Şu anda yürütülmekte olan kod, geçerli "kene" olarak bilinir. Onayın tamamlanması için Math.random on milyon kez çağrılması gerekir. Bu 100 ms sürerse, zaman aşımının çözülmesinden önceki toplam süre 200 ms olacaktır (artı setTimeout işlevinin zaman aşımını önceden gerçekten sıraya koyması ne kadar uzun sürerse sürsün, genellikle birkaç milisaniye).

Sunucu tarafı bağlamında, geçerli onaydaki bir işlemin istekleri tamamlaması uzun zaman alıyorsa, işlenemez ve geçerli onay tamamlanana kadar eşzamansız kod yürütülmeyeceğinden veri getirme gerçekleşemez. Bu, hesaplama açısından pahalı kodun sunucuyla olan tüm etkileşimleri yavaşlatacağı anlamına gelir. Bu nedenle, yoğun kaynak gerektiren işleri ayrı süreçlere ayırmanız ve bunları ana sunucudan çağırmanız önerilir; bu, nadiren kullanılan ancak pahalı rotaların diğer sık ​​kullanılan ancak pahalı olmayan rotaların performansını yavaşlattığı durumlardan kaçınacaktır.

Örnek sunucuda Olay Döngüsünü engelleyen bazı kodlar vardır, bu nedenle sonraki adım bu kodu bulmaktır.

Analiz

Düşük performans gösteren kodu hızlı bir şekilde belirlemenin bir yolu, bir alev grafiği oluşturmak ve analiz etmektir. Bir alev grafiği, fonksiyon çağrılarını üst üste oturan bloklar olarak gösterir - zamanla değil, toplu olarak. Buna 'alev grafiği' denmesinin nedeni, tipik olarak turuncudan kırmızıya bir renk şeması kullanmasıdır; burada bir blok ne kadar kırmızıysa, fonksiyon o kadar "daha sıcak" olur, yani olay döngüsünü engelleme olasılığı o kadar fazladır. Bir alev grafiği için veri yakalama, CPU'nun örneklenmesi yoluyla gerçekleştirilir - bu, şu anda yürütülmekte olan işlevin ve yığınının anlık görüntüsünün alındığı anlamına gelir. Isı, profil oluşturma sırasında her numune için belirli bir işlevin yığının en üstünde olduğu (örneğin, şu anda yürütülmekte olan işlev) zamanın yüzdesiyle belirlenir. Bu yığın içinde çağrılacak son işlev değilse, olay döngüsünü engelliyor olması muhtemeldir.

Örnek uygulamanın alev grafiğini oluşturmak için clinic flame kullanalım:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

Sonuç, tarayıcımızda aşağıdakine benzer bir şekilde açılmalıdır:

Clinic'in alev grafiği server.on'un darboğaz olduğunu gösteriyor
Kliniğin alev grafiği görselleştirmesi

Bir bloğun genişliği, genel olarak CPU'da ne kadar zaman harcadığını gösterir. En çok zaman alan üç ana yığın gözlemlenebilir ve bunların tümü en sıcak işlev olarak server.on vurgular. Gerçekte, üç yığın da aynıdır. Profil oluşturma sırasında optimize edilmiş ve optimize edilmemiş işlevler ayrı çağrı çerçeveleri olarak ele alındığından bunlar birbirinden ayrılır. Ön eki * olan işlevler JavaScript motoru tarafından optimize edilir ve ön eki ~ olan işlevler optimize edilmez. Optimize edilmiş durum bizim için önemli değilse, Birleştir düğmesine basarak grafiği daha da basitleştirebiliriz. Bu, aşağıdakine benzer bir görünüme yol açmalıdır:

Birleştirilmiş alev grafiği
Alev grafiğini birleştirme

En başından itibaren, soruna neden olan kodun, uygulama kodunun util.js dosyasında olduğu sonucunu çıkarabiliriz.

Yavaş işlev aynı zamanda bir olay işleyicidir: işleve giden işlevler, temel events modülünün bir parçasıdır ve server.on , bir olay işleme işlevi olarak sağlanan anonim bir işlevin geri dönüş adıdır. Bu kodun, aslında isteği işleyen kodla aynı kene içinde olmadığını da görebiliriz. Öyle olsaydı, çekirdek http , net ve stream modüllerinden gelen işlevler yığında olurdu.

Bu tür temel işlevler, alev grafiğinin diğer çok daha küçük kısımlarını genişleterek bulunabilir. Örneğin, send aramak için kullanıcı arayüzünün sağ üst köşesindeki arama girişini kullanmayı deneyin (hem restify hem de http dahili yöntemlerinin adı). Grafiğin sağında olmalıdır (fonksiyonlar alfabetik olarak sıralanmıştır):

Alev grafiği, HTTP işleme işlevini temsil eden vurgulanmış iki küçük bloğa sahiptir
HTTP işleme işlevleri için alev grafiğini arama

Tüm gerçek HTTP işleme bloklarının ne kadar küçük olduğuna dikkat edin.

writeHead gibi işlevleri gösterecek ve http_outgoing.js dosyasına (Düğüm çekirdeği http kitaplığının bir parçası) write gibi işlevleri gösterecek şekilde genişleyecek olan camgöbeği ile vurgulanan bloklardan birini tıklatabiliriz:

Alev grafiği, HTTP ile ilgili yığınları gösteren farklı bir görünüme yakınlaştırıldı
Alev grafiğini HTTP ile ilgili yığınlara genişletme

Ana görünüme dönmek için tüm yığınlara tıklayabiliriz.

Buradaki kilit nokta, server.on işlevinin gerçek istek işleme koduyla aynı onay işaretine sahip olmamasına rağmen, aksi takdirde performans gösteren kodun yürütülmesini geciktirerek genel sunucu performansını etkilemesidir.

hata ayıklama

Alev grafiğinden, sorunlu işlevin util.js dosyasında server.on iletilen olay işleyicisi olduğunu biliyoruz.

Hadi bir bakalım:

 server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag })

Serileştirme ( JSON.stringify ) gibi kriptografinin pahalı olma eğiliminde olduğu iyi bilinir, ancak neden alev grafiğinde görünmüyorlar? Bu işlemler yakalanan örneklerdedir, ancak cpp filtresinin arkasına gizlenmiştir. cpp düğmesine basarsak aşağıdaki gibi bir şey görmeliyiz:

Alev grafiğinde C++ ile ilgili ek bloklar ortaya çıkarıldı (ana görünüm)
Serileştirme ve şifreleme C++ çerçevelerini ortaya çıkarma

Hem serileştirme hem de kriptografi ile ilgili dahili V8 talimatları artık en sıcak yığınlar olarak ve çoğu zaman alacak şekilde gösteriliyor. JSON.stringify yöntemi doğrudan C++ kodunu çağırır; bu yüzden bir JavaScript işlevi görmüyoruz. Kriptografi durumunda, createHash ve update gibi işlevler verilerde bulunur, ancak bunlar ya satır içidir (yani birleştirilmiş görünümde kaybolurlar) ya da işlenemeyecek kadar küçüktürler.

etagger işlevindeki kod hakkında akıl yürütmeye başladığımızda, kötü bir şekilde tasarlandığı hemen ortaya çıkabilir. server örneğini neden işlev bağlamından alıyoruz? Bir sürü hash oluyor, bunların hepsi gerekli mi? Ayrıca, uygulamada bazı gerçek dünya senaryolarında yükün bir kısmını azaltacak If-None-Match başlık desteği de yoktur, çünkü müşteriler yalnızca tazeliği belirlemek için bir kafa isteğinde bulunurlar.

Şimdilik tüm bu noktaları görmezden gelelim ve server.on gerçekleştirilen asıl işin gerçekten darboğaz olduğu bulgusunu doğrulayalım. Bu, server.on kodunu boş bir fonksiyona ayarlayarak ve yeni bir alev grafiği oluşturarak başarılabilir.

etagger işlevini aşağıdaki şekilde değiştirin:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

server.on iletilen olay dinleyici işlevi artık işlemsizdir.

clinic flame tekrar çalıştıralım:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

Bu, aşağıdakine benzer bir alev grafiği oluşturmalıdır:

Alev grafiği, Node.js olay sistemi yığınlarının hala darboğaz olduğunu gösteriyor
server.on boş bir fonksiyon olduğunda sunucunun alev grafiği

Bu daha iyi görünüyor ve saniye başına istekte bir artış fark etmeliydik. Ama olay yayan kod neden bu kadar sıcak? Bu noktada HTTP işleme kodunun CPU zamanının çoğunu kaplamasını beklerdik, server.on olayında çalışan hiçbir şey yoktur.

Bu tür bir darboğaz, bir işlevin olması gerekenden daha fazla yürütülmesinden kaynaklanır.

util.js en üstündeki aşağıdaki şüpheli kod bir ipucu olabilir:

 require('events').defaultMaxListeners = Infinity

Bu satırı kaldıralım ve --trace-warnings bayrağıyla başlayalım:

 node --trace-warnings index.js

AutoCannon ile başka bir terminalde profil oluşturursak, şöyle:

 autocannon -c100 localhost:3000/seed/v1

Sürecimiz şuna benzer bir çıktı verecektir:

 (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10) (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)

Düğüm bize sunucu nesnesine birçok olayın eklendiğini söylüyor. Bu gariptir, çünkü olayın eklenip eklenmediğini kontrol eden ve daha sonra erkenden dönen bir boolean vardır ve esasen ilk olay eklendikten sonra AttachAfterEvent'i bir no-op yapar.

attachAfterEvent işlevine bir göz atalım:

 var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) }

Koşullu kontrol yanlış! afterEventAttached yerine attachAfterEvent doğru olup olmadığını kontrol eder. Bu, her istekte server örneğine yeni bir olayın eklendiği ve ardından her istekten sonra önceki tüm eklenen olayların tetiklendiği anlamına gelir. Eyvah!

optimize etme

Artık sorunlu alanları keşfettik, bakalım sunucuyu daha hızlı hale getirebilecek miyiz.

Düşük Asılı Meyve

server.on dinleyici kodunu (boş bir işlev yerine) geri koyalım ve koşullu denetimde doğru boolean adını kullanalım. etagger fonksiyonumuz aşağıdaki gibi görünür:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (afterEventAttached === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

Şimdi tekrar profil oluşturarak düzeltmemizi kontrol ediyoruz. Sunucuyu bir terminalde başlatın:

 node index.js

Ardından AutoCannon ile profil oluşturun:

 autocannon -c100 localhost:3000/seed/v1

Sonuçları 200 kat iyileştirme aralığında bir yerde görmeliyiz (10s test @ https://localhost:3000/seed/v1 — 100 bağlantı çalıştırılıyor):

durum Ort. Stdev Maks.
Gecikme (ms) 19.47 4.29 103
İstek/Sn 5011.11 506.2 5487
Bayt/Sn 51.8 MB 5,45 MB 58.72 MB
10 saniyede 50 bin istek, 519.64 MB okuma

Potansiyel sunucu maliyeti düşüşlerini geliştirme maliyetleriyle dengelemek önemlidir. Bir projeyi optimize etmede ne kadar ileri gitmemiz gerektiğini kendi durumsal bağlamlarımızda tanımlamamız gerekir. Aksi takdirde, çabanın %80'ini hız geliştirmelerinin %20'sine harcamak çok kolay olabilir. Projenin kısıtlamaları bunu haklı çıkarıyor mu?

Bazı senaryolarda, düşük asılı meyve ile 200 kat iyileştirme elde etmek ve buna bir gün demek uygun olabilir. Diğerlerinde, uygulamamızı olabildiğince hızlı yapmak isteyebiliriz. Bu gerçekten proje önceliklerine bağlıdır.

Kaynak harcamasını kontrol etmenin bir yolu bir hedef belirlemektir. Örneğin, 10 kat iyileştirme veya saniyede 4000 istek. Bunu iş ihtiyaçlarına dayandırmak en mantıklısı. Örneğin sunucu maliyetleri bütçeyi %100 aşıyorsa 2 kat iyileştirme hedefi koyabiliriz.

Daha da ileri götürmek

Sunucumuzun yeni bir alev grafiğini çıkarırsak, aşağıdakine benzer bir şey görmeliyiz:

Alev grafiği hala server.on'u darboğaz olarak gösteriyor, ancak daha küçük bir darboğaz
Performans hatası düzeltmesi yapıldıktan sonra alev grafiği

Olay dinleyicisi hala darboğaz, profil oluşturma sırasında hala CPU süresinin üçte birini alıyor (genişlik tüm grafiğin yaklaşık üçte biri).

Hangi ek kazanımlar elde edilebilir ve değişiklikler (ilişkili bozulmalarla birlikte) yapmaya değer mi?

Yine de biraz daha kısıtlı olan optimize edilmiş bir uygulama ile aşağıdaki performans özellikleri elde edilebilir (10s testi @ https://localhost:3000/seed/v1 — 10 bağlantı çalıştırarak):

durum Ort. Stdev Maks.
Gecikme (ms) 0.64 0.86 17
İstek/Sn 8330.91 757.63 8991
Bayt/Sn 84.17 MB 7,64 MB 92.27 MB
11 saniyede 92k istek, 937.22 MB okuma

1.6x'lik bir iyileştirme önemli olsa da, bu iyileştirmeyi oluşturmak için gerekli çaba, değişiklik ve kod kesintisinin haklı olup olmadığı duruma bağlı olarak tartışılabilir. Özellikle tek bir hata düzeltmesiyle orijinal uygulamadaki 200 kat iyileştirme ile karşılaştırıldığında.

Bu iyileştirmeyi başarmak için, aynı yinelemeli profil tekniği, alev grafiği oluşturma, analiz etme, hata ayıklama ve optimize etme, kodu burada bulunabilecek olan nihai optimize edilmiş sunucuya ulaşmak için kullanıldı.

8000 req/s'ye ulaşmak için yapılan son değişiklikler şunlardı:

  • Nesneler oluşturup ardından seri hale getirmeyin, doğrudan bir JSON dizisi oluşturun;
  • Etag'ini tanımlamak için içerikle ilgili benzersiz bir şey kullanın, karma oluşturmak yerine;
  • URL'yi hash etmeyin, doğrudan anahtar olarak kullanın.

Bu değişiklikler biraz daha kapsamlıdır, kod tabanı için biraz daha yıkıcıdır ve etagger ara katman yazılımını biraz daha az esnek bırakır çünkü Etag değerini sağlamak için rotaya yük bindirir. Ancak profilleme makinesinde saniyede ekstra 3000 istek gerçekleştirir.

Bu son iyileştirmeler için bir alev grafiğine bakalım:

Alev grafiği, ağ modülüyle ilgili dahili kodun artık darboğaz olduğunu gösteriyor
Tüm performans iyileştirmelerinden sonra sağlıklı alev grafiği

Alev grafiğinin en sıcak kısmı, net modülündeki Düğüm çekirdeğinin bir parçasıdır. Bu idealdir.

Performans Sorunlarını Önleme

Özetlemek gerekirse, performans sorunlarını dağıtılmadan önce önlemenin yollarına ilişkin bazı öneriler aşağıda verilmiştir.

Geliştirme sırasında performans araçlarını gayri resmi kontrol noktaları olarak kullanmak, performans hatalarını üretime geçmeden önce filtreleyebilir. AutoCannon ve Clinic'in (veya eşdeğerlerinin) günlük geliştirme araçlarının bir parçası haline getirilmesi önerilir.

Bir çerçeve satın alırken, performans politikasının ne olduğunu öğrenin. Çerçeve performansa öncelik vermiyorsa, bunun altyapı uygulamaları ve iş hedefleriyle uyumlu olup olmadığını kontrol etmek önemlidir. Örneğin, Restify açık bir şekilde (sürüm 7'nin yayınlanmasından bu yana) kitaplığın performansını artırmaya yatırım yapmıştır. Ancak, düşük maliyet ve yüksek hız mutlak bir öncelikse, Restify katılımcısı tarafından %17 daha hızlı olarak ölçülen Fastify'ı düşünün.

Diğer geniş çapta etkileyen kitaplık seçeneklerine dikkat edin - özellikle günlüğe kaydetmeyi düşünün. Geliştiriciler sorunları düzelttikçe, gelecekte ilgili sorunların ayıklanmasına yardımcı olmak için ek günlük çıktısı eklemeye karar verebilirler. Performans göstermeyen bir kaydedici kullanılırsa, bu, kaynayan kurbağa masalının modası sonrasında zamanla performansı boğabilir. Pino günlükçü, Node.js için kullanılabilen en hızlı yeni satırla ayrılmış JSON günlükçüsüdür.

Son olarak, Event Loop'un paylaşılan bir kaynak olduğunu daima unutmayın. Bir Node.js sunucusu, en sonunda, en etkin yoldaki en yavaş mantıkla kısıtlanır.