מאמר זה הוא השלישי והאחרון בסדרה העוסקת בשאלה - איך Container Runtime יוצר קונטיינרים? בשני החלקים הקודמים הצגתי את שלושת מאפייני הקרנל שמשמשים לבניית קונטיינר – Namespaces, Capabilities ו-cgroups. בחלק הזה, שהוא קצת יותר טכני, אציג כיצד מפעילים את התכונות הללו ישירות במערכת ההפעלה, ללא שימוש ב- Container Runtime, בדומה לפעולה של ה- Runtime עצמו.
כפי שציינתי בחלק הראשון של הסדרה, בברירת מחדל תהליך-אב שיוצר תהליך-ילד חולק אתו את ה-Namspaces אליהם הוא שייך. כאשר תהליך-אב מעוניין ליצור תהליך שלא יחלוק אתו Namspaces הוא יוצר אותו באמצעות פקודת Unshare. כדוגמה הצגתי הרצת תהליך ב- Mount Namespace נפרד באמצעות הפקודה unshare --mount והראיתי, באמצעות עריכת שינוי של Mount Points, שהיררכית ה-Mount שרואים מתוך ה-Namespace היא היררכיה שונה ונפרדת מההיררכיה שרואים מחוץ ל- Namespace.
אבל איך זה עובד? איך הקרנל יודע לאיזה Namespace התהליך שייך, איזה Mount List להציג לאיזה תהליך ואיך הקרנל מונע מתהליך ב- Namespace אחד להציץ למה שרואה תהליך ב-Namespace אחר?
בלינוקס בדרך כלל אפשר לברר פרטים בנוגע לפעילות של הקרנל באמצעות הממשק של Proc, שמנגיש מידע והגדרות ריצה של הקרנל באמצעות מערכת קבצים מדומה. כל תהליך שרץ במערכת מיוצג ב- Proc בתור תיקייה ששמה הוא מספר ה- Pid של התהליך:
לכל תהליך יש בתיקייה שלו תת תיקייה בשם ns שמציגה את מיפוי ה-Namespaces שאליהם שייך התהליך. בברירת מחדל תהליך שותף ל- Namespaces של התהליך שיצר אותו, כך שבאופן סטנדרטי כל התהליכים במערכת חולקים את ה- Namespace של התהליך הראשון במערכת:
לעומת זאת, ביצירה של תהליך עם Namespace שונה, המיפוי משתנה:
ובנוגע ל-Naming Convention, התשובה היא של- Namespaces אין שמות, הם מצוינים באמצעות מספר סידורי שנקבע על ידי המערכת בזמן שהם נוצרים.
בתחומים רבים מדידת יכולות היא תהליך לא פשוט בכלל, במיוחד כאשר נדרשת רמת דיוק גבוהה, אבל בהקשר של הקרנל, לינוקס והרשאות, בדיקת ה- Capabilities שיש למשתמש או תהליך היא פשוטה וברורה. יש מספר מוגדר של יכולות שמיוצגות כביטים, כלומר היכולות הן בינאריות, למשתמש או תהליך יש את היכולת או שאין לו אותה. מספר היכולות הקיימות במערכת תלוי בגרסת הקרנל, בגרסאות חדשות יותר נוספו יכולות נוספות. פירוט היכולות הקיימות במערכת מופיע בקובץ
/usr/include/linux/capability.h
ומתועד ב-Man Page בשם capabilities. אפשר לבדוק איזה יכולות יש למשתמש או תהליך דרך proc:
בעוד שבדיקת ה-Capabilities של תהליך פשוטה למדי, הבנת המשמעות המדויקת של כל Capability, כלומר מהן הפעולות שהיא מאפשרת לבצע, עשויה להיות מסובכת. מסתבר שגם המפתחים של הקרנל מתקשים (או מתעצלים) לפעמים לאתר את ה-Capability המינימלי הנדרש לביצוע פעולות שונות. מבחינה פרקטית, בהקשר של עבודה מאובטחת עם קונטיינרים, חשוב להבין שהרצת קונטיינר ללא הרשאות root זה אמנם הבסיס המינימלי, אבל הכרחי להבין גם אילו Capabilities יש לקונטיינר ומה המשמעות שלהן. אין משמעות להסרת הרשאת root מהגדרת קונטיינר שרץ עם CAP_SYS_ADMIN, סיכון האבטחה נותר ברמה זהה.
הטכנולוגיה השלישית והאחרונה, cgroups, מאפשרת הקצאה של משאבי מערכת לתהליך או לקבוצת תהליכים. הקצאת המשאבים יכולה לשמש הן להגדרת מגבלה על כמות המשאבים שתהליך יכול לצרוך (quota) והן להבטחה של כמות משאבים מינימלית ששמורה לתהליך (QoS). כיום רוב מערכות לינוקס מבוססות systemd שבעצמה עושה שימוש נרחב ב-cgroups ואף מספקת ממשק API לשימוש בטכנולוגיה. הדיון שלהלן מתייחס למערכת מבוססת systemd ובדוגמאות נעשה שימוש בהיררכיית ה- cgroups הקיימת במערכת.
את ה-cgroup s שמוגדרים במערכת אפשר לראות בהיררכיה שמתחילה ב- /sys/fs/cgroup . בתיקיה זו קיימות תת-תיקיות שמייצגות את משאבי המערכת שמוגדרים ב- cgroup (זיכרון, מעבד, דיסקים, רשת וכו'). מתחת לכל תת-תיקיה כזו מופיעים קבצים מיוחדים שמייצגים את הפרמטרים השונים הניתנים להגדרה וכן תת-תיקיות שמייצגות את ה-cgroups הקיימים. הקבצים שבתיקייה של כל cgroup מאפשרים לראות את הערכים שמוגדרים לפרמטרים השונים של ה-cgroup ולערוך אותם. מעבר ל-cgroups הסטנדרטים ש- systemd יוצרת, אפשר להגדיר cgroups נוספים ולשייך אליהם תהליכים באופן עצמאי.
להדגמת השימוש ב-cgroups נשתמש ב-stress, כלי להעמסת משאבי מערכת. לאחר שנתקין את הכלי, נפתח שני terminal sessions במקביל. באחד נריץ top, כך שנוכל לבחון את העומס שהכלי מייצר ובשני נעמיס באמצעות הכלי את הזיכרון והמעבד של המחשב לעשרים שניות:
במכונה הווירטואלית שעליה ערכתי בדיקה, הרצת הפקודה עם הפרמטרים האלו ניצלה את כל הזיכרון הפנוי ואת כל משאבי המעבד. במכונות עם יותר זיכרון אפשר להעלות את הערך של הפרמטר vm לערך המקסימלי שמאפשר הרצה ללא שגיאות.
עכשיו נגדיר cgroups לזיכרון ולמעבד, נגביל את המשאבים הזמינים לתהליכים המשויכים ל- cgroups ונשייך אליהן את התהליך של ה-shell ממנו אנחנו מריצים את ה-stress. בתור root נריץ:
נריץ top, נעבור ל-terminal השני ונריץ מתוכו שוב את פקודת ה-stress עם אותם הפרמטרים. כצפוי, המשאבים שה-stress יכול לצרוך מוגבלים ומשאבי הזיכרון והמעבד של המכונה נותרים פנויים לשימושם של תהליכים שאינם משויכים ל-cgroups עליו הגדרנו את המגבלות.
כן, אלו הרכיבים שמאפשרים ל-Runtime להריץ תהליכים באופן מבודד. אבל זה ממש לא כל הסיפור, כי השימושיות של קונטיינרים לא מסתיימת בהרצה מבודדת של תהליכים. קונטיינרים מהווים אבסטרקציה שימושית, רוב האנשים שמשתמשים בקונטיינרים לא יודעים מה בדיוק קורה כשהם מריצים docker run, זה לא מעניין אותם ולא נדרש מהם על מנת להפיק מהם תועלת. מעבר לכך, קונטיינרים משמשים לאריזה של תוכנה עם קונפיגורציה מוכנה לשימוש, כך שקונטיינרים משמשים לעיתים קרובות כמעין אלטרנטיבה משופרת ל-Package Manager. הסטנדרטיזציה של האימגי'ם והמנגנון הנוח להפצה וצריכה שלהם הובילו להתפתחות של ecosystem שלם. אבל החלק המשמעותי ביותר בכל הסיפור ומה שמוביל למהפך של ממש בתעשייה הוא האפשרות ליישם שירותים מבוססי קונטיינרים ביעילות באמצעות פלטפורמות אורקסטרציה כמו Kubernetes. המטרה של פלטפורמות אלו, אופן פעולתן ויתרונות השימוש בהן הם הנושאים בהם אעסוק במאמר הבא.
מאמר זה הוא השלישי והאחרון בסדרה העוסקת בשאלה - איך Container Runtime יוצר קונטיינרים? בשני החלקים הקודמים הצגתי את שלושת מאפייני הקרנל שמשמשים לבניית קונטיינר – Namespaces, Capabilities ו-cgroups. בחלק הזה, שהוא קצת יותר טכני, אציג כיצד מפעילים את התכונות הללו ישירות במערכת ההפעלה, ללא שימוש ב- Container Runtime, בדומה לפעולה של ה- Runtime עצמו.
כפי שציינתי בחלק הראשון של הסדרה, בברירת מחדל תהליך-אב שיוצר תהליך-ילד חולק אתו את ה-Namspaces אליהם הוא שייך. כאשר תהליך-אב מעוניין ליצור תהליך שלא יחלוק אתו Namspaces הוא יוצר אותו באמצעות פקודת Unshare. כדוגמה הצגתי הרצת תהליך ב- Mount Namespace נפרד באמצעות הפקודה unshare --mount והראיתי, באמצעות עריכת שינוי של Mount Points, שהיררכית ה-Mount שרואים מתוך ה-Namespace היא היררכיה שונה ונפרדת מההיררכיה שרואים מחוץ ל- Namespace.
אבל איך זה עובד? איך הקרנל יודע לאיזה Namespace התהליך שייך, איזה Mount List להציג לאיזה תהליך ואיך הקרנל מונע מתהליך ב- Namespace אחד להציץ למה שרואה תהליך ב-Namespace אחר?
בלינוקס בדרך כלל אפשר לברר פרטים בנוגע לפעילות של הקרנל באמצעות הממשק של Proc, שמנגיש מידע והגדרות ריצה של הקרנל באמצעות מערכת קבצים מדומה. כל תהליך שרץ במערכת מיוצג ב- Proc בתור תיקייה ששמה הוא מספר ה- Pid של התהליך:
לכל תהליך יש בתיקייה שלו תת תיקייה בשם ns שמציגה את מיפוי ה-Namespaces שאליהם שייך התהליך. בברירת מחדל תהליך שותף ל- Namespaces של התהליך שיצר אותו, כך שבאופן סטנדרטי כל התהליכים במערכת חולקים את ה- Namespace של התהליך הראשון במערכת:
לעומת זאת, ביצירה של תהליך עם Namespace שונה, המיפוי משתנה:
ובנוגע ל-Naming Convention, התשובה היא של- Namespaces אין שמות, הם מצוינים באמצעות מספר סידורי שנקבע על ידי המערכת בזמן שהם נוצרים.
בתחומים רבים מדידת יכולות היא תהליך לא פשוט בכלל, במיוחד כאשר נדרשת רמת דיוק גבוהה, אבל בהקשר של הקרנל, לינוקס והרשאות, בדיקת ה- Capabilities שיש למשתמש או תהליך היא פשוטה וברורה. יש מספר מוגדר של יכולות שמיוצגות כביטים, כלומר היכולות הן בינאריות, למשתמש או תהליך יש את היכולת או שאין לו אותה. מספר היכולות הקיימות במערכת תלוי בגרסת הקרנל, בגרסאות חדשות יותר נוספו יכולות נוספות. פירוט היכולות הקיימות במערכת מופיע בקובץ
/usr/include/linux/capability.h
ומתועד ב-Man Page בשם capabilities. אפשר לבדוק איזה יכולות יש למשתמש או תהליך דרך proc:
בעוד שבדיקת ה-Capabilities של תהליך פשוטה למדי, הבנת המשמעות המדויקת של כל Capability, כלומר מהן הפעולות שהיא מאפשרת לבצע, עשויה להיות מסובכת. מסתבר שגם המפתחים של הקרנל מתקשים (או מתעצלים) לפעמים לאתר את ה-Capability המינימלי הנדרש לביצוע פעולות שונות. מבחינה פרקטית, בהקשר של עבודה מאובטחת עם קונטיינרים, חשוב להבין שהרצת קונטיינר ללא הרשאות root זה אמנם הבסיס המינימלי, אבל הכרחי להבין גם אילו Capabilities יש לקונטיינר ומה המשמעות שלהן. אין משמעות להסרת הרשאת root מהגדרת קונטיינר שרץ עם CAP_SYS_ADMIN, סיכון האבטחה נותר ברמה זהה.
הטכנולוגיה השלישית והאחרונה, cgroups, מאפשרת הקצאה של משאבי מערכת לתהליך או לקבוצת תהליכים. הקצאת המשאבים יכולה לשמש הן להגדרת מגבלה על כמות המשאבים שתהליך יכול לצרוך (quota) והן להבטחה של כמות משאבים מינימלית ששמורה לתהליך (QoS). כיום רוב מערכות לינוקס מבוססות systemd שבעצמה עושה שימוש נרחב ב-cgroups ואף מספקת ממשק API לשימוש בטכנולוגיה. הדיון שלהלן מתייחס למערכת מבוססת systemd ובדוגמאות נעשה שימוש בהיררכיית ה- cgroups הקיימת במערכת.
את ה-cgroup s שמוגדרים במערכת אפשר לראות בהיררכיה שמתחילה ב- /sys/fs/cgroup . בתיקיה זו קיימות תת-תיקיות שמייצגות את משאבי המערכת שמוגדרים ב- cgroup (זיכרון, מעבד, דיסקים, רשת וכו'). מתחת לכל תת-תיקיה כזו מופיעים קבצים מיוחדים שמייצגים את הפרמטרים השונים הניתנים להגדרה וכן תת-תיקיות שמייצגות את ה-cgroups הקיימים. הקבצים שבתיקייה של כל cgroup מאפשרים לראות את הערכים שמוגדרים לפרמטרים השונים של ה-cgroup ולערוך אותם. מעבר ל-cgroups הסטנדרטים ש- systemd יוצרת, אפשר להגדיר cgroups נוספים ולשייך אליהם תהליכים באופן עצמאי.
להדגמת השימוש ב-cgroups נשתמש ב-stress, כלי להעמסת משאבי מערכת. לאחר שנתקין את הכלי, נפתח שני terminal sessions במקביל. באחד נריץ top, כך שנוכל לבחון את העומס שהכלי מייצר ובשני נעמיס באמצעות הכלי את הזיכרון והמעבד של המחשב לעשרים שניות:
במכונה הווירטואלית שעליה ערכתי בדיקה, הרצת הפקודה עם הפרמטרים האלו ניצלה את כל הזיכרון הפנוי ואת כל משאבי המעבד. במכונות עם יותר זיכרון אפשר להעלות את הערך של הפרמטר vm לערך המקסימלי שמאפשר הרצה ללא שגיאות.
עכשיו נגדיר cgroups לזיכרון ולמעבד, נגביל את המשאבים הזמינים לתהליכים המשויכים ל- cgroups ונשייך אליהן את התהליך של ה-shell ממנו אנחנו מריצים את ה-stress. בתור root נריץ:
נריץ top, נעבור ל-terminal השני ונריץ מתוכו שוב את פקודת ה-stress עם אותם הפרמטרים. כצפוי, המשאבים שה-stress יכול לצרוך מוגבלים ומשאבי הזיכרון והמעבד של המכונה נותרים פנויים לשימושם של תהליכים שאינם משויכים ל-cgroups עליו הגדרנו את המגבלות.
כן, אלו הרכיבים שמאפשרים ל-Runtime להריץ תהליכים באופן מבודד. אבל זה ממש לא כל הסיפור, כי השימושיות של קונטיינרים לא מסתיימת בהרצה מבודדת של תהליכים. קונטיינרים מהווים אבסטרקציה שימושית, רוב האנשים שמשתמשים בקונטיינרים לא יודעים מה בדיוק קורה כשהם מריצים docker run, זה לא מעניין אותם ולא נדרש מהם על מנת להפיק מהם תועלת. מעבר לכך, קונטיינרים משמשים לאריזה של תוכנה עם קונפיגורציה מוכנה לשימוש, כך שקונטיינרים משמשים לעיתים קרובות כמעין אלטרנטיבה משופרת ל-Package Manager. הסטנדרטיזציה של האימגי'ם והמנגנון הנוח להפצה וצריכה שלהם הובילו להתפתחות של ecosystem שלם. אבל החלק המשמעותי ביותר בכל הסיפור ומה שמוביל למהפך של ממש בתעשייה הוא האפשרות ליישם שירותים מבוססי קונטיינרים ביעילות באמצעות פלטפורמות אורקסטרציה כמו Kubernetes. המטרה של פלטפורמות אלו, אופן פעולתן ויתרונות השימוש בהן הם הנושאים בהם אעסוק במאמר הבא.
הודעתך לא התקבלה - נסה שוב מאוחר יותר
Oops! Something went wrong while submitting the form