הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

טיפ דוקר: סדר פעולות ב Dockerfile

14/03/2022

קובץ ה Dockerfile הבא מגדיר בניה של אפליקציית Rails שגם מכילה קוד צד לקוח, שבתורו מתקמפל עם node.js:

FROM ruby:2.7

WORKDIR /app
COPY . .

ENV RAILS_ENV=production
ENV NODE_ENV=production

RUN gem install bundler:2.2.5
RUN sh -c "apt-get update && apt-get install -y nodejs npm"

RUN bundle install

RUN sh -c "cd client; npm install"
RUN "./bin/rails assets:precompile"

CMD ["./bin/rails", "s"]

רואים מה הבעיה כאן?

המשך קריאה

שלוש סיבות למה לא כדאי להכריח פריימוורקס לעבוד כמו שאתם רוצים

13/03/2022

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

  1. אין אחריות (או יותר נכון אין עזרה מגוגל) - מרגע שהתחלנו לשבור את הפריימוורק כדי להתאים אותו לצרכים שלנו, יהיה קשה מאוד להיעזר בגוגל כדי למצוא ולתקן באגים. ה Use Case שלנו יהיה שונה ואי אפשר לדעת איזה באגים הגיעו בגלל השינוי שלנו, איזה באגים נובעים באמת מטעויות ושימוש לא נכון, ואיזה באגים באמת קיימים בפריימוורק בלי קשר אלינו.

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

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

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

היום למדתי: הסלקטור focus-within

12/03/2022

סלקטור מאוד שימושי ב CSS שגיליתי רק לאחרונה הוא :focus-within. הוא מאפשר לזהות מתי פוקוס נמצא על אלמנט או על אחד הילדים שלו.

ולמה זה טוב? נניח שאנחנו בונים קומפוננטה מורכבת ב HTML/CSS. בואו נגיד שזו תיבת טקסט לבחירת תגיות, שכשלוחצים על התיבה מופיעה רשימה של כל התגיות לבחירה. מבחינת הפוקוס, לחיצה על תיבת הטקסט נותנת לה את הפוקוס, ואחרי זה לחיצה על כל אחד מהפריטים מעבירה את הפוקוס לפריט שנלחץ. אם תיבת הטקסט ורשימת הפריטים נמצאים בתוך אותו div, אז אני יכול להשתמש ב focus-within על הדיב העוטף הראשי כדי להחליט אם להציג או להסתיר את רשימת האפשרויות.

בקוד ה HTML יראה כך:

<div class="multiselect">
  <label>
      Click To Select:
      <input type="text" />
  </label>

  <ul>
    <li><label tabindex="0"><input type="checkbox" />item 1</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 2</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 3</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 4</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 5</label></li>
  </ul>
</div>

<div class="multiselect">
  <label>
      Click To Select:
      <input type="text" />
  </label>

  <ul>
    <li><label tabindex="0"><input type="checkbox" />item 1</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 2</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 3</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 4</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 5</label></li>
  </ul>
</div>

<div class="multiselect">
  <label>
      Click To Select:
      <input type="text" />
  </label>

  <ul>
    <li><label tabindex="0"><input type="checkbox" />item 1</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 2</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 3</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 4</label></li>
    <li><label tabindex="0"><input type="checkbox" />item 5</label></li>
  </ul>
</div>

וה CSS יהיה:

.multiselect:focus-within {
  background: lightblue;
}

.multiselect ul {
  display: none;
}

.multiselect label {
  display: block;
  width: 100%;
}

.multiselect:focus-within ul {
  display: block;
}

והתוצאה live בקודפן היא:

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

בעולם האמיתי

11/03/2022

נניח שיש לכם שתי רשימות ממוינות בפייתון ואתם צריכים לייצר מהן רשימה שלישית שתהיה ממוינת גם היא. כלומר בהינתן:

a = [10, 20, 33, 51, 94, 100]
b = [2, 3, 99, 102, 500]

צריך להדפיס את הרשימה:

[2, 3, 10, 20, 33, 51, 94, 99, 100, 102, 500]

נו, מה הבעיה אתם אומרים, אנחנו בפייתון, אז אפשר לכתוב פשוט:

def test1():
    return sorted([*a, *b])

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

def test2():
    i = 0
    j = 0
    output = []
    while True:
        if i >= len(a):
            output.extend(b[j:])
            break

        if j >= len(b):
            output.extend(a[i:])
            break

        next_a = a[i]
        next_b = b[j]

        if next_a < next_b:
            i += 1
            output.append(next_a)
        else:
            j += 1
            output.append(next_b)

    return output

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

print("Short solution took - ", timeit.timeit("test1()", globals=locals()))
print("Long smart solution tool - ", timeit.timeit("test2()", globals=locals()))

והתוצאה הלא ממש מפתיעה:

Short solution took -  0.26264833300956525
Long smart solution tool -  1.3138192089973018

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

