Node.js Worker Threads – על קצה המזלג

כידוע, Async ו Await הם לא פתרונות קסם, ולעיתים, אנו נדרשים לבצע מטלה שעלולה "לתקוע" את הEvent Loop, כמו חישוב מורכב , ליטרציה כבדה של מערכים ועוד.
אם יש לכם פונקציה שעושה חישוב מורכב, להוסיף בתחילתה async לא ייפתור את הבעיה והיא עדיין תתקע את הEvent Loop.
להזכירכם , NODE עובד בצורה של Event Loop שהוא Single Threaded למעשה.

(ליתר דיוק, הוא דווקא כן עושה שימוש בTHREADS , דהיינו הlibuv מייצר POOL של 4 THREADS כברירת מחדל מה שמאפשר לEVENT LOOP להעביר "משימות כבדות" לPOOL בצורה אוטומטית, בעיקר משימות שקשורות למערכת הקבצים, הצפנות, דחיסות וכו. אבל זה כבר נושא אחר).
בכל מיקרה, זאת האחריות שלנו כמפתחים לכתוב קוד שעושה שימוש בEvent Loop בצורה הנכונה ביותר.

נו, אז מה הפיתרון?

פיתרון אחד לבעיה שאומנם לא מתאים תמיד מבחינת ארכיטקטורה נכונה לכל המקרים, אבל לעיתים דווקא כן יכול להתאים הוא שימוש בWorker Threads, הדגשים הם:

  • לכל THREAD יש Process אחד ואת הEVENT LOOP שלו. – מה שרץ בWorker Thread אחד לא משפיע על השני מבחינת חסימה (None Blocking)

  • ניתן לחלוק זיכרון (לדוגמה באמצעות SharedArrayBuffer) וניתן להעביר מידע לThread שלנו.

  • לכל THREAD יש את הINSTANCE של הV8 וlubuv שלו (ISOLATED, וכן, הם צורכים משאבים)
  • מומלץ בעיקר למשימות שדורשות משאבי CPU

בואו נתחיל ונכתוב את הקוד הבא בParent שלנו (יכול להיות גם בApp.js שלכם):

//Parent Code:
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');

בשורה 3 אנו טוענים לWORKER את הקובץ שאנחנו רוצים שהWORKER יריץ, במקרה שלנו worker.js.

נוסיף לקובץ את השורות הבאות:

//Subscribing for message on our Parent.
worker.on('message', message => console.log(message))

//Sending Message to our Worker
worker.postMessage('Hello');

בשורה 2, אנו "נרשמים" (Subscribe) להודעה מהWORKER שלנו, משמע כאשר הWORKER שלנו ישלח לנו הודעה במקרה שלנו הPARENT יריץ console.log(message).

בשורה 5, אנו שולחים לWORKER שלנו message עם הערך 'Hello', עד כאן, לא קרה בעצם יותר מדי, יצרנו WORKER ועשינו Subscribe לMessage ממנו, שלחנו לו Message עם הערך Hello. אבל הParent שלנו עדיין לא ידפיס לconsole את הmessage והסיבה לכך היא שהWorker שלנו כרגע לא שולח לParent
Message בחזרה.

בworker.js נכתוב את הקוד הבא:

const { parentPort } = require('worker_threads');
parentPort.on('message', message => 
    parentPort.postMessage({ hello: message })
);

בשורה 2 אנו עושים Subscribe מהWorker שלנו לParent, דהיינו, כאשר הWorker שלנו יקבל Message הוא יריץ את הפקודה
parentPort.postMessage({ hello: message });
מה שישלח למעשה לParent שלנו .

עד כאן למעשה יצרנו סינכרון מלא בין הParent שלנו לבין הWorker Thread שלו. הנה הקוד המלא:

//Parent Code:
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');

//Subscribing for message on our Parent.
worker.on('message', message => console.log(message));

//Sending Message to our Worker
worker.postMessage('Hello');
const { parentPort } = require('worker_threads');
parentPort.on('message', message => 
    parentPort.postMessage({ hello: message });
)

