Uszanowanko Programowanko #32: Node crash course

Liczba odsłon: 28

Minęły już cza­sy, gdy skryp­ty Java­Script słu­ży­ły wy­łącz­nie do reali­zo­wa­nia pros­tych efek­tów gra­ficz­nych w prze­glą­dar­ce. Obecnie moż­li­we jest za po­mo­cą te­go ję­zy­ka two­rze­nie ca­ło­ści skom­pli­ko­wa­nych apli­kac­ji sie­cio­wych na po­zio­mie ser­we­ra i klienta, a na ryn­ku po­ja­wi­ło się no­we sta­no­wis­ko pra­cy: full-stack deve­lo­per.

Wiodącą plat­for­mą umoż­li­wia­ją­cą two­rze­nie częś­ci back-end apli­kac­ji w ję­zy­ku Java­Script jest Node.js. Jej właś­nie zo­sta­ło po­świę­co­ne co­mie­sięcz­ne, 32. spot­ka­nie z cyk­lu Uszanowanko Programowanko, or­ga­ni­zo­wa­ne­go przez fir­mę The Software House.

Spotkanie roz­po­czę­ło się od krót­kie­go wpro­wa­dze­nia do sa­me­go Node.js, któ­re­go udzie­lił Szymon Piecuch w pre­zen­tac­ji „Kickoff to Node.js”. Opisał on hi­sto­rię pro­jek­tu, spo­sób ozna­cza­nia wer­sji sta­bil­nych i eks­pe­ry­men­tal­nych oraz me­to­do­lo­gię wpro­wa­dza­nia no­wych roz­wią­zań i wy­co­fy­wa­nia prze­sta­rza­łych. Następ­nie wy­mie­nił moż­li­we do sto­so­wa­nia w apli­kac­jach ele­men­ty naj­now­szych wer­sji ję­zy­ka Java­Script, ta­kie jak sło­wa klu­czo­we letconst czy wy­ra­że­nia lamb­da (ang. arrow func­tions).

Dłuższą chwi­lę pre­le­gent po­świę­cił kwestii dzie­le­nia apli­kac­ji na mo­du­ły, za­pi­sy­wa­nia ich w for­mie właś­ci­wej dla Node.js oraz dyna­micz­ne­go ła­do­wa­nia mo­du­łów. Z punk­tu wi­dze­nia Node.js mo­du­łem mo­że być za­rów­no skrypt Java­Script, jak i plik tek­sto­wy, obiekt JSON lub plik bi­nar­ny mo­du­łu Node.js. Można też po­trak­to­wać ja­ko mo­duł ca­ły ka­ta­log, o ile umieś­ci się w nim plik pack­age.json z opi­sem mo­du­łu. Dynamicz­ne ła­do­wa­nie mo­du­łów jest rea­li­zo­wa­ne za po­mo­cą CommonJS, jed­nak naj­now­sze wer­sje Node.js bę­dą wy­ko­rzys­ty­wa­ły już stan­dar­do­we roz­wią­za­nia obec­ne w ECMAScript 6.

Tworzenie apli­kac­ji w Node.js wy­ma­ga myś­le­nia w spo­sób asyn­chro­nicz­ny. Maszyna wir­tu­al­na ję­zy­ka pra­cu­je w ra­mach jed­ne­go wąt­ku, wy­ko­nu­jąc ko­lej­ne fa­zy prze­twa­rza­nia rea­li­zo­wa­nych żą­dań i je­że­li apli­kac­ja zbyt dłu­go nie od­da­je ste­ro­wa­nia, jej wy­daj­ność spa­da lub wręcz ca­ła apli­kac­ja ule­ga za­blo­ko­wa­niu. Dlatego istot­ne jest, by wszę­dzie, gdzie jest to moż­li­we sto­so­wać roz­wią­za­nia asyn­chro­nicz­ne. Kiedyś w tym ce­lu sto­so­wa­ło się me­to­dy setTimeout oraz setInterval. W Node.js za­stą­pio­no je roz­wią­za­niem ty­pu call­back, jed­nak w przy­pad­ku za­gnież­dża­nia wie­lu ope­rac­ji asyn­chro­nicz­nych po­wsta­ją­cy tekst źród­ło­wy pro­gra­mu jest nie­czy­tel­ny i po­dat­ny na błę­dy. Dużo wy­god­niej­szym oraz funkcjo­nal­nym roz­wią­za­niem jest wzo­rzec pro­jek­to­wy przy­rze­cze­nia, rea­li­zo­wa­ny za po­mo­cą pa­ry klas PromiseFuture. Istnieje na­wet roz­wią­za­nie naz­wa­ne Promisify, któ­re opa­ko­wu­je ist­nie­ją­ce obiek­ty call­back do po­sta­ci przy­rze­cze­nia. Nadal jed­nak za­pis za­gnież­dżo­nych lub sek­wen­cyj­nych ope­rac­ji mo­że być nie­czy­tel­ny i wy­ma­ga sto­so­wa­nia wie­lu od­dziel­nych blo­ków trycatch. Dlatego w naj­now­szych wer­sjach Node.js wpro­wa­dzo­no ob­słu­gę słów klu­czo­wych asyncawait, za po­mo­cą któ­rych moż­na ope­rac­je za­gnież­dżo­ne rea­li­zo­wać w spo­sób li­nio­wy, obej­mu­jąc je tyl­ko jed­nym, wspól­nym blo­kiem trycatch.

