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

שני מונים, שתי שאלות

LimitNPROC הוא RLIMIT_NPROC תחת שם systemd, ויחידת החשבונאות שלו היא מזה המשתמש האמיתי. כל תהליך שהמשתמש הזה מחזיק בו נספר לאותו סכום, כולל עבודה שהתחילו יחידות אחרות, הפעלת התחברות או משימת מתזמן. המגבלה לכן אינה תקרה לכל שירות בנפרד גם כשהיא מוגדרת בתוך יחידת שירות, ושירות שמסתעף עוזר לכל לקוח מתחרה בכל דבר אחר שרץ תחת אותו חשבון. משימת cron עמוסה תחת אותו משתמש יכולה לדחוף יחידת VPN מעבר לקו שהיא עצמה מעולם לא חצתה.

TasksMax יש לו בעלים אחר: בקר ה-pids על ה-cgroup של היחידה, שסופר משימות ולא תהליכים. חוטים הם משימות, ולכן תהליך רקע שנראה חד-חוטי אך יש לו מאגר עובדים יכול לצרוך תקציב שנראה במבט ראשון גדול בהרבה ממספר התהליכים שלו. EffectiveTasksMax מדווח מה ההיררכיה של cgroup באמת מתירה לאחר שהפרוסות האבות אמרו את שלהן.

למה חריגת ה-root משנה את התשובה

הליבה אינה מחילה את RLIMIT_NPROC על תהליך שנושא CAP_SYS_RESOURCE או CAP_SYS_ADMIN, ולכן יחידה שרצה כ-root בלי לוותר על הרשאות אינה מוגבלת למעשה על ידי אותה הגדרה. החריג היחיד הזה מסביר את העצות הסותרות שאנשים מחליפים: העלאת LimitNPROC מתקנת פריסה שוויתרה על הרשאות ואינה עושה דבר וחצי דבר לזו שנשארה מורשית. בדקו את User= ואם תהליך הרקע מחליף זהות לאחר ההפעלה לפני שתייחסו לאותה הגדרה זכות או אשמה. יחידה מורשית שעוברת לאחר כך לחשבון לא מורשה נופלת חזרה מתחת למגבלה ברגע המעבר, מה שהופך גם את ציר הזמן של הכשל לראוי לקריאה.

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

קראו את הערכים האפקטיביים, לא את אלה של ה-shell

ulimit -u של shell אינטראקטיבי מתאר את אותו shell. הוא אינו מתאר מה מנהל השירותים העביר לתהליך הרקע, ואינו אומר דבר על חשבונאות cgroup. שאלו במקום זאת את היחידה, בשם המופע שקיים במארח ולא בדוגמה מתוך מדריך.

systemctl show [email protected] -p User -p LimitNPROC -p TasksCurrent -p TasksMax -p EffectiveTasksMax
cat /proc/$(systemctl show -p MainPID --value [email protected])/limits | grep -i processes

בתוך מכולה התשובה דורשת עוד שתי קריאות, כי ערך פנימי של infinity מתאר רק את השקפת האורח. בקשו מבעל המארח את תקרת ה-pids של המכולה עצמה, ואמתו האם מנהל השירותים הפנימי יכול להאציל את הבקר הזה בכלל. יחידה מקוננת אינה יכולה להחזיק תקציב שה-cgroup ההורה שלה מעולם לא העניק.

מדדו את היקף התיקון והוכיחו אותו תחת עומס

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

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

sudo systemctl daemon-reload
sudo systemctl restart [email protected]
systemctl show [email protected] -p TasksCurrent -p EffectiveTasksMax
sudo journalctl -u [email protected] -b --no-pager | tail -n 20

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

מאמרים קשורים