← Back to front page

Posted on in Technology

Viime aikoina Node.js on vilahdellut vähän väliä web-maailman blogeissa. Itse olen lukenut lähinnä tämän esittelyn, mutta se riitti kiinnostumaan aiheesta.

Node.js:n perusajatuksena on viedä selaimista tuttu JavaScriptin asynkroninen luonne palvelinpuolelle. Jokainen web-suunnittelija on joskus kirjoittanut tämän tapaisen koodinpätkän:

var msg = 'Sekunti kului';
setTimeout(function() {
    alert(msg);
}, 10000);

Idea on, että selain ei jää 10 sekunniksi jumiin odottelemaan ajan kulumista. Sen sijaan se ajastetaan setTimeout()-funktiolla jatkamaan ohjelman suoritusta 10 sekunnin kuluttua. Odottelun aikana skripti ei kuluta selaimen resursseja millään lailla. Erityisen tärkeää on, että skriptiä varten ei tarvitse varata omaa dedikoitua prosessia tai säiettä CPU:sta täksi ajaksi.

Closurejen ansiosta msg-muuttuja on edelleen käytettävissä, kun skriptin ajo jatkuu 10 sekunnin kuluttua. Tämä tekee asynkronisesta ohjelmoinnista JavaScriptillä yksinkertaista ja selkeää.

JavaScript palvelinpuolella

Kuvitellaan, että ylläoleva skripti ajetaankin palvelinpäässä ja se tekee pelkän odottelun sijasta tietokantahaun, joka kestää 10 sekuntia. Lopuksi se palauttaa vastauksen HTTP-pyyntöön. Oletan tässä, että tietokantahaku tehdään CouchDB:hen REST-pyyntönä jQueryn avulla:

jQuery.getJSON('http://.../_view/recent', function(data) {
    response.sendHeader(...)
    response.sendBody('<html>....</html>');
});
// Oikeasti tätä ei tehtäisi Node.js:ssä jQueryllä.

Mekanismi on täsmälleen sama kuin selaimen JavaScriptissäkin. Sillä välin kun REST-pyyntöön odotellaan vastausta, skripti ei varaa dedikoitua prosessia tai säiettä. Se aktivoituu vasta sitten, kun tietokanta lopulta palauttaa vastauksen hakupyyntöön.

Tämä tarkoittaa, että palvelin voi käsitellä tuhansittain yhtaikaisia hakupyyntöjä, jotka eivät jää varaamaan prosesseja tai säikeitä CPU:sta. Tyypillinen prefork-Apache-palvelin taas pystyy palvelemaan vain noin 100-200 yhtaikaista pyyntöä, koska jokaiselle luodaan erillinen prosessi koko pyynnön ajaksi.

Ei MySQL:lle?

Asynkronisen web-palvelimen edellytys on, että sivuja muodostettaessa käytetään asynkronisia I/O-operaatioita. Tämä tarkoittaa yleensä lähinnä tietokantapyyntöjen tekemistä non-blocking-yhteyksillä. Pyyntö lähetetään ensin tietokantapalvelimelle, jonka jälkeen ei jäädä odottelemaan vastausta, vaan pannaan ohjelma nukkumaan kunnes vastaus saapuu joskus myöhemmin.

CouchDB:lle tämä malli sopii mainiosti, koska se toimii natiivisti juuri näin. Tietokantahaku lähetetään ensin CouchDB:lle HTTP-pyyntönä, ja jonkin ajan kuluttua siihen saadaan HTTP-vastaus.

Mikä parasta, CouchDB toimii sisäisestikin ilman säikeitä. Yhtaikaisten asiakkaiden määrälle ei ole sinänsä rajoitusta. Pyyntöjen käsittely vain kestää vähän pitempään, kun palvelin kuormittuu.

MySQL:n client-protokolla on taas suunniteltu blokkaavaksi ja se muodostuu monesta eri vaiheesta. Asiakas lähettää ensin handshake-paketin johon palvelin vastaa vastaavalla paketilla. Sitten asiakas lähettää autentikointipaketin ja odottelee jälleen vastausta. Sitten lähetellään erilaisia prepare- ja execute- ja fetch-paketteja ja odotellaan aina kuhunkin vastausta. Yhteyden muuttaminen asynkroniseksi vaatisi koko asiakaskirjaston ja sen rajapintojen uudelleensuunnittelun puhtaalta pöydältä.

Tämän lisäksi MySQL toimii sisäisesti säiepohjaisesti. Kun uusi asiakas avaa siihen yhteyden, asiakkaalle luodaan säie yhteyden ajaksi. Näitä säikeitä voi olla vain tietty määrä yhtaikaa käynnissä. Jos pitkäkestoisia kyselyitä kasaantuu riittävästi, uudet asiakkaat eivät pääse enää sisään ja palvelin alkaa herjata virheilmoituksia.

