Безспорно JavaScript владее света на браузърните приложения, но има ли той приложение и от страна на сървъра? Много хора приемат JavaScript като неизбежното зло в света на уеб приложенията, просто защото това е единственият език, който се поддържа от повечето браузъри (това е на път да се промени от WebAssembly), но за сървърни приложения често JavaScript изобщо не се взима предвид. Като основни причини най-често се изтъква, че това е скриптов език, който е бавен, не се компилира и не се валидира преди изпълнение.

Node.js е най-популярната среда за изпълнение на JavaScript извън браузърите. Той намира все повече приложения, сред които десктоп приложения и конзолни инструменти. Тук обаче ще разгледаме доколко той е подходящ за уеб микросървиси.

Скрипт

Наистина JavaScript, както Python и други скриптови езици, не се компилира преди изпълнение. Всъщност компилиране има, но то става скрито по време на изпълнение. Важното в случая е, че преди да стартираме нашата програма, тя е просто текст. Дори не сме сигурни дали той е синтактично коректен. Всъщност това може и да е предимство. Единственият начин да валидираме нашата програма е да я изпълним, и то с всички нейни разклонения. Това прави тестването задължително. Можем да кажем, че скриптовите езици ни стимулират да правим повече тестове, което в крайна сметка е добре за всяка една програма независимо от езика. При компилираните езици се изкушаваме да мислим, че ако се компилира, всичко е наред.

Съществено предимство на скриптовите програми е, че те са с “отворен код”. Просто изходният код представлява самата изпълнима програма. Това позволява бърз достъп до кода на програмата и нейните библиотеки. Лесно можем да видим какво точно прави тя и дори ако е необходимо лесно можем да я променим – просто редактираме съответния текстов файл и рестартираме. При компилираните програми обикновено изходният код не се разпространява заедно с изпълнимия. Дори да знаем къде се съхранява изходният код, трябва да намерим точната версия, от която е компилиран изпълнимият код.

Парадигми

JavaScript не налага парадигми. Той позволява както обектно-ориентирано, така и функционално програмиране. JavaScript поддържа класове (от ES6) и наследяване на обекти, дори динамично. На практика обаче, функционалният стил е по-разпространен в света на JavaScript.

Всъщност езикът е доста прост. Всичко е изградено на базата на речници (maps) и функции. Например прототипите и closures са просто вериги от наследяващи се речници, а класовете всъщност са функции. Учудващо е колко много конструкции могат да се моделират на базата на тези две концепции. И в това е красотата на езика, че е едновременно прост и експресивен.

Статични типове

Широко разпространено схващане е, че сериозните (enterprise) програми изискват строга типизация на данните. Счита се, че това в голяма степен осигурява коректност на програмата. Но това, че подаваме правилния тип (число, текст и т.н.), не гарантира правилната стойност. Единственият начин да проверим коректността на програмата е чрез тестове. Всъщност липсата на строга типизация и динамичността прави тестването много по-лесно. Подмяната на различни зависимости с тестови заместители (stubs/mocks/fakes) в JavaScript код е изключително лесно и това прави писането на тестове бързо и приятно. В други езици често се налага генериране на код за тестови заместители или дори промяна на продуктивния код, така че да е по-тестваем.

Динамичността на JavaScript прави много по-лесна реализацията на общи алгоритми, които могат да работят върху различни обекти и функции. Няма нужда от reflection, generics и т.н. Това значително опростява кода и позволява създаването на мощни библиотеки с широко приложение като lodash, async, sinon, chai и т.н. Също работата с данни без строго дефинирана структура става много по-лесно. Например JSON документи, с каквито работят повечето уеб сървиси.

В крайна сметка се оказва че статичните типове са силно надценени. На практика те повече пречат отколкото помагат. Основното им предимство е при навигация и рефакториране на кода.

Все пак привържениците на статичните типове могат да ползват и TypeScript в Node.js. Той е надмножество на JavaScript с опционални статични типове. TypeScript се компилира до JavaScript.

Производителност

Обикновено скриптовите езици се считат за сравнително бавни и неподходящи за приложения с високо натоварване. Но Node.js може да ви изненада. Програмите се изпълняват от оптимизиран JavaScript интерпретатор – V8, същият който се използва и в Google Chrome. По време на изпълнение той компилира JavaScript до машинни инструкции. В допълнение Node.js предлага интерфейси за достъп до ресурсите на операционната система – файлове, процеси, мрежа и т.н. Подобно на nginx, и Node.js използва асинхронен модел на работа с входно/изходни операции. Това позволява да се използват много по-ефективно ресурсите на операционната система. За да добием представа за производителността направихме един прост тест. Написахме на Node.js, Go и Java (Spring Boot) един минимален REST микросървис, който получава JSON документ (~4KБ), десериализира го и обратно го сериализира в отговора. Целият код и повече подробности могат да бъдат намерени на https://github.com/dotchev/rest-bench.

