ניהול תלות

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

טיפול במודולים ובתלות של המשתמשים

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

שימוש במודולים-פרטניים ובכלל 1:1:1

השאלה הראשונה שמתעוררת במהלך פיתוח מבנה המבוסס על פריט מידע שנוצר בתהליך פיתוח (Artifact), היא בדיקה כמה פונקציונליות צריך לכלול מודול יחיד. ב-Bazel, מודול מיוצג על ידי יעד שמציין יחידה הניתנת לבנייה, כמו java_library או go_binary. באופן קיצוני, ניתן לכלול את הפרויקט כולו במודול אחד על ידי הצבת קובץ BUILD אחד בשורש ושליחה רקורבית של כל קובצי המקור של הפרויקט. במצב קיצוני אחר, ניתן ליצור כמעט כל קובץ מקור למודול של עצמו, ולחייב כל קובץ ליצור קובץ BUILD בכל קובץ שהוא תלוי בו.

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

רמת הפירוט המדויקת משתנה בהתאם לשפה (ולעתים קרובות גם בשפה), אך Google נוטה להעדיף מודולים קטנים באופן משמעותי מאלה שלרוב הם כתובים במערכת build מבוססת משימות. במקרים רבים, תצורה בינארית טיפוסית ב-Google תלויה בעשרות אלפי יעדים, ואפילו צוותים בגודל בינוני יכולים להחזיק כמה מאות יעדים בטווח הקוד שלה. בשפות כמו Java שבהן אריזות מובנות חזקות, כל ספרייה מכילה בדרך כלל חבילה יחידה, יעד וקובץ BUILD (מכנסיים, מערכת בנייה אחרת מבוססת על Bazel, קורא לזה כלל של 1:1). שפות עם מוסכמות אריזה חלשות יותר מגדירות לעיתים קרובות יעדים מרובים בכל קובץ BUILD.

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

חלק מהכלים האלה, כמו buildifier ו-buildozer, זמינים ב-Bazel בספריית buildtools.

חשיפת מודול מינימלי

בzזelל ומערכות בנייה אחרות מאפשרות לכל יעד לציין חשיפה: נכס שמציין אילו יעדים אחרים עשויים להיות תלויים בו. היעדים יכולים להיות ציבוריים, ובמקרה כזה הם יכולים להתייחס אליהם מכל יעד אחר בסביבת העבודה; פרטיים, ובמקרה זה ניתן להפנות אליהם רק מאותו קובץ BUILD; או גלוי רק לרשימה המוגדרת במפורש של יעדים אחרים. חשיפה היא למעשה ההיפך מתלות: אם יעד A רוצה להיות תלוי ביעד B, יעד B חייב להיות גלוי בעצמו ל-A. כמו ברוב שפות התכנות, בדרך כלל עדיף לצמצם ככל האפשר את החשיפה. בדרך כלל, צוותים ב-Google בוחרים יעדים רק באופן ציבורי, אם היעדים האלה מייצגים ספריות נפוצות שזמינות לכל צוות ב-Google. צוותים שמחייבים אחרים לתאם איתם לפני השימוש בקוד שלהם, ישמרו רשימה של יעדי לקוחות כחשיפה של היעד שלהם. יעדי היישום הפנימיים של כל צוות יוגבלו רק לספריות שבבעלות הצוות, וברוב קובצי BUILD יהיה יעד אחד בלבד שאינו פרטי.

ניהול תלות של אחרים

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

יחסי תלות פנימיים

בפרויקט גדול שמחולק למודולים מפורטים, רוב יחסי התלות עשויים להיות פנימיים. כלומר, נמצא ביעד אחר ומוגדר באותו מאגר נתונים של מקור. יחסי תלות פנימיים שונים מתלות תלויות הם בכך שהם נבנו מהמקור ולא ירדו כפריטים שנוצרו מראש בזמן הפעלת ה-build. המשמעות היא גם שאין "גרסה" של יחסי תלות פנימיים – יעד וכל התלויות הפנימיות שלו תמיד בנויים באותו אישור/גרסה במאגר. בעיה אחת שיש לטפל בה בזהירות ביחס לתלות פנימית היא כיצד לטפל בתלויות טרנזיטיביות (איור 1). נניח שיעד א' תלוי ביעד ב', שתלוי ביעד ספרייה משותף ג'. האם יעד א' יכול להשתמש בכיתות המוגדרות ביעד ג'?