אני ממליץ כמעט תמיד ליצור קובץ נפרד לThread שלנו (במקרה של הדוגמא כאן הוא .worker.js).
אבל במידה ולמקרה הספציפי, קובץ נפרד פחות מתאים נוכל להשתמש בטכניקה הבאה של שימוש בisMainThread כדי להכניס את כל הקוד שלנו לקובץ אחד:

const {Worker, isMainThread} = require('worker_threads');

//האם אנו רצים מה
//Parent?
if(isMainThread) {
//יוצרים Worker 
// חדש ושולחים אליו את הקובץ הנוכחי
 const worker = new Worker(__filename);
} else {
  //קוד זה רץ ב
  //Worker
 console.log('Hi from your worker!');
}

במקרה הנל אנו עושים שימוש בisMainThread שמאפשר לנו לדעת האם אנחנו רצים כרגע מתוך הWorker Thread או מתוך הParent שלנו ולעשות הפרדה בין הCode שירוץ.


איך מעבירים מידע ראשוני לWorker שלנו?

כדי להעביר DATA לWorker , נוכל לעשות שימוש באפשרות workerData, לדוגמה:

const { worker, inMainThread, workerData} = require('worker_threads');

if(isMainThread) {
 const worker = new Worker(__filename, { workerData: 'Hi!' })
} else {
 console.log(workerData); // will print 'Hi!'
}

יש עוד דברים מעניין שניתן לעשות בהם שימוש כמו SHARE_ENV ,eval, ממליץ לכם לתת הצצה בתיעוד ולגלות דברים מעניינים.

אופציה נוספת שאני חושב ששווה לתת דגש עליה היא resourceLimit:

const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js', { resourceLimits: {maxOldGenerationSizeMb: 10} })

במקרה הנל אנו מגבילים את הגודל של הHEAP (זיכרון) ל10MB. אם הWorker יגיע למגבלה הזאת הוא יושמד עם שגיאה.

אילו Events קיימים בWorker?

  • message , לדוגמא parentPort.postMessage()
  • exit – כשהworker שלנו הפסיק.
  • online – כאשר הWorker שלנו התחיל להריץ את הקוד שלו
  • error – כאשר נזרקה מהworker שלנו uncaught exception.

הערה לגבי השימוש בpostMessage:


בדוגמאות הנל עשינו שימוש ב postMessage כדי לשלוח מידע/הודעות בין הPARENT לThread. חשוב מאוד לדעת שכאשר אנו עושים שימוש בPostMessage, הדאטא שאנו מעבירים בעצם "משוכפל" – Cloned עם כל החסרונות / ייתרונות בכך, דהיינו, שיכפול של דאטא מורכב יכול לעלות לנו בהרבה כח CPU, ככל שהדאטא יותר מורכב/מסובך/עמוק יותר כך הוא יידרוש יותר כח מחשוב.

שלא נדבר על הRAM הכפול בעצם שאנו צורכים כדי לשמור את הדאטא הנוסף הזה בזיכרון.
מבחינת ארכיטקטורה, מאוד חשוב לשים לב לזה. כדי ללמוד יותר על הנושא ניתן להריץ חיפוש בגוגל עם המונח is postmessage slow, וגם להכיר את האפשרות של TransferList ועוד. (אולי בהזדמנות אכתוב על זה פוסט נפרד).

מצאתם טעות? הערות? שאלות? הסתדרתם? נתקעתם? כתבו לי בתגובות!


פריימווארקים פופולארים לWEB בNodeJS – על קצה המזלג

Nodejs מגיעה עם המון מודלים מובנים, אבל בהרבה מהמקרים, אנו נצטרך לעשות שימוש בפריימוורקים על מנת שיענו לנו על צורך מסויים, ובהרבה מהמקים – פשוט להקל עלינו את העבודה מול המודולים המובנים.
בנוסף, שימוש בפרייווארק בדרכ עושה לנו סדר בקוד, ומקל על התחזוקה שלו.

הפופולארית ביותר – Express

התקנה באמצעות NPM

npm install express

קוד דוגמה ליצירת שרת באקספרס:

const express = require('express');
const myServer = express();

myServer.get('/', (req, res) => {
 res.send('Welcome to our website');
});

