Môžete sa pozrieť na náš systém? Ten, kto napísal softvér, už nie je nablízku a mali sme množstvo problémov. Potrebujeme, aby si to niekto prezrel a vyčistil za nás.
Každý, kto bol v softvérové inžinierstvo po primeranú dobu vie, že táto zdanlivo nevinná požiadavka je často začiatkom projektu, ktorý „má katastrofu napísanú“. Dedenie kódu niekoho iného môže byť nočnou morou, najmä ak je kód nesprávne navrhnutý a chýba mu dokumentácia.
Takže keď som nedávno dostal požiadavku od jedného z našich zákazníkov, aby som si prezrel jeho existujúcu socket.io aplikácia chatovacieho servera (napísaná v Node.js ) a vylepšiť to, bol som mimoriadne opatrný. Pred behom do kopcov som sa ale rozhodol aspoň súhlasiť s tým, že si pozriem kód.
Pohľad na tento kód, bohužiaľ, iba potvrdil moje obavy. Tento chatovací server bol implementovaný ako jeden veľký súbor JavaScriptu. Opätovné inžinierovanie tohto jediného monolitického súboru do čisto navrhnutého a ľahko udržiavateľného softvéru by bolo skutočne výzvou. Ale výzva ma baví, tak som súhlasil.
Existujúci softvér pozostával z jedného súboru, ktorý obsahoval 1 200 riadkov nezdokumentovaného kódu. Fuj. Navyše bolo známe, že obsahuje určité chyby a má problémy s výkonom.
Okrem toho preskúmanie protokolových súborov (vždy vhodné začať pri dedení kódu niekoho iného) odhalilo potenciálne problémy s únikom pamäte. V určitom okamihu sa uvádzalo, že tento proces využíval viac ako 1 GB RAM.
Vzhľadom na tieto problémy sa okamžite ukázalo, že bude potrebné uskutočniť reorganizáciu a modularizáciu kódu, a to ešte pred pokusom o ladenie alebo vylepšenie obchodnej logiky. Za týmto účelom niektoré z počiatočných problémov, ktoré bolo potrebné vyriešiť, zahŕňali:
V procese začatia reštrukturalizácie kódu som sa okrem riešenia vyššie uvedených konkrétnych problémov chcel venovať aj niektorým z kľúčových architektonických cieľov, ktoré sú (alebo by aspoň mali byť) spoločné pre návrh ľubovoľného softvérového systému. . Tie obsahujú:
Naším cieľom je prejsť od jediného súboru zdrojového kódu monolitického monga k modularizovanej množine čisto navrhnutých komponentov. Výsledný kód by sa mal výrazne ľahšie udržiavať, vylepšovať a ladiť.
Pre túto aplikáciu som sa rozhodol usporiadať kód do nasledujúcich samostatných architektonických komponentov:
logs
, ktorý bude obsahovať všetky súbory denníka)Na tomto špecifickom prístupe nie je nič kúzla; môže existovať mnoho rôznych spôsobov, ako reštrukturalizovať kód. Len som osobne cítil, že táto organizácia bola dostatočne čistá a dobre organizovaná bez toho, aby bola príliš zložitá.
Výsledná organizácia adresárov a súborov je uvedená nižšie.
Balíky protokolovania boli vyvinuté pre väčšinu dnešných vývojových prostredí a jazykov, takže v dnešnej dobe je zriedkavé, že budete musieť „zaviesť svoje vlastné“ možnosti protokolovania.
Pretože pracujeme s Node.js, vybral som si uzol log4js , ktorá je v podstate verziou log4js knižnica na použitie s Node.js. Táto knižnica má niekoľko skvelých funkcií, ako je schopnosť protokolovať niekoľko úrovní správ (VÝSTRAHA, CHYBA atď.) A môžeme mať k dispozícii priebežný súbor, ktorý je možné rozdeliť napríklad na dennej báze, takže nemusíme narábajte s obrovskými súbormi, ktorých otvorenie bude trvať veľa času a bude ťažké ich analyzovať a analyzovať.
Pre naše účely som vytvoril malý obal okolo uzla log4js, aby som pridal ďalšie konkrétne požadované funkcie. Všimnite si, že som sa rozhodol vytvoriť obal okolo uzla log4js, ktorý potom použijem v celom svojom kóde. Toto lokalizuje implementáciu týchto rozšírených funkcií protokolovania na jednom mieste, čím sa zabráni nadbytočnosti a nepotrebnej zložitosti celého môjho kódu, keď vyvolám protokolovanie.
Pretože pracujeme s I / O a mali by sme niekoľko klientov (používateľov), ktorí vytvoria niekoľko pripojení (zásuviek), chcem mať možnosť sledovať aktivitu konkrétneho používateľa v súboroch denníka a tiež chcem vedieť zdroj každej položky protokolu. Očakávam preto, že niektoré záznamy v denníku budú zodpovedať stavu aplikácie a niektoré, ktoré budú špecifické pre aktivitu používateľov.
V kóde obálky protokolovania môžem zmapovať ID používateľa a zásuvky, čo mi umožní sledovať akcie, ktoré boli vykonané pred a po udalosti CHYBY. Obálka protokolovania mi tiež umožní vytvoriť rôzne protokolovacie nástroje s rôznymi kontextovými informáciami, ktoré môžem odovzdať obsluhovačom udalostí, aby som poznal zdroj záznamu protokolu.
K dispozícii je kód obálky protokolovania tu .
Pre systém je často potrebné podporovať rôzne konfigurácie. Tieto rozdiely môžu byť buď rozdiely medzi vývojovým a produkčným prostredím, alebo dokonca založené na potrebe zobraziť rôzne zákaznícke prostredia a scenáre použitia.
Namiesto požadovania zmien v kóde, ktoré to podporujú, je bežnou praxou kontrolovať tieto rozdiely v správaní pomocou konfiguračných parametrov. V mojom prípade som potreboval schopnosť mať rôzne prostredia na vykonávanie (inscenačné a produkčné), ktoré môžu mať odlišné nastavenia. Tiež som sa chcel ubezpečiť, že testovaný kód funguje dobre pri inscenácii aj produkcii, a keby som na tento účel potreboval zmeniť kód, zneplatnilo by to proces testovania.
Pomocou premennej prostredia Node.js môžem určiť, ktorý konfiguračný súbor chcem použiť na konkrétne spustenie. Preto som presunul všetky predtým pevne nakonfigurované konfiguračné parametre do konfiguračných súborov a vytvoril som jednoduchý konfiguračný modul, ktorý načíta správny konfiguračný súbor s požadovaným nastavením. Tiež som kategorizoval všetky nastavenia, aby som vynútil určitý stupeň organizácie v konfiguračnom súbore a uľahčil navigáciu.
Tu je príklad výsledného konfiguračného súboru:
{ 'app': { 'port': 8889, 'invRepeatInterval':1000, 'invTimeOut':300000, 'chatLogInterval':60000, 'updateUsersInterval':600000, 'dbgCurrentStatusInterval':3600000, 'roomDelimiter':'_', 'roomPrefix':'/' }, 'webSite':{ 'host': 'mysite.com', 'port': 80, 'friendListHandler':'/MyMethods.aspx/FriendsList', 'userCanChatHandler':'/MyMethods.aspx/UserCanChat', 'chatLogsHandler':'/MyMethods.aspx/SaveLogs' }, 'logging': { 'appenders': [ { 'type': 'dateFile', 'filename': 'logs/chat-server', 'pattern': '-yyyy-MM-dd', 'alwaysIncludePattern': false } ], 'level': 'DEBUG' } }
Doteraz sme vytvorili štruktúru priečinkov na hosťovanie rôznych modulov, nastavili sme spôsob načítania konkrétnych informácií o prostredí a vytvorili sme systém protokolovania. Pozrime sa teda, ako dokážeme spojiť všetky časti bez zmeny podnikového kódu.
Vďaka našej novej modulárnej štruktúre kódu je náš vstupný bod app.js
je dostatočne jednoduchý a obsahuje iba inicializačný kód:
var config = require('./config'); var logging = require('./logging'); var ioW = require('./ioW'); var obj = config.getCurrent(); logging.initialize(obj.logging); ioW.initialize(config);
Keď sme definovali našu štruktúru kódu, povedali sme, že ioW
priečinok by obsahoval kód súvisiaci s podnikaním a socket.io. Konkrétne bude obsahovať nasledujúce súbory (všimnite si, že kliknutím na ktorýkoľvek z uvedených názvov súborov môžete zobraziť zodpovedajúci zdrojový kód):
index.js
- spracováva inicializáciu a pripojenia socket.io, rovnako ako predplatné udalostí, plus centralizovaný obslužný program udalostíeventManager.js
- hostí všetku obchodnú logiku (obsluhy udalostí)webHelper.js
Pomocné metódy na vykonávanie webových požiadaviek -.linkedList.js
Prepojená listová trieda nástrojov -Opätovne sme upravili kód, ktorý generuje webovú požiadavku, a presunuli sme ho do samostatného súboru. Podarilo sa nám udržať našu obchodnú logiku na rovnakom mieste a bez úprav.
Jedna dôležitá poznámka: V tejto fáze eventManager.js
stále obsahuje niektoré pomocné funkcie, ktoré by sa skutočne mali extrahovať do samostatného modulu. Pretože však našim cieľom v tomto prvom prechode bolo reorganizovať kód pri minimálnom dopade na obchodnú logiku a tieto pomocné funkcie sú príliš zložito spojené s obchodnou logikou, rozhodli sme sa odložiť to na ďalší prechod pri zlepšovaní organizácie kód.
Keďže súbor Node.js je podľa definície asynchrónny, často sa stretávame s trochou krysieho hniezda „spätného volania v pekle“, čo sťažuje navigáciu a ladenie kódu. Aby som sa tomuto úskaliu vyhnul, vo svojej novej implementácii som zamestnal sľubuje vzor a konkrétne využívajú Modrý vták čo je veľmi pekná a rýchla knižnica sľubov. Sľuby nám umožnia sledovať kód, akoby bol synchrónny, a tiež poskytnúť správu chýb a čistý spôsob štandardizácie odpovedí medzi hovormi. V našom kóde je implicitná zmluva, že každý spracovateľ udalostí musí vrátiť prísľub, aby sme mohli spravovať centralizované spracovanie chýb a protokolovanie.
Všetci spracovatelia udalostí vrátia prísľub (bez ohľadu na to, či asynchrónne volajú alebo nie). Keď je toto na mieste, môžeme centralizovať spracovanie chýb a protokolovanie a zaisťujeme, že ak sa vyskytne neošetrená chyba vo vnútri obsluhy udalosti, táto chyba sa zachytí.
function execEventHandler(socket, eventName, eventHandler, data){ var sLogger = logging.createLogger(socket.id + ' - ' + eventName); sLogger.info(''); eventHandler(socket, data, sLogger).then(null, function(err){ sLogger.error(err.stack); }); };
V našej diskusii o protokolovaní sme spomenuli, že každé pripojenie bude mať svoj vlastný protokolovací nástroj s kontextovými informáciami. Konkrétne pri vytváraní viazame ID zásuvky a názov udalosti na záznamník, takže keď tento záznamník odovzdáme obslužnej rutine udalosti, každý riadok protokolu bude obsahovať tieto informácie:
var sLogger = logging.createLogger(socket.id + ' - ' + eventName);
Jeden ďalší bod, ktorý stojí za zmienku, pokiaľ ide o spracovanie udalostí: V pôvodnom súbore sme mali setInterval
volanie funkcie, ktoré sa nachádzalo vo vnútri obslužnej rutiny udalosti pripojenia socket.io, a túto funkciu sme identifikovali ako problém.
io.on('connection', function (socket) { ... Several event handlers .... setInterval(function() { try { var date = Date.now(); var tmp = []; while (0 Tento kód vytvára časovač so zadaným intervalom (v našom prípade to bola 1 minúta) pre každú jednu žiadosť o pripojenie že dostaneme. Napríklad, ak máme v danom okamihu 300 online zásuviek, potom by sme mali každú minútu spustených 300 časovačov. Ako vidíte v kóde vyššie, problém spočíva v tom, že neexistuje použitie soketu ani žiadna premenná, ktorá bola definovaná v rozsahu obsluhy udalosti. Jediná použitá premenná je messageHub
premenná, ktorá je deklarovaná na úrovni modulu, čo znamená, že je rovnaká pre všetky pripojenia. Preto nie je absolútne potrebné samostatný časovač pre každé pripojenie. Takže sme to odstránili z obslužnej rutiny udalosti pripojenia a zahrnuli sme to do nášho všeobecného inicializačného kódu, ktorým je v tomto prípade initialize
funkcie.
Nakoniec sme v našom spracovaní odpovedí V webHelper.js
pridali spracovanie pre každú nerozpoznanú odpoveď, ktorá bude zaznamenávať informácie, ktoré potom budú užitočné pri procese ladenia:
if (!res || !res.d || !res.d.IsValid){ logger.debug(sendData); logger.debug(data); reject(new Error('Request failed. Path ' + params.path + ' . Invalid return data.')); return; }
Posledným krokom je nastavenie súboru protokolovania pre štandardnú chybu súboru Node.js. Tento súbor bude obsahovať nespracované chyby, ktoré sme možno prehliadli. Na nastavenie procesu uzla v systéme Windows (nie je to ideálne, ale viete ...) ako služby používame nástroj s názvom nssm ktorý má vizuálne používateľské rozhranie, ktoré umožňuje definovať štandardný výstupný súbor, štandardný chybový súbor a premenné prostredia.
O výkone Node.js
Node.js je jednovláknový programovací jazyk. Na zlepšenie škálovateľnosti môžeme použiť niekoľko alternatív. K dispozícii je modul uzlov klastra alebo jednoducho len pridať viac procesov uzlov a na vrchole nich umiestniť nginx, ktorý vykoná presmerovanie a vyrovnanie záťaže.
V našom prípade však za predpokladu, že každý podproces klastra uzlov alebo proces uzla bude mať svoj vlastný pamäťový priestor, nebudeme môcť ľahko zdieľať informácie medzi týmito procesmi. Pre tento konkrétny prípad teda budeme musieť použiť externé úložisko dát (ako napr opakovať ), aby boli online zásuvky dostupné pre rôzne procesy.
Záver
Týmto všetkým sme dosiahli významné vyčistenie kódu, ktorý nám bol pôvodne odovzdaný. Nejde o to, aby bol kód dokonalý, ale skôr o jeho reinžiniering, aby sa vytvoril čistý architektonický základ, ktorý sa bude ľahšie podporovať a udržiavať a ktorý uľahčí a zjednoduší ladenie.
V súlade s vyššie vymenovanými kľúčovými princípmi návrhu softvéru - udržiavateľnosťou, rozšíriteľnosťou, modularitou a škálovateľnosťou - sme vytvorili moduly a štruktúru kódu, ktoré jasne a zreteľne identifikovali rôzne zodpovednosti modulu. Zistili sme tiež niektoré problémy v pôvodnej implementácii, ktoré vedú k vysokej spotrebe pamäte, ktorá znižovala výkon.
Dúfam, že sa vám článok páčil, dajte mi vedieť, ak máte ďalšie pripomienky alebo otázky.