✕ סגור 
צור קשר
תודה על ההתעניינות .

Thank you! Your submission has been received!

Oops! Something went wrong while submitting the form

כיצד להיות CONTAINER RUNTIME - חלק ג

דוד יונגמן
|
קלה
|
Aug 22, 2018
alt="facebook"alt="linkedin"
להרשמה לניוזלטר

מאמר זה הוא השלישי והאחרון בסדרה העוסקת בשאלה - איך Container Runtime יוצר קונטיינרים? בשני החלקים הקודמים הצגתי את שלושת מאפייני הקרנל שמשמשים לבניית קונטיינר – Namespaces, Capabilities ו-cgroups. בחלק הזה, שהוא קצת יותר טכני, אציג כיצד מפעילים את התכונות הללו ישירות במערכת ההפעלה, ללא שימוש ב- Container Runtime, בדומה לפעולה של ה- Runtime עצמו.

באיזה Naming Convention מומלץ להשתמש ל- Namespaces?

כפי שציינתי בחלק הראשון של הסדרה, בברירת מחדל תהליך-אב שיוצר תהליך-ילד חולק אתו את ה-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 מצביעים בשני התהליכים לאובייקטים זהים

לעומת זאת, ביצירה של תהליך עם Namespace שונה, המיפוי משתנה:

‍תיקייה של תהליך שנוצר ב-Mount Namespace עצמאי, הלינקים זהים למעט הלינק של mnt שמצביע על Namespace עם מזהה שונה.

ובנוגע ל-Naming Convention, התשובה היא של- Namespaces אין שמות, הם מצוינים באמצעות מספר סידורי שנקבע על ידי המערכת בזמן שהם נוצרים.

איך מודדים Capabilities?

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

/usr/include/linux/capability.h

ומתועד ב-Man Page בשם capabilities. אפשר לבדוק איזה יכולות יש למשתמש או תהליך דרך proc:

‍הערך של היכולות מופיע ב-proc מקודד, הפקודה capsh ממירה את הערך המקודד לרשימה טקסטואלית

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

cgroups

הטכנולוגיה השלישית והאחרונה, 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. המטרה של פלטפורמות אלו, אופן פעולתן ויתרונות השימוש בהן הם הנושאים בהם אעסוק במאמר הבא.

מאת: דוד יונגמן, מייסד-שותף בחברת Wizards

מאמר זה הוא השלישי והאחרון בסדרה העוסקת בשאלה - איך Container Runtime יוצר קונטיינרים? בשני החלקים הקודמים הצגתי את שלושת מאפייני הקרנל שמשמשים לבניית קונטיינר – Namespaces, Capabilities ו-cgroups. בחלק הזה, שהוא קצת יותר טכני, אציג כיצד מפעילים את התכונות הללו ישירות במערכת ההפעלה, ללא שימוש ב- Container Runtime, בדומה לפעולה של ה- Runtime עצמו.

באיזה Naming Convention מומלץ להשתמש ל- Namespaces?

כפי שציינתי בחלק הראשון של הסדרה, בברירת מחדל תהליך-אב שיוצר תהליך-ילד חולק אתו את ה-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 מצביעים בשני התהליכים לאובייקטים זהים

לעומת זאת, ביצירה של תהליך עם Namespace שונה, המיפוי משתנה:

‍תיקייה של תהליך שנוצר ב-Mount Namespace עצמאי, הלינקים זהים למעט הלינק של mnt שמצביע על Namespace עם מזהה שונה.

ובנוגע ל-Naming Convention, התשובה היא של- Namespaces אין שמות, הם מצוינים באמצעות מספר סידורי שנקבע על ידי המערכת בזמן שהם נוצרים.

איך מודדים Capabilities?

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

/usr/include/linux/capability.h

ומתועד ב-Man Page בשם capabilities. אפשר לבדוק איזה יכולות יש למשתמש או תהליך דרך proc:

‍הערך של היכולות מופיע ב-proc מקודד, הפקודה capsh ממירה את הערך המקודד לרשימה טקסטואלית

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

cgroups

הטכנולוגיה השלישית והאחרונה, 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. המטרה של פלטפורמות אלו, אופן פעולתן ויתרונות השימוש בהן הם הנושאים בהם אעסוק במאמר הבא.

מאת: דוד יונגמן, מייסד-שותף בחברת Wizards

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
דוד יונגמן
בואו נעבוד ביחד
צרו קשר