אם האתר שלכם נראה מהיר כשאין מבקרים, אבל זוחל ברגע שהתנועה עולה, הבעיה כמעט אף פעם לא בקוד שלכם. היא בדרך כלל ב-PHP-FPM. ובאופן ספציפי, באופן שבו ה-pool שלו מוגדר לטפל בבקשות במקביל. אם זה מוגדר לא נכון, תראו את זמן התגובה של השרת מטפס גם אם ה-CPU והזיכרון נראים תקינים על הנייר.
בואו נפרק מה PHP-FPM בעצם עושה, למה הגדרות ה-pool שלו כל כך חשובות, ואיך לכוון אותן כדי לצמצם את זמן התגובה של השרת תחת תנועה אמיתית.
מה PHP-FPM בעצם עושה
PHP-FPM (FastCGI Process Manager) היא השכבה שיוצרת ומנהלת תהליכי worker של PHP. בכל פעם שבקשה צריכה להריץ PHP, למשל טעינת עמוד ב-WordPress או קריאה ל-API, היא מועברת לאחד מתהליכי ה-worker האלה. אם יש worker פנוי, הבקשה מטופלת מיד. אם לא, היא נכנסת לתור.
התור הזה הוא המקום שבו זמני התגובה מתפרקים בשקט. בקשה שאמורה לקחת 80 אלפיות שנייה יכולה להסתיים בהמתנה של 2-3 שניות רק כדי ש-worker יתפנה, במיוחד בזמן עלייה בתנועה.
ההגדרות המרכזיות ששולטות בקיבולת
בקובץ ההגדרות של ה-pool שלכם (בדרך כלל ב-/etc/php/8.x/fpm/pool.d/www.conf) יש כמה הגדרות שקובעות כמה בקשות אפשר לשרת במקביל:
- pm - מצב ניהול התהליכים: static, dynamic, או ondemand
- pm.max_children - התקרה הקשיחה למספר תהליכי PHP במקביל
- pm.start_servers - כמה workers עולים בהפעלה הראשונית
- pm.min_spare_servers / pm.max_spare_servers - טווח ה-workers הפנויים במצב dynamic
- pm.max_requests - כמה בקשות worker מטפל בהן לפני שהוא ממוחזר (עוזר עם דליפות זיכרון)
Static מול Dynamic מול Ondemand
מצב static שומר על מספר קבוע של workers פעילים כל הזמן. הוא צפוי ונמנע מהעלות של יצירת תהליכים חדשים באמצע בקשה, מה שהופך אותו לבחירה הטובה יותר בשרתים עם תנועה כבדה ועקבית.
מצב dynamic מגדיל ומקטין את מספר ה-workers בין ההגדרות המינימליות למקסימליות שלכם. הוא חסכוני יותר בזיכרון עבור אתרים עם תנועה משתנה, אבל יש לכך מחיר קטן: יצירת worker חדש לוקחת זמן, ואם הביקוש עולה מהר יותר מהיכולת של FPM ליצור workers, בקשות נכנסות לתור בכל מקרה.
מצב ondemand יוצר workers רק כשמגיעות בקשות וממית אותם אחרי זמן חוסר פעילות. הוא חוסך זיכרון באתרים עם תנועה נמוכה, אבל זו האפשרות הגרועה ביותר אם אכפת לכם מהפחתת זמן תגובת שרת עד למילישניות, מכיוון שכל תקופת חוסר פעילות אומרת שהבקשה הבאה משלמת "מחיר התחלה קרה".
למה max_children היא ההגדרה שהכי הרבה אנשים טועים בה
המספר הבודד הזה הוא ההבדל בין שרת שמתמודד עם עלייה בתנועה בחן לבין שרת שקורס. אם תקבעו אותו נמוך מדי, בקשות ייכנסו לתור למרות שלשרת יש עדיין הרבה RAM ו-CPU פנויים. אם תקבעו אותו גבוה מדי, אתם מסתכנים בכך שהשרת ייגמר לו הזיכרון, מה שמפעיל את ה-OOM killer ומפיל את כל PHP-FPM.
החישוב פשוט:
pm.max_children = (זיכרון RAM זמין ל-PHP) / (זיכרון ממוצע לתהליך PHP)
לדוגמה, אם הקצתם 2GB זיכרון ל-PHP וכל תהליך צורך בממוצע 40MB, תוכלו להריץ בבטחה בערך 50 תהליכים. בדקו את צריכת הזיכרון בפועל עם:
ps -ylC php-fpm8.1 --sort:rss
הריצו את זה תחת עומס אמיתי, לא מיד אחרי הפעלה מחדש, מכיוון שצריכת הזיכרון גדלה ככל שה-workers מטפלים בעמודים ותוספים שונים.
איך תשישות ה-pool באה לידי ביטוי בזמן התגובה של השרת
כשכל ה-workers עסוקים, בקשות חדשות נכנסות לתור המתנה שמוגדר ב-listen.backlog. מבחוץ, זה נראה כאילו השרת שלכם סתם נהיה איטי. המערכת שלכם לניטור עשויה להראות שימוש תקין ב-CPU, זמני שאילתות תקינים במסד הנתונים, ועדיין קפיצה בזמן עד לבייט הראשון. זה הסימן המובהק: הבקשות לא מעובדות לאט יותר, הן פשוט ממתינות יותר זמן כדי להתחיל.
אפשר לאשר את זה על ידי הפעלת עמוד הסטטוס של FPM:
pm.status_path = /status
בדיקה שלו בזמן עלייה בתנועה מראה לכם את המונים listen queue ו-max children reached. אם המספרים האלה מטפסים, ה-pool שלכם קטן מדי לתנועה שאתם מקבלים.
צעדים מעשיים לכוונון
- מדדו את צריכת הזיכרון הממוצעת של תהליך PHP תחת תנועה אמיתית, לא בבדיקות מלאכותיות
- קבעו את max_children בהתאם ל-RAM הזמין, והשאירו מקום ל-MySQL, Redis ולמערכת ההפעלה
- השתמשו במצב static אם התנועה שלכם יציבה וצפויה
- קבעו את pm.max_requests ל-500-1000 כדי למחזר workers ולמנוע הצטברות זיכרון
- עקבו אחרי עמוד הסטטוס באופן קבוע, לא רק כשמשהו נשבר
זה בדיוק סוג הכוונון שנהנה מניטור ברמת השרת ולא מניחושים. אנחנו עוקבים אחרי מדדי PHP-FPM כחלק מהבריאות הכללית של השרת, כך שגודל ה-pool מותאם לפני שהוא הופך להאטה גלויה ולא אחריה. אם אתם מריצים שרת משלכם, כדאי להשקיע בהקמת ניטור זמינות וביצועים שעוקב אחרי התנהגות מסוג זה של תורים.
איפה caching נכנס לתמונה
כוונון ה-pool עוזר רק עד גבול מסוים אם כל בקשה בפועל פוגעת ב-PHP. צמצום מספר הבקשות שבכלל זקוקות ל-worker של PHP חשוב בדיוק כמו קביעת גודל נכון ל-pool. שכבת caching של עמודים ואובייקטים טובה אומרת שהרבה פחות בקשות מתחרות על אותם workers מלכתחילה, וזה לרוב שיפור גדול יותר מכל כוונון של ה-pool. התעמקנו יותר בסוג הזה של צוואר בקבוק הקשור למסד הנתונים במאמר למה שאילתות איטיות במסד הנתונים הן צוואר הבקבוק הנסתר ברוב אפליקציות הווב, מכיוון ששאילתה איטית תופסת worker של PHP ביעילות ממש כמו pool קטן מדי.
לסיכום
הגדרת ה-pool של PHP-FPM היא אחת מההגדרות שבלתי נראות עד שתנועה אמיתית בודקת אותה. pool שגודלו לא מתאים אומר שזמן התגובה של השרת שלכם נראה תקין בשעות שקטות ומתפרק ברגע שמשתמשים אמיתיים מגיעים. מדדו את צריכת הזיכרון בפועל, קבעו את max_children בהתאם, ועקבו אחרי עמוד הסטטוס תחת תנועה אמיתית, לא רק אחרי שמגיעה תלונה. למידע נוסף על אבחון האטות מסוג זה ברמת השרת, ראו למה זמן ה-Time to First Byte שלכם עולה לכם בהמרות ואיך לתקן זאת.