myServer.get('/products', (req, res) => {
 res.send('Products list');
});

myServer.listen(4153, () => {
 console.log('Hello from express server');
});

כאשר משתמש יכנס לכתובת הראשית של הURL שלנו, הוא יקבל בדפדפן את ההודעה
Welcome to our website.
כאשר הגולש ייכנס לכתובת /products הוא יקבל בדפדפן את ההודעה Products list.

לאתר אקספרס – https://expressjs.com/


פריימוארקים פופולאריים נוספים:
1. KOAJS – לאתר ליחצו כאן
2. Sails – דומה מאוד לRails, היא MVC, עם אפשרויות מיוחדות לREST API's. ליחצו כאן לאתר
3. Meteor – עולם ומלואו, לאתר ליחצו כאן
4. וכמובן האהובה עלי ביותר: NEST, לאתר ליחצו כאן

שימוש בNodemon בסביבות הפיתוח שלנו

NodeMon מאפשר לנו לאתחל את שרת הNode שלנו בכל פעם שמתבצע שינוי בקובץ, יעיל מאוד והכרחי כאשר אנו מפתחים בNode.

על מנת להתקין את nodemon נריץ את הפקודה הבאה:

npm i -g nodemon

נניח והקובץ הראשי שלנו הוא main.js, כדי להריץ אותו באמצעות nodemon נריץ את הפקודה הבאה:

nodemon main.js

וזאת במקום הפקודה הרגילה שהיא:

node main.js

ועכשיו בכל פעם שנשנה את הקובץ main.js שלנו – nodemon יפעיל את עצמו מחדש ואנו נוכל לראות את השינויים שעשינו מיידית ללא צורך לאתחל מחדש את השרת.

יצירת שרת HTTP בNodeJS באמצעות הAPI http – על קצה המזלג

http – הוא בילטאין בNodeJS, לכן, לא נדרשת התקנה של שום ספרייה. בExpress למשל אנו נצטרך כמובן להתקין אותה באמצעות NPM.

//השורה הבאה מחזירה את ה
// API של 
// http
const myHttpAPI = require('http');
const myRequestListener = (req, res) => {
 res.end('Hello from NodeJS !!!');
}

//הפקודה הבאה יוצרת את השרת שלנו למעשה, אך לא מפעילה אותו
const myServer = myHttpAPI.createServer(myRequestListener);

//נאזין לקריאות לשרת שיצרנו מקודם בפורט 80 או כל פורט אחר שנבחר
myServer.listen(80, () => {
 console.log('YAY! The server is ready!');
});

זוכרים שדיברנו על Events?
בכל פעם שמתקבלת בקשה – Request לשרת שלנו, משודר למעשה EVENT בשם 'request'.
הפונקציה myRequestListener שלנו בעצם "מאזינה" לEvent 'request' ומופעלת בכל פעם שמגיע אלינו request חדש.

חשוב להדגיש שהפונקציה myServer.listen היא פונקציה אסינכרונית, דהיינו, הקוד שבתוך פונקצית הcallback שלה יופעל רק כאשר הmyServer שלנו יהיה מוכן.


מצאתם טעות? הערות? שאלות? הסתדרתם? נתקעתם? כתבו לי בתגובות!

קלסטרים בNodeJS – הקדמה.

בסביבות פרודקשין, חשוב לעשות שימוש בNode Clusters, לדוגמה אם למעבד שלנו יש כמה ליבות – כל Cluster יעשה שימוש בליבה וכך נוכל לנצל את מלוא היכולת של המעבד שלנו. אגב, גם אם למעבד לשנו יש רק ליבה אחת – עדיין כדאי לעשות שימוש בCluster כיון שיש לו יכולת לנטר את הפרוסס הקיים ולהתחיל אחד חדש כאשר הקיים נכשל/נסגר.

ניתן להריץ קלסטרים של נוד באמצעות מודל הCluster שהוא בילט-אין בNode. לחילופין ניתן ליצור קלסטר בשימוש בכלים כמו PM2 שיכול לעשות שימוש בכל הליבות של השרת שלנו בצורה אוטומטית (עם Load Balancer מובנה) ולהפעיל מחדש את האפלקציה שלנו ללא Downtime.