בוא ננסה ונראה מה יקרה

10/03/2022

לפעמים אנחנו יושבים מול בעיה ולא בטוחים מספיק לגבי הטכנולוגיה או הפרטים הטכניים שלה. במצבים כאלה אנחנו עשויים להגיד משהו כמו "בואו ננסה ונראה מה יקרה".

בתיאוריה אין בזה שום סיכון...

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

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

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

הלימוד הכואב

09/03/2022

יש שני סוגים מרכזיים של לימוד-

  1. הלימוד הקל זה כשאתה לומד משהו שקודם לא ידעת. לדוגמה היום למדתי ש"יעה" ברוסית זה "סאבוק". הנה עכשיו גם אתם יודעים את זה.

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

דוגמה יותר קשה היתה כשיותר ויותר לקוחות שלי עברו מ perl ל Python. זה לקח לי שנתיים להבין שאנשים מעדיפים Python על פני perl. זה לקח לי עוד שנתיים אחרי זה להתחיל להעדיף בעצמי את פייתון על פני פרל. 90% מהזמן לא הושקע בלימוד פייתון אלא בניסיון מנטלי לדחות את מה שהעולם ניסה להגיד לי.

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

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

מדריך: הקמת שרת לוגים עם syslog-ng

08/03/2022

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

המשך קריאה

אילוצים מזויפים

07/03/2022

״בשביל להגיע ל Zero Downtime Deployments אנחנו צריכים לשדרג לקוברנטיס.״

״בשביל לפתור את בעיות הביצועים אנחנו חייבים לעבור מאנגולר לריאקט.״

״בשביל שנוכל לבנות פיצ'רים חדשים יותר מהר אנחנו חייבים לעשות Rewrite לכל המערכת.״

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

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

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

טיפ דוקר: שימוש ב Multistage Builds

06/03/2022

בבניית Docker Image יהיו לנו מצבים שאנחנו צריכים אימג' בסיס מסוים כדי לבצע עבודת "הכנה", אבל אחרי שההכנה הסתיימה אפשר לקחת את התוצרים ולהגיש אותם מאימג' אחר. דוגמה פשוטה היא אפליקציית next.js שיוצרת אתר HTML סטטי. אני צריך את node.js וכל מה שקשור אליו בשביל לבנות את האתר הסטטי, אבל אחרי שבניתי אותו אני יכול להגיש את התוצאה מאימג' של nginx.

בדוקר יש מנגנון שנקרא multi stage builds שנועד לתת מענה למצבים כאלה ולאפשר בניה מאימג' אחד ואריזה באימג' אחר. בואו נראה איך זה עובד.

המשך קריאה

מושגים בסיסיים ב Docker: קונטיינרים ואימג'ים

05/03/2022

דוקר פותח בחברת איחסון ענן בשם dotCloud (כיום כבר שינתה את שמה ל Docker Inc) בשנת 2013. באותה תקופה דוטקלאוד הציעה מוצר שיודע לקחת קוד מהמחשב שלכם ולהעלות אותו להרצה בענן שלהם - והבעיה המרכזית שהיתה להם היתה איך להעביר את הקוד ממכונה אחת לשניה בלי לשבור אותו.

עד לפיתוח של דוקר בשביל להעביר קוד ממקום למקום היית צריך לשכפל את "הדברים החשובים בסביבת הריצה" למכונה החדשה. אז אם יש לי תוכנית פייתון שצריכה לרוץ על שרת, אני צריך להתקין על השרת את אותה גירסת פייתון שעליה עבדתי בפיתוח, את אותן ספריות פייתון שהתוכנית שלי צריכה (עם pip install), והחלק הקשה - את החלקים ממערכת ההפעלה שרלוונטיים לתוכנית שלי. אם התוכנית שלי משתמשת ב Shared Library מסוימת אז אני צריך לוודא שהספריה תותקן גם על השרת. אם התוכנית שלי משתמשת בתוכנית חיצונית מסוימת, אותה תוכנית צריכה להיות מותקנת גם על השרת.

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

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

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

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

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

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

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

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

המנוע של דוקר (Docker Engine) הוא רכיב תוכנה שיודע לבנות קונטיינר מתוך אימג'. לכן על כל מכונה שמריצה Docker Engine אני יכול לקחת כל אימג' שתואם לו וליצור ממנו קונטיינר.

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

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

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

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

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

החל מ 2015, דוקר וחברות מובילות נוספות בתעשיה, מתחזקים מבנה קוד פתוח שנקרא OCI (קיצור ל Open Container Initiative) שאחראי על יצירת סטנדרט פתוח לפורמטים של קונטיינרים ואימג'ים.

למידע נוסף על דוקר ואיך המנגנון הזה של קונטיינרים עובד מתחת לפני השטח שווה לקרוא את המאמר בקישור:

https://accenture.github.io/blog/2021/03/10/docker-behind-the-scenes.html