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

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

לא שמתי לב

03/12/2021

לא שמתי לב לגודל של ה Docker Image ... עד שהתחיל להיגמר לי המקום על הדיסק.

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

לא שמתי לב לאיטיות השאילתות ב DB ... עד שקוד התחיל להיכשל על Timeouts בדיוק בשיא העומס על המערכת.

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

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

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

מדריך: תמיכה במספר סביבות קוברנטס עם Kustomize

02/12/2021

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

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

המנגנון המובנה ב kubectl לניהול שינויים בין סביבות נקרא kustomize. בפוסט זה אציג דוגמה פשוטה לפרויקט שמשתמש ב kustomize כדי לייצר קבצי yaml שונים לסביבות השונות.

המשך קריאה

מצאו את ההבדלים

01/12/2021

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

איפשהו בדרך הקוד שלא עובד יתחיל לעבוד - וזה ייתן לנו רמז מה עשינו לא בסדר.

(בגיט יש מנגנון מובנה בשם git bisect שבנוי בדיוק על הרעיון הזה)

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

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

חלומות על עננים

30/11/2021

לפני פחות מעשרים שנה לשרתי יוניקס עדיין היתה נוכחות בחיים של מפתחי תוכנה. בחברות רבות היה "חדר שרתים" בו איחסנו שרתי יוניקס שלמים, וחברות אחרות היו שוכרות שרת שלם שישב באיזו חוות שרתים. אני אישית עוד זכיתי לעבוד על מכונת HP True 64 שיושבת בתוך ארון בחדר בהמשך המזדרון.

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

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

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

  1. כל שירותי הענן צריכים לעבור סטנדרטיזציה. לא צריך 5 דרכים שונות לאחסן קובץ. אני חולם על ממשק אחד מבוסס CLI שיעבוד לכל העננים בצורה זהה.

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

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

  4. אנחנו צריכים דרך לשתף spec שלם של מערכת על ענן. משהו כמו helm chart או docker compose עבור כל אותם אינסוף ממשקים של AWS או Azure או GCP או מה שלא תרצו.

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

ומה אתכם? איזה חלומות יש לכם על הענן?

קיים בשפה

29/11/2021

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

a = [1, 2, 3]
b = ['a', 'b', 'c']
c = ['@', '!', '#']

אז הפעלה של zip על שלושת הרשימות תחזיר רשימה אחת של שלושה איברים שכל אחד מהם מורכב מאיבר אחד מכל אחת מהרשימות, כלומר:

>>> list(zip(a, b, c))
[(1, 'a', '@'), (2, 'b', '!'), (3, 'c', '#')]

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

בקיצור הבעיה של זיפ היא מה עושים כשהרשימות לא באותו אורך. בפייתון החליטו לפני הרבה שנים שהפיתרון יהיה לקחת רק את החלק ברשימות שכן מתאים באורך. במילים אחרות אם הרשימה a תהיה ארוכה יותר מהשאר:

a = [1, 2, 3, 4, 5, 6]
b = ['a', 'b', 'c']
c = ['@', '!', '#']

אז התוצאה של ה zip תישאר בדיוק אותו דבר - והאיברים העודפים ב a פשוט לא יקבלו טיפול.

עכשיו תשמעו מה היה לבראנט בושר להגיד על זה:

It is clear from the author's personal experience and a survey of the standard library that much (if not most) zip usage involves iterables that must be of equal length.

ובהמשך אותו מסמך:

In fact, the author has counted dozens of other call sites in Python's standard library and tooling where it would be appropriate to enable this new feature immediately.

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

ופה יש שני דברים שצריך לשים לב אליהם:

  1. כל אחד היה יכול לעטוף את zip בפונקציה חדשה (נקרא לה safezip) שבודקת שאכן כל רשימות הקלט באותו האורך. מעטים אם בכלל עשו את זה.

  2. (וזה ניחוש שלי) - אחרי התוספת ל zip בפייתון 3.10, הרבה אנשים יתחילו להעביר את הפרמטר החדש ואפילו יסתכלו מוזר על אלה שלא מעבירים אותו.

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

עיקרון הרצף

27/11/2021

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

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

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

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

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

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

פייתון ומפתחות בלי מרכאות

26/11/2021

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

filters = [
        { id: 12, filter: None },
        { id: 15, filter: None },
        { id: 16, filter: lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

במקום להפעיל את פילטר מספר 16 הקוד מתרסק עם השגיאה הבאה:

Traceback (most recent call last):
  File "/Users/ynonp/tmp/blog/c.py", line 11, in <module>
    print(run_filter(16, [2, 5, 9, 11, 20]))
  File "/Users/ynonp/tmp/blog/c.py", line 8, in run_filter
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
StopIteration

שאומרת ש next נכשל כי הוא לא מצא אף פילטר שמתאים לתנאי. זה מוזר כי אני די בטוח שהוגדר פילטר שה id שלו הוא 16. כרגע הקוד לא כולל Type Hints ולכן גם mypy לא מתלונן.

נוסיף Type Hints ונראה אם זה יעזור לקבל יותר מידע:

from collections.abc import Callable
from typing import TypedDict 

class Filter(TypedDict):
    id: int
    filter: Callable[[int], bool]|None

filters: list[Filter] = [
        { id: 12, filter: None },
        { id: 15, filter: None },
        { id: 16, filter: lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x.get("id") == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

התוכנית עדיין מתרסקת אבל עכשיו לפחות mypy מצליח למצוא בעיות:

$ mypy b.py

b.py:9: error: Expected TypedDict key to be string literal
b.py:10: error: Expected TypedDict key to be string literal
b.py:11: error: Expected TypedDict key to be string literal
Found 3 errors in 1 file (checked 1 source file)

מבט ממוקד יותר בשורה 9 חושף את הבעיה:

{ id: 12, filter: None },

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

[{<built-in function id>: 12, <class 'filter'>: None}, {<built-in function id>: 15, <class 'filter'>: None}, {<built-in function id>: 16, <class 'filter'>: <function <lambda> at 0x1032b3d90>}]

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

from collections.abc import Callable
from typing import TypedDict 

class Filter(TypedDict):
    id: int
    filter: Callable[[int], bool]|None

filters: list[Filter] = [
        { "id": 12, "filter": None },
        { "id": 15, "filter": None },
        { "id": 16, "filter": lambda x: x > 7 }
        ]

def run_filter(filter_id, data):
    fn = next(x for x in filters if x["id"] == filter_id)["filter"]
    return [x for x in data if fn(x)]

print(run_filter(16, [2, 5, 9, 11, 20]))

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

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

מהיר, עמיד ומתועד

25/11/2021

שלוש מילים שאומרות הכל על הפיתרון הבא שעליך לכתוב:

מהיר- כי Over Engineering זו בעיה; כי יש לקוח שמחכה; כי הזמן שלך יקר ואין טעם לבזבז אותו על סיבוכים מיותרים.

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

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

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