יחסי תלות טרנזיטיביים

איור 1. יחסי תלות טרנזיטיביים

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

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

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

יחסי תלות חיצוניים

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

ניהול תלות אוטומטי לעומת ידני

מערכות build יכולות לאפשר ניהול של גרסאות של יחסי תלות חיצוניים באופן ידני או אוטומטי. כשמנהלים את ה-build באופן ידני, קובץ ה-build מפרט במפורש את הגרסה שהוא רוצה להוריד מהמאגר של פריטי המידע שנוצרו בתהליך פיתוח (Artifact), ולעיתים קרובות באמצעות מחרוזת גרסה סמנטית כגון 1.1.4. כשקובץ המקור מנוהל באופן אוטומטי, הוא מציין מגוון של גרסאות מקובלות, ומערכת ה-build תמיד מורידה את הגרסה האחרונה. לדוגמה, Gradle מאפשרת להצהיר על תלות של רצף מילים בשם "1.+ " כדי לציין שכל וריאציה של גרסה משנית או תיקון של גרסה קבילה, כל עוד הגרסה הראשית היא 1.

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

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

כלל הכולל גרסה אחת

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

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

יחסי תלות חיצוניים טרנזיטיביים

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

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

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

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

שמירת תוצאות build באמצעות יחסי תלות חיצוניים

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

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

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

אבטחה ואמינות של יחסי תלות חיצוניים

בהתאם לחפצים שמקורם במקורות צד שלישי הם מסוכנים מטבעם. קיים סיכון זמין לזמינות אם מקור של צד שלישי (כמו מאגר מידע שנוצר בתהליך פיתוח) יושמט, מפני שכל ה-build שלך יותחצר באופן יחסי אם לא תהיה לו אפשרות להוריד יחסי תלות חיצוניים. יש גם סיכון אבטחה: אם מערכת הצד השלישי נפגעה על ידי תוקף, התוקף עלול להחליף את המרכיב המופנה עם אחד מהעיצוב שלו, וכך לאפשר לו להחדיר קוד שרירותי למבנה שלך הנתונים. אתם יכולים לצמצם את הבעיות האלה על ידי שיקוף של פריטי מידע שנוצרו בתהליך פיתוח (Artifact), התלויים בשרתים שאתם שולטים בהם וחסימת מערכת ה-build שלכם ולא תאפשר להם לגשת למאגרי מידע של צדדים שלישיים כמו Maven Central. המטרה היא, שהמראות האלה משקיעות מאמץ ומשאבים לתחזוקה, כך שהבחירה אם להשתמש בהן לעתים קרובות תלויה בגודל הפרויקט. בנוסף, ניתן למנוע לחלוטין את בעיית האבטחה עם תקורה מועטה, באמצעות דרישת גיבוב (hash) של כל פריט מידע שנוצר על ידי צד שלישי במאגר הנתונים המקורי, וכתוצאה מכך ה-build ייכשל אם יבוצעו בו שינויים. חלופה נוספת, שצדדית לחלוטין את הבעיה היא לספק את יחסי התלות של הפרויקט שלך. כשפרויקט מספק את יחסי התלות שלו, הוא בודק את בקרות המקור לצד קוד המקור של הפרויקט כמקור או כבינארי. המשמעות היא שכל התלות החיצוניות של הפרויקט מומרות לתלות פנימית. Google משתמשת בגישה הזו באופן פנימי, ובודקת כל ספרייה של צד שלישי ההפניה דרך Google לספרייה third_party שבשורשי עץ המקור של Google. עם זאת, הדבר פועל ב-Google רק מפני שמערכת בקרת המקור של Google מותאמת אישית לטיפול במונופורם גדול במיוחד, לכן ייתכן שלא ניתן למכור שירותים לכל הארגונים.