שכבות ב Docker Images

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

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

1. אימג' מורכב ממספר שכבות

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

$ docker pull python

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

$ docker history python

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
59a8c21b72d4        3 weeks ago         /bin/sh -c #(nop)  CMD ["python3"]              0B                  
<missing>           3 weeks ago         /bin/sh -c set -ex;   wget -O get-pip.py 'ht…   6.04MB              
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV PYTHON_PIP_VERSION=19…   0B                  
<missing>           3 weeks ago         /bin/sh -c cd /usr/local/bin  && ln -s idle3…   32B                 
<missing>           3 weeks ago         /bin/sh -c set -ex   && wget -O python.tar.x…   70.4MB              
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV PYTHON_VERSION=3.7.3     0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV GPG_KEY=0D96DF4D4110E…   0B                  
<missing>           3 weeks ago         /bin/sh -c apt-get update && apt-get install…   17MB                
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV LANG=C.UTF-8             0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV PATH=/usr/local/bin:/…   0B                  
<missing>           3 weeks ago         /bin/sh -c set -ex;  apt-get update;  apt-ge…   562MB               
<missing>           3 weeks ago         /bin/sh -c apt-get update && apt-get install…   142MB               
<missing>           3 weeks ago         /bin/sh -c set -ex;  if ! command -v gpg > /…   7.81MB              
<missing>           3 weeks ago         /bin/sh -c apt-get update && apt-get install…   23.2MB              
<missing>           3 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B                  
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:843b8a2a9df1a0730…   101MB               

מה שאנחנו רואים הוא רשימת הפקודות מה Dockerfile שיצרו שכבות, ואת הגודל של כל שכבה. המילה missing מסמנת לנו שמדובר בשכבה שהיא חלק מאימג' אחר, ואין אימג' שמחובר ישירות רק אליה. האימג' python שמסומן עם המזהה 59a8c21b72d4 מכיל את כל השכבות ברשימה.

שיכבה ב Docker מיוצגת על ידי HASH שמחושב על המידע שבתוך השיכבה. אנחנו יכולים לראות את ה Hash-ים האלה עם הפקודה inspect:

$ docker inspect python
       "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:fbb641a8b94349e89886f65d79928e4673530e2a2b4d33c2c95e0426713f78e4",
                "sha256:604829a174eb966a2102e2e68c7669e1fe56721e8d7ea27f9a286aa33be8be20",
                "sha256:12cb127eee44270330891b1b610ce34e81f53a91a22e3a7f53f0632391d99892",
                "sha256:b17cc31e431beb2f39988dff23d04f85ba4b446fc0a13f304774852fa3c87d85",
                "sha256:0fe19df8b8f8eb5f545f50e11f958bf37d27ab7da5c260a9bd2bc0ff6fb760a4",
                "sha256:9f72e27f9aa489ec42f79fd3adeedc7c9dab6a1de6ccec6227ffc7fac50f466a",
                "sha256:5fb94d47198016a70d7e8328d6f6154bf9134a7a6708ab0414e483ada5efac41",
                "sha256:12a02885dd033997c190c079446b5e21305883ade908ff1ea356a441598a41bf",
                "sha256:7b5b713a60afc2d797f0dd3f2039150d4893ccf9f7b24a0fdea33f1f73f50ca5"
            ]
        },

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

2. קונטיינר מוסיף שכבת כתיבה על אימג'

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

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

3. דוגמא: יצירת שכבות באמצעות Dockerfile

הפקודות: FROM, COPY, RUN ו ADD יוצרות כל אחת שכבה חדשה. נתחיל את הדוגמאות עם ה Dockerfile הבא:

FROM ubuntu:18.04

RUN ls
RUN ls

בקובץ יש 3 פקודות שיוצרות שכבות ולכן הוא יוצר 3 שכבות. נבנה ונראה אותן בפעולה:

$ docker build . -t layers01
$ docker history layers01

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
6876e790b0a8        13 seconds ago      /bin/sh -c ls                                   0B                  
2b962f1a6061        14 seconds ago      /bin/sh -c ls                                   0B                  
94e814e2efa8        5 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           5 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B                  
<missing>           5 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B                  
<missing>           5 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B                
<missing>           5 weeks ago         /bin/sh -c #(nop) ADD file:1d7cb45c4e196a6a8…   88.9MB              

בהרצת הבניה דוקר בנה שלוש שכבות, ועבור כל שכבת ביניים שנבנתה הוא יצר גם אימג' שכולל רק את השכבה הזאת. דוקר ישתמש באימג'ים 2b962f1a6061 ו-94e814e2efa8 כשנרצה לבנות מחדש את האימג' שלנו כדי שהבניה מחדש תהיה מהירה יותר.

בשביל לראות את השימוש החוזר נעדכן את קובץ ה Dockerfile כך שיכלול את התוכן הבא:

FROM ubuntu:18.04

RUN ls
RUN ls

RUN echo test > /tmp/test.txt

נריץ בניה מחדש ונצפה שוב בהיסטוריה:

Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM ubuntu:18.04
 ---> 94e814e2efa8
Step 2/4 : RUN ls
 ---> Using cache
 ---> 2b962f1a6061
Step 3/4 : RUN ls
 ---> Using cache
 ---> 6876e790b0a8
Step 4/4 : RUN echo test > /tmp/test.txt
 ---> Running in cc2112622b6b
Removing intermediate container cc2112622b6b
 ---> 1e9b9dce7048
Successfully built 1e9b9dce7048
Successfully tagged layers01:latest

שימו לב לפלט Using cache שמופיע אחרי חלק מהשלבים. זה אומר שדוקר מזהה שהוא כבר יודע מה יצא מהרצת הפקודה ולכן משתמש באימג' שכבר שמור אצלו. נפעיל שוב history ונראה:

$ docker history layers01

1e9b9dce7048        49 seconds ago      /bin/sh -c echo test > /tmp/test.txt            5B                  
6876e790b0a8        17 minutes ago      /bin/sh -c ls                                   0B                  
2b962f1a6061        17 minutes ago      /bin/sh -c ls                                   0B                  
94e814e2efa8        5 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           5 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B                  
<missing>           5 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B                  
<missing>           5 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B                
<missing>           5 weeks ago         /bin/sh -c #(nop) ADD file:1d7cb45c4e196a6a8…   88.9MB              

שנוצרה שיכבה אחת חדשה והישנות יותר נלקחו מה Cache.

4. שכבות וגודל האימג'

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

FROM ubuntu:18.04

RUN echo hello > /tmp/test.txt

נבנה אותו ונשתמש בפקודה inspect כדי לראות מה גודל האימג' שנוצר:

$ docker build . -t size02:one
$ docker inspect  --format='{{.Size}}' size02:one
88908197

עכשיו נראה מה קורה אם נרצה למחוק את הקובץ באמצעות פקודת RUN נוספת. עדכנו את ה Dockerfile שיכיל את הקוד הבא:

FROM ubuntu:18.04

RUN echo hello > /tmp/test.txt
RUN rm /tmp/test.txt

נבנה ונסתכל בגודל:

$ docker build . -t size02:two
$ docker inspect --format='{{.Size}}' size02:two
88908197

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

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

FROM ubuntu:18.04

RUN echo hello > /tmp/test.txt && rm /tmp/test.txt

ועכשיו קיבלנו:

$ docker build . -t size02:three
$ docker inspect --format='{{.Size}}' size02:three
88908191

וכך הצלחנו להוריד 6 בתים מגודל האימג'.

מודל השכבות של דוקר הוא המפתח לביצועים הטובים של הכלי והוא שמאפשר לנו לשתף מידע בצורה יעיל בין אימג'ים וליצור מהר קונטיינרים חדשים. בניה של Dockerfile שמנצל טוב את המודל תאפשר לכם ליצור אימג'ים יעילים וקטנים יותר.