גישה נוספת כמובן לשימוש בקלסטר היא שימוש בקוברנטיס – בשימוש בקוברנטיס אף אחד לא יכול להבטיח לנו שכאשר נבקש תהליך חדש – הוא ירוץ על ליבה נפרדת, בPM2 לעומת זאת – כאשר אנו מבקשים תהליך חדש – הוא ירוץ על ליבה נוספת.

"אירועים" – EventEmitter בNodeJS על קצה המזלג

EventEmitter הוא מודל בילט אין בNodeJS, הוא מאפשר "האזנה" לאירועים ו"שידור" שאירוע קיים קרה לכל ה"מאזינים".

לדוגמה, נוכל להאזין לאירוע בשם "NEW_USER_REGISTER" שישודר בכל פעם שמשתמש חדש נירשם למערכת שלנו, במקביל נוכל "להאזין" לאירוע מכל מקום שנירצה. בכל פעם שהאירוע יתקיים – נוכל להריץ פונקציות בצורה אוטומטית.

const EventEmitter = require('events');
const ourEmitter = new EventEmitter();

//נאזין לאירוע NEW_USER_REGISTER
//בכל פעם שהאירוע ישודר, הפונקציה הנל תרוץ
ourEmitter.on('NEW_USER_REGISTER'), () => {
 console.log('משתמש חדש נירשם למערכת');
}

//ניתן להאזין לאירוע ביותר ממקום אחד
ourEmitter.on('NEW_USER_REGISTER'), () => {
 console.log('שלח אימייל ברוך הבא');
}

//על מנת לשדר אירוע נריץ את הפקודה הבאה
ourEmitter.emit('NEW_USER_REGISTER');

הEventEmitter עוזר כאשר אנו צריכים לעדכן מודלים אחרים שאירוע מסויים קרה והוא שימושי מאוד, חשוב להכיר אותו!

שימו לב, אם האירוע "ישודר" לפני שאנו נאזין לו – מה שמאזין לו לא יידע שהאירוע קרה, לכן – קודם כל צריך להאזין ורק לאחר מכן לשדר.

תבניות אסינכרוניות בNodeJS – על קצה המזלג

תבניות אסינכרוניות בNodeJS,
SyncPattern, Async Error CallBack, The Promise Pattern – Async-Await

Sync Pattern

דומה לאיך שאנו כותבים בPHP למשל – קוד סינכרוני. כל פקודה מתבצעת בזו אחר זו. לדוגמה:

const fs = require('fs');
let fileName = 'logs.txt';
const fileData = fs.readFileSync(fileName);

console.log('File content is', fileData);

console.log('Sync Test');

בקוד הנל, אנו קוראים את כל התוכן של הקובץ, לאחר שקראנו אותו אנו מדפיסים את התוכן שלו. ולאחר מכן מדפיסים 'Sync Test'. בקוד הסינכרוני הנל קודם יודפס תוכן הקובץ ורק לאחר מכן יודפס 'Sync Test'. חשוב להדגיש: הקוד הנל לא עובר דרך הEvent Loop של NodeJS.


Async Error CallBack

בדוגמה הבאה, נכתוב קוד אסינכרוני בNodeJS בגישת CallBack. הקוד הנל יעבור דרך הEvent Loop ויתבצע בצורה אסינכרונית.

const fs = require('fs');

let fileName = 'logs.txt';
fs.readFile(fileName, function cback(err,data) {
 console.log('File content is', data);
});

console.log('Async Test');

בקוד הנל, אנו משתמשים במתודה readFile שהיא מתודה אסינכרונית אשר עושה שימוש בEventLoop.
אנו לא נוכל לגשת לתוכן הקובץ ישירות אחרי קריאה לפונקציה הזאת – כיוון שהוא עדיין לא זמין, וזאת הסיבה שאנו צריכים להשתמש בפונקצית CallBack – במקרה שלנו cback שמקבלת 2 פרמטרים. הפרמטר הראשון err (לשגיאות) והפרמטר השני data – התוכן של הקובץ.

