הרצת שרת Node.js על כמה ליבות

שרת Node.js שעובד על כמה ליבות יכול ממש לעשות סקיילינג נאה ממש. אבל איך עושים את זה? עם pm2!

אחד הדברים הכי חשובים לדעת על Node.js שבעצם סקריפט Node.js רץ, באופן טבעי, על ליבה אחת של CPU. למה זה חשוב? כי אם יש לי סקריפט אחד של Node.js שעושה משהו על השרת ומג'עג'ע – גם אם אני ארחיב את מספר הליבות על השרת זה לא ממש יעזור. כי סקריפט אחד רץ על ליבה אחת.

אני יכול באופן עקרוני להשתמש במודול הטבעי cluster כדי לפתוח פרוססים בליבות נוספות מסקריפט אחד. למרות שזה די קל, לא רואים את זה יותר מדי כי מתכנתי Node.js, ומתכנתים בלבד, נוטים להשאיר את החלק הזה לאנשי Devops או אנשי IT – שידאגו לסקיילינג. וחבל מאוד – הבנה טובה בקוד היא גם הבנה טובה בסביבה שבה הקוד הזה רץ. ולמרות הפיתוי הגדול להשאיר נושאים כאלו לאנשי הפרודקשן, חייבים קצת להסתכל על איפה שהקוד שלנו רץ 🙂 במיוחד כשזה לא קשה.

אז על cluster אני אכתוב במאמר אחר, אבל לא חייבים cluster כדי לעבוד ובקלות על כמה ליבות בשרתי ווב מבוססי Node.js. בגדול שרתי Node.js הם נהדרים ויכולים לעמוד בעומסים גבוהים – אבל הם גם יכולים להחנק. אם יש I\O רציני או שיש חישוביות CPU גבוהה, שרת HTTP של Node.js יכול להגיע מהר מאוד ל-100% CPU.

אדגים את זה באמצעות מודול פשוט שמוצא מספר ראשוני. משהו באמת פשוט. הנה מודול שעושה את זה באופן הכי נאיבי שיש:

// findPrimeNumber.js
const findPrimeNumber = (n) => {
    switch(n) {
        case 1:
            return false;
        case 2:
            return n;
        default: 
        for(let x = 2; x < n; x++){
            if(n % x === 0){
              return false;
            }
        }
        return n;
    }
}

module.exports = findPrimeNumber;

מציאת מספרים ראשוניים זה דבר שבהחלט יכול לקחת משאבים, אם אני מצרף את זה לשרת HTTP אז זה יכול להיות די אסוני. במיוחד אם אני בכלל לא משקיע ב-caching. הנה שרת HTTP קלאסי שמגיש לי את כל המספרים הראשוניים מ-0 ועד 100,000