Skuteczne sto­so­wa­nie Node.js, szcze­gól­nie na eta­pie two­rze­nia i roz­wo­ju apli­kac­ji, war­to wspo­ma­gać ze­wnętrz­ny­mi na­rzę­dzia­mi nad­zo­ru­ją­cy­mi ta­ki­mi jak Nodaemon oraz PM2. Uruchamiają one wie­le rów­no­leg­łych in­stan­cji ma­szy­ny wir­tu­al­nej oraz auto­ma­tycz­nie ła­du­ją apli­ka­cję od no­wa w mo­men­cie wy­kry­cia zmian w jej tekś­cie źród­ło­wym, skra­ca­jąc czas mię­dzy wpro­wa­dze­niem zmia­ny i jej prze­tes­to­wa­niem.

Node.js jest pro­jek­tem pod­le­ga­ją­cym in­ten­syw­ne­mu roz­wo­jo­wi. Najnowsze wer­sje śro­do­wis­ka wpro­wa­dza­ją no­we roz­wią­za­nia w za­kre­sie asyn­chro­nicz­ne­go do­stę­pu do sys­te­mu pli­ków, po­dzia­łu apli­kac­ji na wąt­ki, ob­słu­gi pro­to­ko­łu HTTP/2 oraz do­da­wa­nia włas­nych mo­du­łów ma­szy­no­wych pi­sa­nych w ję­zy­ku C++.


Kolejną pre­zen­tac­ję, za­ty­tu­ło­wa­ną „One tool to rule them all”, przed­sta­wił Kamil Raczyński. Postawił on so­bie za cel zna­le­zie­nie na­rzę­dzia, któ­re umoż­li­wia­ło­by zre­a­li­zo­wa­nie moż­li­wie sze­ro­kiej funkcjo­nal­noś­ci apli­kac­ji Node.js bez ko­niecz­noś­ci sa­mo­dziel­ne­go włą­cza­nia zew­nętrz­nych bi­blio­tek. Tego ty­pu na­rzę­dziem jest bi­blio­te­ka Nest, któ­ra in­te­gru­je wie­le nie­za­leż­nych roz­wią­zań w jed­ną ca­łość, na­rzu­ca struk­tu­rę apli­kac­ji i ułat­wia za­pi­sy­wa­nie ko­du przez wska­zy­wa­nie funk­cji i za­leż­noś­ci po­szcze­gól­nych klas za po­mo­cą me­cha­niz­mu ad­no­tac­ji.

Prelegent po­ka­zał moż­li­woś­ci bi­blio­te­ki Nest two­rząc przy­kła­do­wą apli­ka­cję prze­cho­wu­ją­cą i aktua­li­zu­ją­cą kur­sy wa­lut. Aplikac­ja mia­ła ko­rzy­stać z ba­zy da­nych, udo­stęp­niać inter­fejs CRUD do po­bie­ra­nia i aktua­li­zo­wa­nia kur­sów oraz na bie­żą­co po­wia­da­miać klien­tów o zmia­nie kur­su za po­mo­cą me­cha­niz­mu Web­Socket. Dodatkowo, do­stęp do dwóch pod­sta­wo­wych funkcjo­nal­noś­ci – po­bie­ra­nia oraz zmia­ny kur­sów – miał być ogra­ni­czo­ny tyl­ko do kon­kret­nych, uwie­rzy­tel­nio­nych użyt­kow­ni­ków.

Bibliotekę Nest opi­sy­wa­łem już przy okaz­ji re­la­cjo­no­wa­nia spot­ka­nia Gorrion Unplugged, na któ­rym też by­ła ona pre­zen­to­wa­na. Tutaj ogra­ni­czę się za­tem do przed­sta­wie­nia li­sty za­let tej bi­blio­te­ki, wy­mie­nio­nych przez pre­le­gen­ta:

Pod ko­niec pre­zen­tac­ji z sa­li pad­ło py­ta­nie o naj­po­waż­niej­sze wed­ług pre­le­gen­ta prob­le­my przy ko­rzy­sta­niu z Nest. Wymienił on sto­so­wa­nie ję­zy­ka Type­Script, do któ­re­go mu­szą przy­zwy­czaić się oso­by, któ­re wcześ­niej two­rzy­ły apli­kac­je klien­ckie wy­łącz­nie w „czys­tym” ję­zy­ku Java­Script. Drugą wa­dą jest trud­ność w diag­no­zo­wa­niu błę­dów, gdyż częs­to na­wet try­wial­ne uster­ki apli­kac­ji po­wo­du­ją po­ja­wia­nie się w kon­so­li skom­pli­ko­wa­nych, wie­lo­po­zio­mo­wych ko­mu­ni­ka­tów błę­dów.


Trzecia z pre­zen­tac­ji, za­ty­tu­ło­wa­na „Microservices in Node: patterns and tech­niques” i wy­gło­szo­na przez Mariusza Richt­scheida, do­ty­czy­ła bar­dzo mod­nej obec­nie archi­tek­tu­ry mik­ro­ser­wi­sów w aspek­cie śro­do­wis­ka apli­ka­cyj­ne­go Node.js. Prelegent za­czął od wy­mie­nie­nia wad apli­kac­ji mo­no­li­tycz­nych:

Następ­nie przed­sta­wił po­wo­dy, dla któ­rych two­rzy się apli­kac­je zbu­do­wa­ne z mik­ro­ser­wi­sów lub do­ko­nu­je roz­bi­cia ist­nie­ją­cych apli­kac­ji mo­no­li­tycz­nych:

Stosowa­nie tech­ni­ki mik­ro­ser­wi­sów ozna­cza jed­nak po­ja­wie­nie się zu­peł­nie no­wych wyz­wań. Programi­ści mu­szą za­pew­nić spraw­ną ko­mu­ni­kac­ję mię­dzy usłu­ga­mi skła­do­wy­mi oraz bez­pie­czeń­stwo i spój­ność prze­cho­wy­wa­nych da­nych. Na no­wo mu­szą zo­stać roz­wią­za­ne prob­le­my, któ­re w świe­cie apli­kac­ji mo­no­li­tycz­nych zo­sta­ły opa­no­wa­ne tak daw­no, że ich roz­wią­za­nia są przyj­mo­wa­ne za oczy­wi­stość. Na po­moc przy­cho­dzą na szczęś­cie wzor­ce archi­tek­to­nicz­ne.

Najważniej­szym z nich jest bra­ma API, sta­no­wią­ca je­dy­ny punkt wejś­cia do­stęp­ny dla apli­kac­ji zew­nętrz­nych. Brama reali­zu­je uwie­rzy­tel­nia­nie oraz wstęp­ną auto­ry­zac­ję do­stę­pu do usług skła­do­wych. Odpowiada ona też za szyf­ro­wa­nie in­for­mac­ji: dla utrzy­ma­nia pros­to­ty i wy­daj­noś­ci ca­ła we­wnętrz­na ko­mu­ni­kac­ja prze­bie­ga nie­szyf­ro­wa­nym pro­to­ko­łem HTTP. Brama ma po­śred­ni­czyć w ko­mu­ni­kac­ji z pierw­szą usłu­gą skła­do­wą i nie mo­że im­ple­men­to­wać lo­gi­ki biz­ne­so­wej.

Wewnątrz wy­dzie­lo­nej, za­ufa­nej sie­ci skła­do­wych mik­ro­ser­wi­sów ko­niecz­ne jest uru­cho­mie­nie sku­tecz­ne­go i auto­ma­tycz­ne­go re­je­stru usług po­zwa­la­ją­ce­go wy­kry­wać dzia­ła­ją­ce in­stan­cje (ang. ser­vi­ce dis­co­very) oraz we­ry­fi­ko­wać ich stan (ang. health check). Taką ro­lę mo­że peł­nić opro­gra­mo­wa­nie Consul. Uruchamia­ne usłu­gi auto­ma­tycz­nie zgła­sza­ją się do pro­wa­dzo­ne­go przez nie­go re­je­stru, po­da­jąc przy okaz­ji adres punk­tu do­stę­po­we­go REST po­zwa­la­ją­ce­go we­ry­fi­ko­wać po­praw­ność reali­zo­wa­nia da­nej usłu­gi. Consul jest w sta­nie na bie­żą­co in­for­mo­wać wszyst­kie dzia­ła­ją­ce usłu­gi skła­do­we o zmia­nach sta­nu jed­nej z nich, po­zwa­la­jąc im prze­kon­fi­gu­ro­wać się w spo­sób po­mi­ja­ją­cy lub uwzględ­nia­ją­cy nie­któ­re usłu­gi, o ile nie są klu­czo­we do dzia­ła­nia sys­te­mu.

