Node.js – teoria

Node.js – teoria

Node.js to platforma zbudowana na javascriptowym silniku v8 (od Google), służąca do tworzenia szybkich, skalowalnych aplikacji serwerowych, wykorzystuja nieblokujący model wejscia/wyjscia i bazująca na eventach. Jej popularność wzrasta z każdym dniem, nic dziwnego… Możliwość napisania całej aplikacji z wykorzystaniem tylko jednego języka to ogromny atut, nie jedyny. No cóż, czas przyjrzeć się temu zagadnieniu nieco bliżej.

Podstawy

Czym różni się zatem Node od pozostałych silników wykorzystujących inne języki? Weźmy pod uwagę aplikację napisaną w PHP,  który sam w sobie nie daje nam możliwości pracy z wieloma wątkami. Jak widać na załączonym obrazku, przychodzące żądania są układane w kolejkę, której elementy są zwracane po uprzednim przetworzeniu.

Aby wymusić coś więcej, teoretycznie możemy emulować wielowątkowość tworząc pochodne procesy (systemy linuxowe, forkowanie), aczkolwiek nie jest to naturalne rozwiązanie w przypadku tego języka. Druga sprawa, coś takiego trudno kontrolować. Weźmy pierwszy lepszy przykład z brzegu, komunikacja, da się ją co prawda rozwiązać choćby za pomocą pamięci współdzielonej, jednak w przypadku błędu w skrypcie trzeba samemu zadbać o jej zwolnienie.

Wróćmy zatem do naszych rozważań i weźmy pod lupę inny język, natywnie umożliwiający multithreading. Nadal musimy poradzić sobie z paroma istotnymi problemami. Zarządzanie wieloma wątkami to zadanie samo w sobie wymagające dużych umiejętności, dodatkowo, w wielu przypadkach dajęce mało wymierne korzyści. Przede wszystkim wiąże się to z zwiększną zasobożernością, to po pierwsze, po drugie samo przełączanie się między wątkami kosztuje nas bardzo dużo czasu.

Analizując powyższy obrazek wątek 1,3 to operacje I/O heavy czyli polegające na pobraniu danych z pamięci/dysku, gdzie ich czas wykonania jest długi głównie z powodu czasu dostępu do danych. Tak naprawdę w tym przypadku realne wykorzystanie zasobów trwa ~1/10 cyklu niezbędnego do wykonania zadania, reszta to oczekiwanie. Jedynie w przypadku wątku nr 2 w pełni wykorzystujemy przydzielone zasoby.

Jak natomiast jest to rozwiązane w NodeJs? NodeJS wykorzystuje również tylko jeden wątek, aczkolwiek dzięki temu, że bazuje na eventach posiada możliwość przyjęcia kolejnego zadania w momencie,  gdy poprzednie oczekuje na proces zewnętrzny.

Jak widzimy na załączonym obrazku, zapytanie nr 2 wykona się w trakcie zapytania pierwszego. W tym czasie zostanie również zarejestrowane zapytanie nr 3.
Co jednak gdy nie chcemy aby zapytanie nr 2 blokowało nam wątek ? Możemy uruchomić proces pochodny lub użyć mechanizmu WebWorkerów.

Czy NodeJS nie ma zatem minusów? No cóż pisanie kodu który wykonywany jest synchronicznie, jest sporo przyjemniejsze i znacznie łatwiejsze. Natomiast w momencie w którym dochodzi nam asynchroniczność często spotykamy się z sytuacją typu “WTF?  Co tu się właśnie stało ?”.

Warto dodać, że javascript po stronie serwera to nie tylko ciekawostka. Z tego rozwiązania korzystają już naprawdę solidne firmy, np:
- Microsoft, który NodeJS wykorzystuje w Yahoo, a także daje nam możliwość używania go w swojej chmurze (Windows Azure).
- LinkedIn na Node’dzie oparł cały swój backend aplikacji mobilnej, podobnie postąpił WalMart.

Co warto wiedzieć

Wzorce:
Callback Pattern: Wzorzec wynikający z samej budowy NodeJS, stosowany bardzo często także w zewnętrznych bibliotekach. Na czym polega ? Posłużmy się przykładem:

var result;
function onQuery(rows){
   console.log(rows);
}
result = database.query("SELECT * FROM tabela", onQuery);

Mimo wywołania metody query na obiekcie database zmienna result wcale nie będzie zawierała listy wyników. Osoby, które przyswoiły sobie wstęp zapewne wiedzą lub przynajmniej domyślają się dlaczego tak się dzieje. Postarajmy się pomóc reszcie. W momencie odpytywania skryptu do bazy powstaje luka, w której skrypt czeka na odpowiedź i w której Node stara się wykonać inną partie kodu. By to jednak zrobić najpierw musi sobie poradzić z obecnym zadaniem. Nie może jednak zwrócić czegokolwiek ponieważ wciąż nie jest w posiadaniu interesujących nas rekordów. Jednak dzięki budowie opierającej się eventach jest w stanie zarejestrować funkcję, którą wykona gdy wszystkie operacje z bazą danych zostaną zakończone.

Przekazanie funkcji jako argument, jej rejestracja i wykonanie w momencie wywołania eventu to w najprostszych słowach właśnie callback pattern.

Antywzorce:
Spaghetti code: 
Czym jest spaghetti code ? Najprościej, termin określający skomplikowany, trudny do zrozumienia kod źródłowy programu. Spójrzmy na poniższy przykład.

getDataFromDb(options, function(resultData) {
    readSomethingFromDisk({options1:options, options2:resultData}, function(file) {
        getMoreDataFromDb({options1:options,options3:file}, function(moreData) {
            doSmthUnderstandable({options1:options,options4:moreData}, function(finalResult) {
                res.send(finalResult);
            });
        });
    });
});

Widzimy wyraźnie, że im więcej callbacków rejestrujemy tym nasz kod staje się mniej czytelny. Jak się zatem zabezpieczyć ?
Po pierwsze, możemy przypisać poszczególne callback’i do zmiennej.

var finalCallback = function (finalResult) {
        res.send(finalResult);
    },
    thirdCallback = function (moreData) {
        doSmthUnderstandable({options1: options, options4: moreData}, finalCallback);
    },
    secondCallback = function (file) {
        getMoreDataFromDb({options1: options, options3: file}, thirdCallback);
    }, firstCallback = function (resultData) {
        readSomethingFromDisk({options1: options, options2: resultData}, secondCallback);
    }

getDataFromDb(options, firstCallback);

Lub użyć EventEmittera.

var EE = require('events').EventEmmiter,
    ee = new EE(),
    options = {};

getDataFromDb(options, function(resultData) {
    ee.emit('callbackOne', options, resultData);
});

ee.on('callbackOne', function (options, resultData){
    readSomethingFromDisk({options1:options, options2:resultData}, function(file) {
        ee.emit('callbackTwo', options, file);
    });
});

ee.on('callbackTwo', function (options, file){
    getMoreDataFromDb({options1:options,options3:file}, function(moreData) {
        ee.emit('callbackFinal', options, moreData);
    });
});

ee.on('callbackFinal', function (options, moreData){
    doSmthUnderstandable({options1:options,options4:moreData}, function(finalResult) {
        res.send(finalResult);
    });
});

Pozbyliśmy się wielopoziomowych zagnieżdżeń, a to co napisaliśmy jest teraz znacznie przejrzystsze. Oto właśnie chodziło!

Na teraz to już wszystko. Następna część już jutro. Bądźcie gotowi!

Komentarze:
Stefanov
Odpowiedz
09/11/2016 w 10:06 AM

Nie działają obrazki…

nulmail
Odpowiedz
22/11/2016 w 9:45 PM

Obrazki umieszczone na google drive nie są udostępnione publicznie dlatego nie są widoczne.

QnQn
Odpowiedz
10/05/2018 w 9:18 PM

Czekam już piąty rok, na kolejny odcinek który miał być “jutro”… :/ czarna dziura mnie dopadła czy co?

Zostaw odpowiedź

Proszę wypełnij wymagane pola aby dodać komentarz.


Drag circle to the rectangle on the right!

Wyślij