Node.js'yi Hızlı Tutmak: Yüksek Performanslı Node.js Sunucuları Yapmak İçin Araçlar, Teknikler ve İpuçları
Yayınlanan: 2022-03-10Node.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.
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 |
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:
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:
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:
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:
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):
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:
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:
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:
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 |
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:
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 |
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ğ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.