Poszczegól­ne usłu­gi mo­gą ko­mu­ni­ko­wać się ze so­bą bez­po­śred­nio, jed­nak w przy­pad­ku du­żych sys­te­mów ko­rzyst­ne mo­że być uży­cie opro­gra­mo­wa­nia dys­try­bu­to­ra ko­mu­ni­ka­tów ta­kie­go jak Apache Kafka. Oprogra­mo­wa­nie ta­kie mo­że do­ko­ny­wać wstęp­ne­go prze­twa­rza­nia ko­mu­ni­ka­tów, bu­fo­ro­wa­nia ich w sytu­acji zwięk­szo­ne­go ob­cią­że­nia sys­te­mu lub wstrzy­my­wa­nia do mo­men­tu po­wtór­ne­go uru­cho­mie­nia usług, któ­re uleg­ły awarii.

Dane prze­twa­rza­ne przez sys­tem mo­gą być prze­cho­wy­wa­ne we wspól­nej ba­zie da­nych, jed­nak nie jest to po­le­ca­ne. Aby moż­na by­ło łat­wo ska­lo­wać wy­daj­ność i po­jem­ność sys­te­mu, war­to wy­dzie­lić od­ręb­ną ba­zę da­nych dla każ­dej usłu­gi skła­do­wej. Powoduje to jed­nak po­ja­wie­nie się prob­le­mu ze spój­no­ścią da­nych w przy­pad­ku błę­du w trak­cie reali­zac­ji żą­da­nia, któ­re­go nie moż­na już roz­wią­zać me­cha­niz­mem tran­sak­cji. Rozwią­za­niem jest nad­rzęd­ny sys­tem trans­akcyj­ny, któ­ry two­rzy me­ta-transak­cje obej­mu­ją­ce wie­le lo­kal­nych tran­sak­cji w ra­mach usług skła­do­wych. Jeżeli ca­ły łań­cuch usług reali­zu­ją­cych żą­da­nie zgło­si po­praw­ne za­koń­cze­nie je­go ob­słu­gi, me­ta-trans­akcja jest za­twier­dza­na, co mo­że być trak­to­wa­ne przez mik­ro­ser­wi­sy ja­ko za­twier­dze­nie lo­kal­nej tran­sak­cji lub brak ope­rac­ji. Jeżeli jed­nak choć jed­na z usług skła­do­wych zgło­si błąd, wszyst­kie po­przed­nie ele­men­ty łań­cu­cha są o tym za­wia­da­mia­ne, dzię­ki cze­mu mo­gą al­bo anu­lo­wać lo­kal­ne transak­cje, al­bo wy­co­fać już wpro­wa­dzo­ne da­ne.

Niektóre za­da­nia mo­gą być rea­li­zo­wa­ne przez go­to­we mo­du­ły im­ple­men­tu­ją­ce pros­te wzor­ce archi­tek­to­nicz­ne, bez ko­niecz­noś­ci pi­sa­nia włas­ne­go ko­du. Wzorzec ar­chi­tek­to­nicz­ny zło­że­nia API (ang. API com­po­sition) po­zwa­la zreali­zo­wać ope­ra­cję po­przez rów­no­leg­łe lub sze­re­go­we wy­wo­ła­nie kil­ku usług skła­do­wych. Z kolei wzo­rzec bez­piecz­ni­ka (ang. cir­cuit breaker) po­zwa­la rea­go­wać na nad­sy­ła­ne in­for­mac­je o awarii nie­któ­rych usług przez – o ile jest to moż­li­we – po­mi­ja­nie ich lub na­tych­mias­to­we zgła­sza­nie błę­du, bez ko­niecz­noś­ci cze­ka­nia na prze­daw­nie­nie (ang. time-out) wy­wo­ła­nia.