לאחר שקריאת הקובץ תסתיים, מתבצעת קריאה לפונקצית הCallBack שלנו ורק בה יש לנו גישה לתוכן של הקובץ (Event Loop)

  1. קריאת הקובץ
  2. הדפסת Async Test
  3. הדפסת תוכן הקובץ על ידי פונקציית הCallBack
    כאשר למעשה הEvent Loop רץ רק פעמיים, בפעם הראשונה קריאת הקובץ + הדפסת הAsync Test ובפעם השנייה הרצת פונקצית הCallBack שלנו.

    שימו לב, שכיון שמדובר בכתיבה אסינכרונית, שורת הAsync Test שלנו תופיע לפני שורות התוכן של הקובץ שלנו.

חשוב לשים לב שפונקצית הCallBack שלנו תמיד תקבל בפרמטר הראשון את משתנה הerr, אם תחול שגיאה בקוד פרמטר הerr שלנו יהיה אוביקט מסוג error ובמידה ולא תתרחש שגיאה הערך של הפרמטר err יהיה null.

החיסרון העיקרי בשימוש בכתיבה עם callbacks הוא הצורך לשרשר את הפונקציות שלנו, מה שקרוי "עץ אשוח" – דבר הגורם לקוד לא קריא וקשה לתיחזוק.
אם נירצה לדוגמה גם לקרוא מהקובץ וגם לכתוב לקובץ אחר בשימוש בתוכן הקובץ הראשון נקבל קוד בסיגנון הבא:

const fs = require('fs');

let fileName = 'logs.txt';
let newFileName = 'newlog.txt';
fs.readFile(fileName, function cback(err,data) {
 fs.writeFile(newFileName, data, function cback2(err) {
  //yada yada
 });
});

console.log('Async Test');

שימו לב, איך 2 הפונקציות הנל משורשרות, עכשיו דמיינו לכם קוד אסיכרוני כזה שעושה 5 ויותר פעולות..
פיתרון לבעיתיות הזו נמצא בתבנית אסינכרונית אחרת בשם Promise Pattern


The Promise Pattern, Async – Await

Nodejs מגיע עם כלי שמאפשר לעשות "להמיר" לpromise כל פונקציה אסינכרונית שהיא בילט אין.

const fs = require('fs');
const util = require('util');

const readLogFile = util.promisify('fs.readFile');

async function readLogFile(fileName) {
 const logFileData = await readFile(fileName);
 console.log('Log Data is', logFileData);
}

readLogFile('logs.txt');

console.log('Promise Async Test');

בדוגמה זו אנו משתמשים בpromisify כדי להשתמש בפונקציה readFile (שהיא פונקציה אסינכרונית בילט אין) – בצורה שמחזירה Promise. אנו יכולים להמיר כל פונקציה אסינכרונית שכתובה בצורת CallBack לצורה שמחזירה Promise.
הערת אגב: למודל FS יש כבר פונקציות Promise מובנות, להלן:

const readFile = util.promisify(fs.readFile);

בדוגמה הקודמת כמובן לא השתמשנו בהן ועשינו במקום זאת שימוש בutil.promisify.

ראו את הדוגמה הבאה בה אנו גם קוראים מקובץ וגם כותבים לקובץ עם Promise

const fs = require('fs').promises;

let fileName = 'logs.txt';
let newFileName = 'newlog.txt';

async function copyLog(filename, newFileName) {
 await logFileData = await fs.readFile(filename);
 await fs.writeFile(newFileName, logFileData);
}

copyLog('logs.txt','logs-copy.txt');
console.log('Async Test');

נכון שהקוד הרבה יותר ברור מהקוד הבא?

const fs = require('fs');

let fileName = 'logs.txt';
let newFileName = 'newlog.txt';
fs.readFile(fileName, function cback(err,data) {
 fs.writeFile(newFileName, data, function cback2(err) {
  //yada yada
 });
});

console.log('Async Test');


לסיכום: לטעמי האישי, Promises הן הרבה יותר נוחות לתחזוקה וגם לכתיבה מאשר CallBacks.


מצאתם טעות? הערות? שאלות? הסתדרתם? נתקעתם? כתבו לי בתגובות!