Да, резултатите са изненадващи. Вероятно доброто представяне на Node.js се дължи на това че HTTP протоколът и обработката на JSON (част от V8) са реализирани на C.

Паралелизъм

Node.js изпълнява JavaScript само в една нишка. На пръв поглед това изглежда бавно и неефективно. Всъщност благодарение на асинхронния режим на работа Node.js може да изпълнява няколко заявки паралелно. Ето относителното време на някои типични операции в една програма.

Както се вижда, входно/изходните операции (диска, мрежата и т.н.) отнемат многократно повече време от изпълнението на програмния код. А тези операции са типични за повечето микросървиси. Те най-често получават заявки по мрежата (обикновено HTTP), работят с файлове и комуникират с бази данни или други микросървиси отново по мрежата. По-голямата част от времето за обработване на една заявка преминава в чакане на входно/изходни операции. Именно за такова натоварване е оптимизиран Node.js. Докато някои от заявките чакат входно/изходни операции, се изпълнява JavaScript кода на някоя от заявките, които имат необходимите данни в паметта. Така JavaScript кодът се изпълнява в една нишка, но входно/изходните операции се изпълняват паралелно. За оползотворяването на повече изчислителни ресурси обикновено се стартират няколко Node.js процеса.

От друга страна последователното изпълнение на JavaScript кода предлага и някои съществени преимущества, като отсъствие на синхронизационни проблеми (race conditions). Всеки програмист с опит знае колко трудни за тестване и дебъгване са тези проблеми. Изпълнението на програмния код в една нишка прави тези проблеми невъзможни.

(Източник: https://bholley.net/blog/2015/must-be-this-tall-to-write-multi-threaded-code.html)

Скалиране

Node.js стартира относително бързо, консумира малко памет и не натоварва системата с множество нишки. Това го прави много подходящ за микросървиси в облака, където един сървис обикновено е скалиран с много инстанции, всяка една с ограничени ресурси, често се спират и стартират нови инстанции според промените в натоварването.

Екосистема

В много отношения Node.js се възползва от популярността и огромната екосистема около JavaScript. Редица организации инвестират съществени ресурси в тази област. Съществуват множество инструменти за писане, тестване, дебъгване, профилиране и т.н. Повечето от тях могат да се използват и с Node.js.

JavaScript еволюира доста бързо през последните години. Напоследък всяка година излиза нова версия. Например в ES8 бяха добавени асинхронни функции (async/await), които решават по елегантен начин известния callback hell проблем и значително опростяват кода.

Един от основните фактори за успеха на Node.js е отличното управление на зависимостите съвместно с npm. Всички зависимости се инсталират под директорията на проекта, което прави много лесно неговото прехвърляне на друго място. Могат да се инсталират паралелно различни версии на един и същи пакет, от който проектът зависи индиректно. Поддръжката на гъвкави диапазони от семантични версии на зависимостите дава пълен контрол кои ъпдейти да се приложат. Например може да се укаже да се вземат само корекции в зависимостите, но не и несъвместими промени.

npmjs.com е най-голямото и най-бързо растящото хранилище на софтуерни пакети. Скоро то ще достигне един милион пакета. Там могат да се открият готови компоненти за най-различни сценарии, а това може да съкрати значително времето за разработване на даден проект.

Продуктивност

Съчетанието от простота, гъвкавост, изразителна мощ, многообразие от библиотеки и развити инструменти увеличава не само продуктивността, но и удовлетвореността на програмистите. Това всъщност е неговото основно предимство. Обикновено за една и съща програма на JavaScript се изисква много по-малко код отколкото на повечето популярни езици.

Възможността да се използва един и същи език както в браузъра, така и на сървъра, опростява значително разработването на едно уеб приложение.

Заплатите на програмистите растат през годините, докато цените на изчислителните ресурси намаляват. Затова е по-икономично да се оптимизира първо продуктивността на програмистите, а после производителността на програмите.

Както видяхме Node.js има доста предимства, което го прави оптимален за повечето уеб микросървиси. Разбира се, той не винаги е най-доброто решение. Но за да се прецени

това, трябва да се направят необходимите измервания и да се вземат предвид и фактори като цена за разработване и бъдеща поддръжка. Изборът на технология не бива да се осланя на предположения и остарели схващания.

И така въпросът не е “Защо Node.js?”, а “Защо не Node.js?”.


За автора:

Петър Дочев

Работя като разработчик на софтуер в SAP от 14 години. Участвал съм в много проекти, в които съм използвал различни програмни езици като Java, C++, JavaScript и Go. През последните години работя основно върху микросървиси, които са част от SAP Cloud Platform. Като среда за изпълнение обикновено използваме Cloud Foundry и Kubernetes.

 

 

 

 

Share This