Kolejnym wy­zwa­niem, daw­no już opa­no­wa­nym w świe­cie apli­kac­ji mo­no­li­tycz­nych, jest two­rze­nie dzien­ni­ków zda­rzeń. W sys­te­mie, gdzie wie­le usług skła­do­wych mo­że jed­no­cześ­nie rea­li­zo­wać wie­le drob­nych ope­rac­ji skła­da­ją­cych się na więk­sze dzia­ła­nia, zwyk­łe dzien­ni­ki zda­rzeń nie zda­ją eg­za­mi­nu. Z po­mo­cą przy­cho­dzi kon­cep­cja śle­dze­nia roz­pro­szo­ne­go (ang. dis­tri­bu­ted tracing), w któ­rej two­rzo­ne są dzien­ni­ki od­dziel­nie dla każ­de­go żą­da­nia, a każ­da usłu­ga skła­do­wa do­pi­su­je do właś­ci­we­go dzien­ni­ka da­ne wej­ścio­we, in­for­mac­je o reali­zac­ji żą­da­nia oraz wy­nik. Dzięki te­mu wy­kry­wa­nie uste­rek w dzia­ła­niu po­szcze­gól­nych usług lub na ich sty­ku sta­je się du­żo prost­sze. Ponie­waż roz­po­czę­cie reali­zac­ji żą­da­nia przez każ­dą ko­lej­ną usłu­gę jest za­pi­sy­wa­ne ze znacz­ni­kiem cza­so­wym, pro­gra­mis­ta mo­że ob­ser­wo­wać też wy­daj­ność Mechanizm śle­dze­nia roz­pro­szo­ne­go zo­stał okreś­lo­ny ja­ko stan­dard Open­Tracing, któ­re­go naj­po­pu­lar­niej­szą im­ple­men­tac­ją jest roz­wi­ja­ny przez Uber pa­kiet Jaeger.

Prezenta­cja Mariusza Richt­scheida zro­bi­ła bar­dzo du­że wra­że­nie tak na mnie, jak i na in­nych uczest­ni­kach spot­ka­nia. Prelegent zmie­rzył się ze spo­rą licz­bą py­tań do­ty­czą­cych spo­so­bu dzia­ła­nia roz­pro­szo­nych tran­sak­cji, two­rze­nia bra­my API oraz ska­lo­wa­nia skła­do­wa­nia da­nych w sys­te­mie i po­szcze­gól­nych usłu­gach. Dyskusja do­tknęła też istot­ne­go py­ta­nia kie­dy wpro­wa­dzać archi­tek­tu­rę mik­ro­ser­wi­sów do pro­jek­tu i na ja­kim po­zio­mie roz­wo­ju pro­jek­tu mo­no­li­tycz­ne­go na­le­ży roz­wa­żać je­go roz­dzie­le­nie. Prelegent wy­jaś­nił, że wpro­wa­dze­nie bra­my API mo­że być do­sko­na­łym po­cząt­kiem pro­ce­su roz­dzie­la­nia apli­kac­ji mo­no­li­tycz­nej na mniej­sze frag­men­ty rea­li­zo­wa­ne przez nie­za­leż­ne usłu­gi.


Tematy zwią­za­ne z ję­zy­kiem Java­Script oraz śro­do­wis­kiem Node.js cie­szą się wiel­kim za­in­te­re­so­wa­niem, cze­go prze­ja­wem by­ła frek­wen­cja na spot­ka­niu. Sala by­ła wy­peł­nio­na po brze­gi, po każ­dej pre­zen­tac­ji pre­le­gen­ci od­po­wia­da­li na py­ta­nia pub­licz­noś­ci, a po piz­zy po­da­nej na ko­niec w cią­gu pa­ru mi­nut zo­sta­ły tyl­ko pu­ste pu­deł­ka. Tym bar­dziej na­le­ży do­ce­nić świet­ną or­ga­ni­za­cję spot­ka­nia, któ­re roz­po­czę­ło się z tyl­ko kil­ku­mi­nu­to­wym opóź­nie­niem i skoń­czy­ło zgod­nie z har­mo­no­gra­mem.

Wszystkie pre­zen­tac­je mia­ły wy­so­ki po­ziom me­ry­to­rycz­ny, przy czym z mo­je­go punk­tu wi­dze­nia naj­cie­kaw­sze te­ma­ty pre­zen­to­wa­ła ostat­nia z nich. Choć na te­mat wyż­szo­ści archi­tek­tu­ry mik­ro­ser­wi­sów nad mo­no­li­tycz­ną moż­na kłó­cić się rów­nie za­żar­cie, jak w przy­pad­ku sys­te­mów opera­cyj­nych mo­no­li­tycz­nych i z mik­ro­jąd­rem, trud­no za­prze­czyć, że wy­dzie­lo­ne usłu­gi mo­gą być w wie­lu przy­pad­kach do­brym roz­wią­za­niem i war­to znać naj­lep­sze prak­ty­ki zwią­za­ne z ich wdra­ża­niem, łą­cze­niem i diag­no­zo­wa­niem.