Miksi säikeet ovat huono juttu

Tämän koko asynkronisuuden pihvi on siinä, että säikeet (kuten prosessitkin) ovat raskaita. Käyttöjärjestelmiä ei ole suunniteltu siten, että palvelinohjelmat voisivat luoda tuhansia tai kymmeniä tuhansia säikeitä asiakkaita palvelemaan. Jokaiselle säikeelle täytyy pitää yllä esimerkiksi erillistä stack-muistialuetta ja muuta kontekstitietoa, ja niiden skedulointiin ja context-switchaamiseen kuluu ylimääräistä prosessoriaikaa.

Asynkroninen I/O taas on huomattavasti kevyempää, koska muistissa pidetään käyttöjärjestelmän puolesta lähinnä muutaman tavun kokoisia deskriptoreita/pointtereita per yhteys. Linuxin epoll-rajapinta on suunniteltu siten, että yhteyksien määrällä ei ole nopeudelle suoraan merkitystä, koska deskriptorit ja callback-funktiot etsitään O(1)-algoritmeilla. Palvelinsofta voi huoleti ladata epoll-rajapintaan kymmeniä tuhansia rinnakkaisia I/O-operaatioita, ja odotella sitten kaikessa rauhassa niiden valmistumista.

6 Comments
matti 2.2.2010 13:00:04

Tämä on kyllä erittäin mielenkiintonen tuttavuus. Hieno idea, pohjimmiltaan aika yksinkertainen! Kiitti nostosta.

tuner 5.2.2010 10:22:21

Yhdellä prosessilla/säikeellä on paha utilisoida useampaa ceepua. Aika paljon käytetään jonkinlaisia hybridimalleja - esim lighttpd-prosessi edessä ja FastCGI:llä siinä kiinni kasa PHP-prosesseja. Myöskin käytetään esim softan sisäisiä threadpooleja. Noiden kanssa on kyllä aina melkosta kikkailua ja juuri tuon takia Go-kielen goroutinet näyttää nopealla vilkaisulla mielenkiintosilta!

tuner 5.2.2010 10:55:51

Näyttää muuten coolilta tuo node.js!

Kennu 5.2.2010 11:07:02

tuner: Mietin samaa asiaa, mutta toisaalta N kpl Node.js-palvelinprosesseja ajaminen rinnakkain voisi ehkä työllistää CPU-coreja riittävästi.

Tuoppi 10.11.2011 15:00:29

Hieno että yritetään kehittää uusia skaalautuvia tekniikoita mutta niitä kaikkia vaivaa yksi perustavanlaatuinen ongelma; yhteensopivuus vanhaan.

Jos sovelluksen kehittäminen aloitetaan täysin tyhjästä ja myös ryhmä joka sovellusta kehittää on koulutettu tähän uuteen tekniikkaan, voidaan tehdä hyvinkin vapaita valintoja.

Valitettavasti käytännön tilanne on että sovelluksia tehdään jotta saataisiin rahaa ja tällöin on tärkeää että sovellus saadaan toteutettua kohtuullisen ripeässä aikataulussa, koodista tulee ylläpidettävää, voidaan hyödyntää jo tehtyjä apukirjastoja sekä poimia valmiita osia jo tehdyistä sovelluksista. Lisäksi on tärkeää että sovellus voi hyödyntää olemassaolevaa tietokantaa jonne on jo ajettu vuosikausien datat.

Voidaankin laskea että paljonko maksaa kouluttaa 10 hengen sovelluskehitystiimi käyttämään aivan uutta (kehittäjäryhmälle uutta) tekniikkaa, tehdä tietokantakonversiot luotettavasti, yms... kuin että nopeuttaa sovellusta esim. ostamalla pari palvelinrautaa lisää ja hajauttamalla sovellus usealle palvelimelle?

Ns. nykyajan sanonta "työ maksaa, aika on rahaa, rauta on halpaa" taitaa pitää valitettavasti paikkansa.

Kennu 10.11.2011 15:08:07

Tuoppi: Tuo asia ei oikeastaan liity tietokantoihin tai webbipalvelimiin vaan teknologiaan yleensä. Alalla työskentelevät joko seuraavat alan kehitystä tai eivät seuraa. Ne jotka eivät seuraa, käyttävät vanhaa tekniikkaa (vrt Cobol), ja ne jotka seuraavat, voivat soveltaa uutta tekniikkaa sopivissa tilanteissa. Markkinatalous näyttää, kumpi voittaa.

Comments are closed.