// server.js
const http = require('http');
const findPrimeNumber = require('./
'); // From previous file

http.createServer((req, res) => {
    let result = '';
    for (let i = 0; i < 100000 ; i++) {
        const primeNumber = findPrimeNumber(i).toString();
        if(primeNumber !== 'false') {
            result += `\n${primeNumber}`;
        }
    }
    res.writeHead(200);
    res.end(result);
}).listen(3000);

אם אני אריץ את זה, כל בקשה תיקח חמש שניות בערך. שזה בסדר (נניח). אבל בחמשת השניות האלו הוא יביא את ה-cpu ל-100 אחוז. ומה קורה אם יש בקשות נוספות? ובכן, אתם יכולים לדמיין לעצמכם מה קורה.

אבל למה לדמיין? בוא ונראה! אני אקח רספברי פיי, שאני משתמש בו לבדיקות ואבדוק איך אתר כזה מתנהג. אתקין עליו מערכת הפעלה RaspberryOS. היא 32 ביט אבל תספיק לבדיקה. לרספברי פיי 4 יש ארבע ליבות והוא בהחלט חזק מספיק להריץ שרת כזה. לא פחות משרתים שיתופיים. על הרספברי פיי אני אתקין nvm ואפעיל עליו את Node.js latest. גרסה 16 בשעת כתיבת הפוסט הזה. אכניס לתוכו את הסקריפטים האלו ואריץ אותם.

אם אני אריץ את השרת באמצעות node server.js ואבדוק באמצעות כניסה ממחשב אחר אל http://raspberrypi.local:3000 – אחרי כשלוש שניות, האתר ייטען:

כאמור לא להיט גדול אבל בהחלט יכול "לעבור", נניח באתר ממשלתי. הייתי יכול לייעל את זה ממש, אבל כאמור – היום אנחנו מדגימים. אז בואו ונגיע לסקייל, שהוא מה שמעניין אותנו. מה קורה אם לא אדם אחד נכנס לאתר הזה אלא 100 איש בבאצ'ים של 10 בו זמנית?

יש לי הרבה ילדים בבית, אבל להעמיד אותם מול המחשבים שלהם ולצרוח עליהם לעשות F5 קצת פחות מתאים והם גם יסרבו לשתף פעולה/יעשו לי שנינגנס כי הם טרולים כמו אבא שלהם. אז מה עושים? יש כלי פשוט אך אפקטיבי למדידות כאלו. איזה? apache benchmark. כלי ותיק ומוכר שכתבתי עליו בעבר ואני משתמש כבר שנים. איתו אני יכול לבדוק ביצועים של אתרי אינטרנט. ההתקנה וההפעלה שלו על מחשבי מק או לינוקס זה הדבר הכי פשוט בעולם. ההפעלה שלו היא דרך CLI:

ab -n 100 -c 10 http://raspberrypi.local:3000/

כאן אני קורא מאה פעמים לאתר הזה כאשר יש 10 קריאות בו זמנית. זה עומס גולשים ממש רגוע ועדין אבל אפשר לראות שמן הסתם הכל מתחיל לג'עג'ע מייד:

הנתון המעניין מבחינתנו כאן הוא הנתון החציוני: 19 שניות של טעינה חציונית (!!!) שזה באמת רע. זמן הבדיקה הכולל: 214.980 שניות.

איך אנחנו יכולים לשפר את זה? אם אנחנו מנהלים במשרד הבריאות למשל, אנחנו נגיד שזו תוצאה מצוינת ואנו תוהים למה העיתונאים הרעים לא מפרגנים למנהלי המחשוב של משרד הבריאות שעושים ימים כלילות (כן, הטעות בכוונה פה). אחרים יגידו פשוט להריץ את זה על מחשב חזק יותר מהרספברי פיי המסכן שלי.

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

הרספברי פיי הוא לא מכונה חזקה במיוחד, אבל יש לה ארבע ליבות! שלא עובדות קשה מדי במקרה הזה. בעוד שליבה אחת עובדת קשה כמו עורך דין של זמר לאומי, ליבות אחרות בקושי זזות.

אם אני אצליח לרתום את הליבות האחרות, האם יהיה פה איזה שינוי? בואו ונבדוק.

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

npm install pm2 -g

ומריצים אותה כך:

pm2 start app.js

בהנחה ש-app.js זה הסקריפט. עכשיו בעצם הסקריפט נשאר במכונה גם אם הוא יקרוס, הוא יעלה שוב. אפשר לבחון את מה שרץ באמצעות pm ls. יש ל-pm דוקומנטציה ממש טובה שמסבירה עליו ואם אתם לא מכירים, אז כדאי לבדוק.

עוד תכונה ממש ממש חשובה ל-pm2 היא האפשרות להריץ שרתי ווב על כמה ליבות בלי מאמץ. כמה בלי מאמץ? שימוש ראשוני הוא עניין של דקה. מאחורי הקלעים יש שימוש ב-cluster אבל למשתמש הקצה זה עניין פשוט. אנו יוצרים קובץ JSON בשם app.json ומכניסים אליו את הנתונים הבאים:

{
    "apps" : [{
      "script"    : "server.js",
      "instances" : "max",
      "exec_mode" : "cluster" 
    }]
  }

ה-script הוא מה שמריצים. במקרה שלי זה server.js. ה-instances הוא מספר ההרצות. שמתי את זה על max כי אין לי דברים אחרים שרצים על המכונה וה-exec_mode הוא על קלאסטר. אנו מריצים את זה כך:

pm2 start app.json

וזו תהיה התוצאה:

אני יכול לנטר את pm2 בלי בעיה עם פקודת pm2 monit (אין קשר למונית, זה קיצור של מוניטור אבל אפשר להקשיב לשיר הזה תוך כדי).

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

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

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

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

פוסטים נוספים שכדאי לקרוא

מיקרו בקרים

הפעלת מסך oled קטנטן עם esp32

יצירת מערכת מולטימדיה שלמה בגודל של בול דואר עם מיקרובקר ובעלות של דולרים בודדים. וגם על הדרך נלמד על ת׳רדים

פייתון

קבצי קונפיגורציה בפואטרי

הגדרות שמשנות את ההתנהגות של פואטרי באמצעות קבצי הגדרות גלובליים, לוקליים ואפילו משתני סביבה.

גלילה לראש העמוד