Event loop: Node.js v jednom kole

Než se rozhodnete použít Node.js ve své nové aplikaci či službě, je třeba stoprocentně porozumnět principu jeho fungování, jinak se vydáváte na jistou cestu do pekla. I sebelepší technologie vám nepomůže, používáte-li ji špatně.

Pěkně po pořádku, řádek po řádku

Základní princip Node.js stojí na jednoduché myšlence. Rychlost přístupu do paměti serveru ve srovnání s rychlostí přístupu na disk, či do databáze, je enormní. Asi takový, jako když z Prahy do Brna pošlete místo emailu s dopisem holuba. Na odpověď určitě nebudete čekat a raději se budete věnovat své práci.

A stejně funguje i Node.js. Operace s pamětí jsou rychlé, tudíž Node.js další instrukce zpracovává až po jejich dokončení. Práce s databází je pomalá, a tak místo čekání na výsledek jen k dotazu přidáte informaci, co se má po jeho zodpovězení provést (callback), a Node.js pokračuje dál v postupném zpracovávání instrukcí. V momentě, kdy z databáze přijde odpověď, zařadí se tato událost do fronty, kde počká, až Node.js dokončí rozdělanou práci, aby pak zavolal váš callback. Například takto:

const crypto = require('crypto');

console.log('will be called first');

var result = null;

crypto.randomBytes(256, (err, buf) => {  
    if (err) {
        throw err;
    }
    result = buf.toString('hex');
    console.log('will be called third with filled result', result);
});

console.log('will be called second with null result:', result);  

Tento zajímavý přiklad poskytuje sama dokumentace Node.js, konkrétně metoda randomBytes() v modulu crypto. Tato metoda generuje bezpečné náhodné sekvence znaků. Pokud není k dispozici dostatečná entropie (míra neuspořádanosti), operace může nějakou dobu trvat, než se entropie nashromáždí. Generovaní náhodného řetězce pak probíhá asynchronně - na pozadí, proto metodě randomBytes předáváme callback.

Často se neděje, že by bylo možné metodu volat jak synchronně, tak asynchronně. Metoda randomBytes() je jednou z výjimek. Její časová náročnost může být za jistých okolností tak velká, že by mohla blokovat další události. Většinou to však není třeba a tak záleží na vývojáři, zda za cenu jednodušší implementace akceptuje riziko blokování event-loopu.

Hlavně proces neblokovat

I přesto, že Node.js nečeká na odpovědi z databáze či souborového systému, jeden Node.js proces nikdy nebude zpracovávat dvě instrukce současně (paralelně). Díky tomu odpadá jeden obrovský problém: nemusíte řešit konkurenční přístup k proměnným, nebo k atributům objektů, a máte jistotu, že se vám vaše proměnné nezmění pod rukama. To, že Node.js zpracovává instrukce postupně ale také znamená, že každá náročnější práce s daty v procesu aplikace spolehlivě zablokuje ostatní činnosti serveru, například odpovědi na jiné, jednodušší dotazy uživatelů.

Nekažme práci inženýrům, kteří Node.js vytvořili, a neblokujme procesy buď neuváženými konstrukcemi, nebo složitějšími operacemi, které by mohly probíhat v pozadí. Skvělou ukázkou neuváženého kódu je následující příklad, který výborně ilustruje princip event-loopu jeho úplným zablokováním.

var continueInLoop = true;

// call after one second
setTimeout(() => {

    // this will never be called
    console.log('this will never be called');
    continueInLoop = false;
}, 1000);

while (continueInLoop) {  
    // this 'nice' blocking code will kill your application
}

console.log('this will never be called too');  

I když je striktně napsáno, že za jednu vteřinu má být proměnná continueInLoop nastavená na false, čímž by mělo dojít k ukončení cyklu, nic takového se nestane. Node.js se zasekne uvnitř cyklu, a na odbavení události vypršení timeoutu už nedojde. A takový kód krásně umrtví vaši novou aplikaci.

Jak pak ale může být Node.js výkonný?

Uvnitř Node.js běží V8-engine, za jehož vývojem stála potřeba rychlého zpracování JavaScriptu na prohlížeči. V8-ička svůj úděl plní skvěle, avšak běží jen v jednom vlákně. Jaký to má smysl v dnešní době, kdy servery mají k dispozici desítky "procesorů" (ve smyslu počtu vláken, které mohou být současně zpracovávány)? Je třeba si uvědomit následující:

  1. Z vaší Node.js aplikace můžete spouštět a kontrolovat další procesy, dokonce mezi nimi komunikovat. A k dispozici k tomu máte modul Child process či rovnou modul Cluster.
  2. A protože jeden Node.js proces běží vždy jen v jednom vlákně, máte naprostou kontrolu nad tím, jak vytíží procesor.

Pak je jen na vás, jestli vaše aplikace poběží v osmi procesech na jednom stroji, ve dvou procesech na čtyřech strojích, nebo v jednom procesu na osmi strojích. A samozřejmě každý proces může mít svůj specifický účel. Node.js se stará jen o to, aby co nejvíce vytížila své jedinné vlákno a dává dostatek prostoru, abyste využili i ta ostatní.

V době, kdy fakturační jednotkou všech cloudových služeb je prakticky "jeden procesor", tento princip přijde velmi vhod. Ušetří vám spoustu peněz a připraví vás na okamžik, kdy začne růst nápor na vaši aplikaci. Aby byla vaše aplikace škálovatelná, stačí dodržovat jen několik základních pravidel. Ale o tom jindy.

David Menger

Read more posts by this author.