יום חמישי, 4 ביולי 2019
איך יוצרים ספריית קוד שאפשר לצרוך גם ב-Java וגם ב-Javascript?
יום חמישי, 16 במאי 2019
Pull vs. Push
כרגע, אנחנו מנהלים את רוב התקשורת אצלנו בארגון בצורה של polling. לעיתים קרובות באים לשאול אותי למה אנחנו לא עוברים למודל תקשורת שהוא PubSub/Web Sockets/SSE וכד', ויש לא מעט מקרים שבהם פנימית צוותי הפיתוח משתמשים בטכניקות האלה פנימית בתוך המוצרים.
כדי לסייע להבין את השיקולים ולקבל החלטה מושכלת, הפוסט הזה ינסה לעשות קצת סדר בטכניקות השונות על יתרונותיהן והאתגרים שהן מציבות.
במהלך הפוסט נשתמש במושג "ספק" או "שרת" כדי לציין את המערכת שהיא בעלת המידע ויודעת שהוא התעדכן, ובמושגים "צרכן" או "לקוח" כדי לציין את המערכת המעוניינת במידע ומעוניינת לדעת שהוא התעדכן.
החלטה טכנולוגית תמיד צריכה להתקבל בהתאם לצורך. ה-Use Case שאנו עוסקים בו:
- מידע המתעדכן ברובו באופן ידני (כלומר לא מדובר על כמויות וקצבים של IoT לדוגמא)
- המשתמשים בקצה עובדים בשיתוף פעולה ולכן צריכים לראות שינויים אחד של השני בטווח של שניות בודדות
- חלק מהעדכונים הם קריטיים ברמה הבטיחותית ולכן חייבים לוודא שהמשתמש בקצה מקבל עדכונים של אחרים או מודע לזה שהוא לא מקבל אותם
- המערכת שלנו היא read intensive
- אנחנו מבססים את רוב הממשקים שלנו על GraphQL, לכן אני מניח שלרוב יש לצרכן יכולת לבחור את השדות שהוא צופה בהם מתוך כלל המידע.
בפוסט אני לא מציין יתרונות ואתגרים המשותפים לכל השיטות. למשל, בכל השיטות להלן הלקוח יכול לחזור למצב מעודכן אחרי נתק ארוך גם בלי לטעון מחדש את כל המידע.
מה ההבדל בין Push ל-Pull?
צריך להבדיל בין המבט הלוגי לבין המבט הפיסי באשר לשאלה הזאת.
- במבט הלוגי, ההבדל בין push ל-pull הוא: האם כדי להתעדכן, על המשתמש (האנושי, או קוד שמחוץ לרכיב שמטפל בתקשורת והפרוטוקול) ליזום פנייה לשירות (pull) או לא (push). בהסתכלות הזאת, polling היא דרך מימוש לביצוע push. לצורך העניין, כאשר אנחנו משתמשים ב-Kafka client בתוך הקוד שלנו, מאחורי הקלעים ה-client מבצע polling מול ה-Kafka ואנחנו מקבלים חווית push, לכן ברמה הלוגית אנחנו מקבלים חווית push.
- במבט הטכני יש כאן את שאלת "מי השרת?" - מי יוזם את העברת המידע (שברמה הטכנית הנמוכה ביותר זו שאלה זהה ל-"מי צריך להכיר את ה-IP של השני כדי לבצע העברה של המידע?" - זה הלקוח). אם היוזם הוא הספק זה נחשב push, אם היוזם הוא הצרכן זה נקרא pull. הרבה מה-pattern-ים הם שילוב של השניים.
ל-use case שהגדרנו אנחנו מחפשים רק שיטות שהן push לוגי, ולכן נדון רק בהן.
שיטות ההתעדכנות ותכונותיהן
נעשה מעבר קצר על רוב השיטות הקיימות להעברת עדכונים ונראה מה מאפיין אותן. נתעסק בעיקר בנושא של איך צרכן מקבל עדכוני מידע ופחות באיך הוא עושה טעינה ראשונית שלו.
Polling
השיטה הפשוטה ביותר. הרעיון הוא שבאופן מחזורי, מדי כמה זמן (קבוע או לא) הצרכן פונה לספק כדי לקבל תמונת מצב עדכנית (השם "poll" בא מהמילה "סקר", כלומר דגימה).
בגרסה הפשוטה ביותר של polling הצרכן מושך את המידע במלואו, בגרסה הבינונית רק רשומות שהתעדכנו ובגרסה המורכבת ביותר הוא מקבל גם מה התעדכן (ברמת עמודות), כאשר הגרסה האחרונה מורכבת משמעותית מהשתיים הראשונות ולכן חלק מהיתרונות של polling לא חלים עליה. הניתוח כאן מתייחס בעיקר לגרסה הבינונית.
יתרונות:
- פשוט. מבחינת הספק כל request עומד בפני עצמו וצריך פשוט לענות לו. הן מבחינת הספק והן מבחינת הצרכן כל הפניות זהות בצורתן ואין כל מיני מקרי קצה בפרוטוקול שצריך לטפל בהם.
- גמישות מוחלטת ללקוח בשאילתה.
- Scalable - מכיוון שכל request עומד בפני עצמו, הוספה של שרת נוסף ל-LB תגרום מיידית לחלוקה מחדש של העומס שווה בשווה. הכל stateless לחלוטין.
- קל להתאושש מבעיות - הלקוח זוכר איפה הוא נמצא. לא משנה איפה היה כישלון - בין אם בשרת, ברשת או בשמירה בלקוח - ממשיכים מהנקודה האחרונה ששמורה אצל הלקוח. זאת גם השיטה שבה הכי קל להתאושש מאירוע של מעבר לאתר backup (או availability zone אחר), אפילו אם המעבר גרם לחזרה לאחור במידע.
- אין צורך בניהול לקוחות. הספק מטפל בשאילתה המיידית של הצרכן ואז שוכח מקיומו.
- הקטנת העומס על הרשת - במקרה של שינויים תכופים יותר מקצב הדגימה,
הצרכנים לא יקבלו את כל עדכוני הביניים אלא בקצב שבו הם יקבעו. רוב שאר
השיטות מעבירות ללקוח כל עדכון.
- הנקודה הזאת גרמה לנו להעביר את דווקא את ה-API שהיה לנו הכי ברור שאמור להיות PubSub ל-Polling - יש שם הפצה מחזורית של מידע שמבטל מידע קודם ובשיטות ה-push העברנו הרבה יותר מידע על הרשת. עוד לגבי זה יופיע באתגרים של השיטות האחרות.
- עוד תופעה מעניינת שגילינו בהקשר הזה - כאשר יש הרבה מאד עדכוני מידע קטנים, יש עוד יתרון בלקחת אותם ב-batch - הכיווץ של המידע (gzip) הרבה יותר יעיל. באופן פחות משמעותי זה מקטין גם את ה-overhead של HTTP.
אתגרים:
- ב-polling יש delay מובנה (זמן הדגימה) שחוסם מלמטה את ה-latency המקסימאלי לקבלת עדכון.
- קשה להבטיח שהצרכן יקבל את כל מצבי הביניים של המידע (כלומר אם היו שני עדכונים בין דגימות).
- עומס על הספק - המון שאילתות שהוא צריך לחשב להן תשובה למרות ששום דבר לא השתנה.
- השאלות התכופות של "יש משהו חדש?" באופן מיותר עלולות ליצור עומס על הרשת. ב-use cases של הרבה צרכנים בקצב דגימה גבוה ורשת עם רוחב פס לא מאד גבוה (למשל פנייה מהאינטרנט אל תוך הרשת הארגונית) עשוי להיות לזה impact משמעותי.
- Polling פחות מתאים למובייל כיוון שהוא גורם לבזבוז משמעותי של הסוללה. עוד ניתן למצוא כאן, וגם מקרה אמיתי שממחיש.
Long Polling
לשיטה הזאת יש כמה וריאציות. הבסיסית והנפוצה ביותר היא שהלקוח פונה לשרת עם שאילתה, אבל כל עוד אין עדכון השרת לא עונה וה-connection נשאר פתוח במצב ממתין. במקרה שמגיע עדכון, השרת עונה ללקוח ואז הלקוח פונה שוב. אם הרבה זמן אי עדכון, הלקוח יקבל timeout ויפנה שוב.
יתרונות:
- קבלת עדכון מיידית - ברגע שמגיע עדכון הוא מופץ ללקוחות.
- גמישות ללקוח בשאילתה (לא בטוח שטוב כמו polling כי במימוש הטריוואלי אין שימוש ביכולת DB).
- Scalable - כאשר מוסיפים שרת ל-LB, פניות חדשות יחולקו באופן שווה. ה-scale איטי יותר מאשר ב-Polling כי נדרש לעבור timeout שלם עד שהעומס יחולק באופן שווה.
- קל להתאושש מבעיות - הלקוח זוכר איפה הוא נמצא (כמו ב-polling).
אתגרים:
- יש צורך בניהול לקוחות (אם כי זה ניהול לטווח קצר בלבד, פשוט יותר מחלופות אחרות בהן יש ניהול).
- צריך לכתוב קוד ייעודי כדי להבין "על אילו צרכנים עדכון משפיע". עדכון יכול לגרום להודעת update ללקוח אחד, create לאחר, delete לשלישי ולרביעי שום דבר - צריך להסתכל מה השאילתות ה-"פתוחות" כרגע ולהבין איך העדכון רלוונטי לכל אחת מהן.
- אם יש מספר instance-ים של הספק (ו\או CQRS בין שרתים), איך ה-instance שמולו בוצע עדכון מודיע עליו ל-instance-ים האחרים כדי שיודיעו לצרכנים שמחוברים אליהם? זה דורש מנגנון להעברת העדכונים.
- איך מבטיחים שכל הלקוחות יקבלו את ההודעה? לא ניתן להודיע להם באופן אטומי יחד עם הכתיבה ל-DB, ולכן עלול להיות מקרה שנודיע להם על עדכון שלא נשמר או ששמרנו עדכון בלי להודיע. דרך אחת להתמודד היא לעשות ב-DB לוג עדכונים שעליו מסומן איזה לקוח קיבל איזה עדכון. זה לא נעים וגם מוסיף delay במקרה של כישלון (כי חייב להיות רכיב רקע שעושה polling מול הלוג).
- Back Pressure - צרכן שלא עומד בקצב העדכונים פוגע בספק ומחייב התמודדות של הספק עם מקרים כאלה.
Inbox Polling
השיטה הזאת פועל בשיטה של הימנות - הצרכן פונה לספק ומספר לו לאיזו שאילתה הוא מעוניין להימנות. הספק מחזיר לו state נוכחי, ושומר לכל צרכן inbox עם הודעות רלוונטיות. בכל עדכון הספק מחַשב עבור אילו צרכנים העדכון רלוונטי ושומר הודעה על עדכון ב-inbox שלהם. הלקוח עושה polling מול הספק כדי לבדוק אם יש לו הודעות חדשות.
יתרונות:
- בדיקת עדכון - פשוט. מבחינת הספק כל request עומד בפני עצמו וצריך פשוט לענות לו. הן מבחינת הספק והן מבחינת הצרכן כל הפניות זהות בצורתן ואין כל מיני מקרי קצה בפרוטוקול שצריך לטפל בהם.
- גמישות ללקוח בשאילתה (לא בטוח שטוב כמו ב-polling כי זה יותר מורכב).
- Scalable - מכיוון שכל request עומד בפני עצמו, הוספה של שרת נוסף ל-LB תגרום מיידית לחלוקה מחדש של העומס שווה בשווה. הכל stateless לחלוטין. זה scalable רק בטיפול ב-polling, ה-scale בעדכוני ה-inbox הוא לאו דווקא טריוויאלי.
- קל להתאושש מבעיות - הלקוח זוכר איפה הוא נמצא. כמו ב-polling רגיל, רק שמספרי הגרסה הם מול ה-inbox ולא מול ה-data.
- פעולת ה-read, שהיא השכיחה, הופכת למהירה מאד (המאמץ עובר ל-write שהוא הרבה פחות עמוס מראש).
אתגרים:
- ב-polling יש delay מובנה (זמן הדגימה) שחוסם מלמטה את ה-latency המקסימלי לקבלת עדכון.
- השאלות התכופות של "יש משהו חדש?" באופן מיותר עלולות ליצור עומס על הרשת. ב-use cases של הרבה צרכנים בקצב דגימה גבוה ורשת עם רוחב פס לא מאד גבוה (למשל פנייה מהאינטרנט אל תוך הרשת הארגונית) עשוי להיות לזה impact משמעותי.
- יש צורך בניהול לקוחות. בפרט, קשה להחליט שלקוח מסויים לא רלוונטי יותר (ולכל לקוח מנוהל יש השפעה על הביצועים, גם אם הוא הפסיק לפנות לספק).
-
צריך לכתוב קוד יעודי להבנה "על אילו צרכנים עדכון משפיע?". עדכון יכול
לגרום להודעת update ללקוח אחד, לאחר create, לאחר delete ולאחר שום דבר.
- יש כאן שתי אפשרויות: אפשר לחשב בזמן הכתיבה לאילו לקוחות כל עדכון רלוונטי ולכתוב את זה באותה הטרנזקציה, מה שיגרום לזמן update ארוך מאד. לחילופין אפשר לעשות את זה ברקע, ואז יש בעיית הבטחת הפצה כמו ב-long polling או בעיית סדר מסרים, תלוי במימוש.
- Polling פחות מתאים למובייל כיוון שהוא גורם לבזבוז משמעותי של הסוללה. עוד ניתן למצוא כאן, וגם מקרה אמיתי שממחיש.
Web Socket (WS)/Server Side Events (SSE)
בשיטה הזאת פותחים ערוץ פתוח בין הספק לצרכן, שבו השרת יכול לדחוף מידע אל הלקוח (SSE מבוסס HTTP בעוד ש-Web Sockets מבוסס TCP). מדובר בטכנולוגיות סטנדרטיות שנתמכות על ידי דפדפנים מודרניים (אבל לא על ידי הישנים). WS היא במידה רבה השיטה "המגניבה והחדשנית" היום - צריך לזכור רק שהיא באה לתת מענה למקומות שפעם היו משתמשים ב-socket פתוח.
Subscription של GraphQL, למשל, מממשים לרוב באמצעות Web Sockets.
יש עוד וריאציות שמבוססות על socket פתוח (כמו יכולות ב-HTTP2, או פתיחת בקשת HTTP והשארתה בחיים על ידי הספק ע"י הזרמה פריודית של נתונים רק כדי למנוע timeout, משהו שנשמע כמו long polling אבל בלי פתיחה מחדש). הוואריאציות מתאפיינות ביתרונות ואתגרים דומים.
יתרונות:
- קבלת עדכון מיידית - ברגע שמגיע עדכון הוא מופץ ללקוחות.
- גמישות ללקוח בשאילתה (לא בטוח שטוב כמו polling כי במימוש הטריוואלי אין שימוש ביכולת DB).
- אם הלקוח זוכר איפה הוא נמצא ברצף העדכונים, קל להתאושש מבעיות.
אתגרים:
- יש צורך בניהול לקוחות.
- צריך לכתוב קוד יעודי להבנה "על אילו צרכנים עדכון משפיע?". עדכון יכול לגרום להודעת update ללקוח אחד, לאחר create, לאחר delete ולאחר שום דבר.
- אם יש מספר instance-ים של הספק (ו\או CQRS בין שרתים), איך ה-instance שמולו בוצע עדכון מודיע עליו ל-instance-ים האחרים כדי שיודיעו לצרכנים שמחוברים אליהם? זה דורש מנגנון להעברת העדכונים.
- Scaleability - כאשר מוסיפים עוד שרת ל-LB, העומס הקיים נשאר ורק פניות חדשות יתחלקו שווה בין השרתים. כדי לחלק מחדש עומסים צריך לנתק באופן יזום חלק מהלקוחות כדי שיתחברו מחדש (וקנפוג ה-LB ל-least connections).
- איך מבטיחים שכל הלקוחות יקבלו את ההודעה? לא ניתן להודיע להם באופן אטומי יחד עם הכתיבה ל-DB, ולכן עלול להיות מקרה שנודיע להם על עדכון שלא נשמר או ששמרנו עדכון בלי להודיע. דרך אחת להתמודד היא לעשות ב-DB לוג עדכונים שעליו מסומן איזה לקוח קיבל איזה עדכון. זה לא נעים וגם מוסיף delay במקרה של כישלון (כי חייב להיות רכיב רקע שעושה polling מול הלוג).
- Back Pressure - צרכן שלא עומד בקצב העדכונים פוגע בספק ומחייב התמודדות של הספק עם מקרים כאלה.
- בזבזני במשאבים - מצריך החזקת ערוץ פתוח לכל צרכן גם אם כרגע לא בשימוש.
- במקרה של Web Socket אין שימוש ב-HTTP, ולכן יש לו חסרונות אבטחתיים ובטיפול סטנדרטי של רכיבי רשת - proxies, נתבים חכמים וכו'.
PubSub
השיטה הזאת דורשת bus ארגוני (=message broker) שעליו יופצו כלל העדכונים (אם אתם חושבים: "אבל לא חייבים להעביר את התוכן של העדכון!", זאת תהיה השיטה הבאה). יהיו אוסף של topic-ים שיסוכמו בין ספקים לצרכנים, והספקים יעבירו עליהם את כל העדכונים שנוצרו. הצרכנים ירַשמו לעדכונים מול ה-bus.
יתרונות:
- קבלת עדכון מיידית - ברגע שמגיע עדכון הוא מופץ ללקוחות.
- חוסר צימוד בין צרכנים לספקים - אפשר להוסיף ספקים נוספים ששולחים על אותו topic (יתרון אדיר ומתיר צימודים ב-use cases של many-to-many). זה גם אומר שאין ניהול לקוחות.
- Scalable - הספק לא מושפע ממספר הצרכנים וטריוויאלי להוסיף עוד instance-ים לספק - כל instance אחראי לדווח ל-bus על השינויים הרלוונטיים אליו. גם בכמות המידע, כיום קל יחסית לבזר את ה-bus.
אתגרים:
- אין גמישות ללקוח בשאילתה - אוסף החתכים ידוע ומוגדר מראש וכל סינון נוסף נעשה אצל הלקוח.
- בניגוד לכל שאר השיטות, מכיוון שאין כאן היכרות עם שאילתה ספציפית ללקוח אז כל עדכון חייב לעבור עם כל שדות היישות, מה שיגדיל את כמות הנתונים העוברת ברשת משמעותית וגם את העומס על הצרכנים.
- איך מבטיחים שה-bus יקבל את ההודעה? כי אי אפשר לכתוב אליו באופן אטומי עם הכתיבה ל-DB. דרך התמודדות אחת היא לעשות ב-DB לוג עדכונים כמו בשיטות הקודמות, החסרון הוא הגדלת ה-delay במקרים האלה. שיטה אחרת היא שהספק יאזין ל-topic של עצמו ויכתוב ל-DB באופן אסינכרוני. זה מגדיל משמעותית את זמני ה-evantual consistancy שאחריהם מי שכתב יכול לקרוא את מה שהוא כתב.
- Bus ארגוני = תלות ארגונית. כלומר תלות במוצר שקשה להחליף, קשה לשנות מוסכמות על שמות topic-ים וכו'. ככל שהארגון גדול יותר זה בעייתי יותר. צריך לזכור גם שבארכיטקטורה כזאת קשה לדעת מי כל הלקוחות של מידע מסויים.
- Bus מרכזי הוא נקודת כשל מרכזית. הוא נדרש לתחזוקה ויציבות ברמה גבוהה כמו אלה של רכיבי הרשת הפיסיים (כמו FW ונתבים).
- מעבר כלל הנתונים (בניגוד לרק מה שנדרש עבור הלקוח ברמת שורות ועמודות) יגרום לעומס משמעותי על ה-bus - דורש מוצר חזק. פוטנציאל גם לעומס על הרשת.
- לא נתמך על ידי הדפדפן ישירות ולכן לא מתאים להעברת העדכונים עד ל-front end (ניתן ליצור ספריית לקוח לדפדפן, אך זו לא ארכיטקטורה מומלצת).
- במידע ומדובר במידע רגיש, קשה יותר לאכוף את חשיפת המידע לצרכנים ברזולוציה מדוייקת.
הפצת מידע אודות עדכון
השיטה הזאת עושה push על עדכונים (באמצעות Long Polling, Web Socket או PubSub - נכנה אותם "PLW" לצורך סעיף זה בלבד), אך נשלחת רק הודעה על עצם קיום עדכון בלי תוכנו - יופצו עדכונים בצורה "נוצרה הזמנה חדשה", "מטוס 16548 עודכן", "כלב 314 נמחק" וכד'. הספק מעביר לצרכנים את כל ההודעות (או בפילטור בסיסי קבוע - כמו topic) והצרכן מחליט מה מתוך כל הדברים האלה מעניין אותו, ואז מבצע שאילתה מול הספק כדי לקבל את הנתונים.
באופן כללי, השיטה הזאת היא שילוב של polling עם אחת משיטות PLW, ובהתאם מתאפיינות בשילוב של יתרונותיהן וחסרונותיהן.
יתרונות:
- גמישות מוחלטת בשאילתה ללקוח.
- Scalable - כמו polling (ההנחה היא שההרשמה על עדכונים היא lightweight. אם לא, ל-Web Sockets יש חסרון משמעותי בהיבט הזה).
- קבלת עדכון מיידית - כמו ב-PLW, בתוספת request הוסף להבאת המידע.
- בשילוב טכניקות מ-polling המימוש קל מאד לספק, כאשר כל request עומד בפני עצמו.
אתגרים:
- פחות סטנדרטי מפתרונות אחרים.
- כמו בכל שיטות PLW, אין אטומיות בין עדכון לבין הודעה על העדכון אז קשה להבטיח שהצרכנים אכן קיבלו את ההודעה על עדכון שקרה.
- אם עושים Long Polling או Web Socket זה מגיע עם החסרונות שלהם:
- יש צורך בניהול לקוחות
- אם יש מספר instance-ים של הספק (ו\או CQRS בין שרתים), איך ה-instance שמולו בוצע העדכון מודיע עליו ל-instance-ים האחרים כדי שיודיעו לצרכנים שעובדים מולם?
- Back Pressure - צרכן שלא עומד בקצב העדכונים פוגע בספק ומחייב התמודדות של הספק עם מקרים כאלה.
- אם עושים PubSub זה מגיע עם חסרונות שלו:
- Bus מרכזי - נקודת כשל מרכזית
- Bus מרכזי - נקודת תלות מרכזית
- לא נתמך ע"י הדפדפן native
מסקנות
המסקנה המרכזית מכל הנושאים האלה - לכל שיטה יש יתרונות אך היא גם מציבה לא מעט אתגרים. ישנן כמובן גם שיטות נוספות בעולם, אך רובם דומות לאלה המפורטות כאן.
מגוון השיקולים הרחב לא מאפשר לתת כללי אצבע ברורים ומאד תלוי ב-use case ובצוות המממש. ניסיתי לרכז כאן דוגמאות למקרים בהם כדאי להשתמש בשיטות שונות. תוכלו לשים לב שהדוגמאות לא זרות - צריך להיכנס לעומקם של היתרונות והאתגרים כדי לבחור. יש לשים לב - אלה השיקולים עבור ה-use case שהוצג בהתחלה, ל-use case-ים אחרים לחלוטין יש יתרונות ואתגרים נוספים (לדוגמא - WS השיטה היחידה שמאפשרת תקשורת דו כיוונית בזמן אמת בין הצרכן לספק. רק חלק מהשיטות מאפשרות streaming של מידע או העברה יעילה של מדיה) - התעלמנו במכוון ממכלול השיקולים האינסופי כדי להתכנס למשהו מובן.
שיטה | תנאים בהם השיטה מצטיינת |
---|---|
Polling |
|
Long Polling |
|
Inbox Polling |
|
Web Socket / SSE |
|
PubSub |
|
הפצת מידע אודות עדכון |
|
כאמור, הפוסט הזה תפקידו רק לציין את השיקולים השונים שיכולים להשפיע על הבחירה. לעיתים אנשים שונים תופסים אופציה אחת כ-"הכי טובה", "הכי מודרנית" או "הפתרון הנכון" - אבל האמת תמיד מורכבת יותר מאמירות כאלה.
למי שמעוניין להעמיק יותר, אני רוצה להמליץ על הספר הנהדר High Performance Browser Networking מאת Ilya Grigorik, בהקשרנו הפרק Browser APIs and Protocols שמסביר חלק מהטכולוגיות שנזכרו פה לעומק. הוא מציג הסברים משלו לשיקולים לבחור בטכנולוגיה כזאת שלא תמיד זהים לשלי, חלקית בגלל דעה וחלקית בגלל use case שונה.
חושבים ששכחתי משהו? רוצים להעלות עוד שיקולים רלוונטיים? מזמנים לרשום בתגובות!
יום ראשון, 21 באוקטובר 2018
מחיר הטעות - הורדת הסיכון בגרסה חדשה
“Negative results are just what I want. They’re just as valuable to me as positive results. I can never find the thing that does the job best until I find the ones that don’t.” - Thomas A. Edison
כשאנחנו מחליטים לפתח משהו, יש לנו השערה
(או בשמה המדעי - 'היפותיזה'). יכול להיות שאני סטרטאפיסט שחושב שהעולם רוצה
אפליקציה להתאמת בגדים לחתולים, יכול להיות שאני איש UX שחושב שאם כפתור יגדל
ב-5px ויהיה בצבע כתום, אז יותר אנשים ילחצו עליו ויקנו מוצרים, ויכול להיות
שאני מְפַתֵּח שחושב שאם אשנה את הקונפיגורציה של mongoDB להיות
replication.secondaryIndexPrefech: _id_only
אז זה יפתור את
בעיית הביצועים שיש לי ולא יפריע למשתמשים או שאני חושב שהקוד שלי עובד טוב
ועמיד בתקלות רשת.
בכל המקרים האלה, הצגנו היפותיזה ואנחנו מתחילים לפתח בכיוון.
אבל איך נדע אם מה שחשבנו זה אכן נכון? ואיך נדע אם מה שפיתחנו זה טוב? אולי העולם לא רוצה אפליקציה כזו, אולי הכפתור הכתום ירתיע משתמשים או יבלבל עיוורי צבעים, אולי הקונפיגורציה החדשה תיצור נזק ואולי בגרסה החדשה של התוכנה נוסף באג שקורה רק ב-production או רק בשילוב עם מערכת אחרת והוא יגרום לה להתרסק.
יש לנו חוסר ודאות בקבלת ההחלטות שלנו. כדי להתמודד עם זה, יש לנו 2 אפשרויות:
- לשבת ולחשוב הרבה, לעשות ניתוח משתמשים וראיונות, לכנס הרבה אנשים חכמים ולדון בזה, ולהביא "צוות אדום" שיבחן את ההנחות שלנו במטרה לשפר את ההסתברות שההיפותיזה נכונה.
- להתחיל לכתוב קוד, כאשר נתחיל עם החלקים בתכולה שיאפשרו לנו לבחון את ההפיתוזה על מנת להוכיח או להפריך אותה אמפירית עבור המקרה שלנו מהר ככל האפשר, תוך מתן דגש על הגבלת הנזק שנוצר מזה שההיפותיזה מוטעית.
אפשרות 1 דורשת להעסיק הרבה אנשים חכמים בלדבר על אפשרויות, זמן שבו הם לא מייצרים תוצרים בעצמם, לא תורמים מחוכמתם לאנשים אחרים בארגון ודורש גופי הנהלה שיבחרו מי מכל האנשים (המומחים יותר מהם) צודק. התוצאה של זה היא שאנחנו רק מעלים את ההסתברות שההיפותיזה נכונה, אבל כולנו נרגיש טוב כי "התקבלה החלטה אחרי המון עבודה ומחשבה". נשקיע הרבה משאבים בלפתח לפי ההחלטה, נבטיח ללקוחות שלנו כל מיני זמנים, ובסוף נגיע ל-production ו(אולי)נגלה שההשערה שלנו הייתה מוטעית. וגם לא יהיה לנו הרבה מה ללמוד במקרה שזה לא הצליח.
כמובן שהחיסרון של אפשרות 2 הוא הסיכון הגבוה שהיא מציגה.
בניהול סיכונים לומדים ש:
עוצמת הסיכון היא: סיכוי x נזק
כלומר יש שתי אפשרויות להקטין סיכון: הורדת הסיכוי למימוש הסיכון (זה מה שאפשרות 1 מנסה להשיג) או להוריד את הנזק שיגרום מימוש הסיכון - זה מה שאנחנו נתמקד בו באפשרות 2.
הפחתת הנזק
“One of the reasons people stop learning is that they become less and less willing to risk failure” - John W. Gardner
כדי להפחית נזק באופן מועיל, עלינו תחילה להעריך מה כמות הנזק שעשויה להיגרם. את הנזק נעריך ביחידות המידה הרלוונטיות (למשל כסף, אמון, יכולת או שילוב שלהם. נכנה את יחידות המידה כאן DU). כשאנחנו מנסים להעריך כמה נזק יגרם, יש כמה היבטים שלינו לחשוב עליהם:
- כמה אנשים מושפעים מהנזק? זה יכול להיות לקוחות, זה יכול להיות אנשים בתוך הארגון (כמו אנשי תפעול או צוות הפיתוח שמאבד שעות עבודה) או אפילו קבוצות אנשים בארגון (הפסד כספי במחלקה מסויימת, פגיעה במותג)
- כמה נזק יגרם להם (ב-DU)? לעיתים קרובות זהו חישוב סטטיסטי (נזק ממוצע למשתמש למשל)
- במשך כמה זמן הם ימשיכו לספוג נזק (לרוב נזק הוא מצטבר)?
כל אחת מהתשובות האלה משפיעות על כמות הנזק (באופן גס סך הנזק הוא מכפלה שלהם), ולכן אנחנו נוכל לתקוף כל אחד מההיבטים האלה בנפרד כדי להקטין את הכמות הנזק. במקרים רבים אנחנו נקטין את פוטנציאל הנזק עד שהסיכוי להתממשות הסיכון תרד מספיק שנוכל להעלות את הפוטנציאל בחזרה.
נעבור על כל אחד מההיבטים האלה ונראה מהם הדרכים שעולם התוכנה מציע לנו כדי להתמודד איתם. כמו תמיד, מדובר כאן באוסף של כלים שזמין לנו ועלינו לבחור באילו מתוכם נרצה להשתמש. סביר מאד להניח שאם ננסה להשתמש בכולם ביחד יווצר יותר נזק מתועלת...
כמה אנשים מושפעים מהנזק?
אחד הדברים המרכזיים שנרצה לעשות הם להקטין את כמות האנשים שנחשפים לבעיה. זה גם מקטין מאד את הנזק העקיף שיכול להיגרם למערכת (כמו חוסר אמון מצטבר במערכת. במקרה של פיתוח פנימי ב-enterprise, למשל, לעיתים קרובות המשתמשים מחליפים חוויות ובעיות משותפות מעצימות את חוסר האמון).
אחת הדרכים לעשות את זה היא Canary Testing. בקצרה, בשיטה הזו אני מתקין גרסה חדשה לצד הגרסה הקודמת ומפנה אליה משתמשים באופן הדרגתי. למשל, הפנייה של 1% מהמשתמשים ואז השוואת המדדים לגרסה הראשית (אחוז conversion, מדדי ביצועים וכו'). אם המדדים עונים על המצופה, מגדילים את אחוז האנשים בהדרגה עד שמגיעים ל-100%, ואז אפשר להוריד את הגרסה הישנה. האנשים יכולים להיבחר אקראית או לפי מאפיין מסויים (מיקום גיאוגרפי, נרשמים לתוכנית beta וכו'). מדובר בשיטה נפוצה מאד.
העבודה בשיטה כזו מאפשרת לנו להגביל מאד את כמות הנזק בסיכון בזמן שאנחנו מקטינים בהדרגה את הסיכוי להתמשות הסיכון.
שיטה מתקדמת יותר לביצוע התהליך הזו היא Feature Toggle, שמאפשר לנו לשלוט ברזולוציה מאד גבוהה בחשיפה של תכולות חדשות. יש מאמר מפורט שמסביר איך עושים את זה.
כמובן גם A/B Testing יכול לשמש במקרה הזה, אם כי לעיתים קרובות הוא מערב כמויות גדולות של משתמשים ומשמש להגבלת משך זמן הנזק.
כמה נזק יגרם לנפגעים?
לפעמים הנזק שיווצר למספר קטן של אנשים הוא גדול מדי, או שלא ניתן להגביל את השינוי למספר קטן של אנשים. במקרה כזה אפשר להקטין את הנזק לנפגעים.
שיטה אחת שבה נוקטים כדי להקטין את הנזק מכוּנה "mirroring", "shadowing" או "dark launch" (המושג האחרון משמש לפעמים גם ל-feature toggle). השיטה הזאת אומרת שמתקינים את הגרסה החדשה לצד הקודמת, ושֹם proxy בין בלקוחות לבין המערכת שמשכפל את כל התנועה הנכנסת לשתי הגרסאות. את התשובה מהגרסה הקודמת הוא מחזיר ללקוח, והתשובות משתי הגרסאות נשמרות לטובת השוואה שמוודאת שאין שינויים לא צפויים בין הגרסאות. בנוסף משווים בין מדדי ביצועים שונים בין הגרסאות. לאחר זמן מה במצב כזה, הסיכוי להתממשות הסיכון ירד מספיק שאפשר להעביר את הגרסה לשימוש של משתמשים.
שיטה אחרת לטפל במקרים מסויימים היא automatic degrade - במקרה שיכולת מסויימת מפסיקה לתפקד כצפוי, יש כניסה של היכולת ל-"מצב בטוח". זה יכול לקרות על ידי החזרת התעבורה לגרסה הקודמת או ע"י התנהגות ברירת מחדל כלשהי שנכנסת לפעולה (הדוגמה הקלאסית היא מודול המלצות על מוצרים. במקרה שהמודול הזה עובד בצורה לא תקינה, המערכת מכבה אותו וממליצה לכל המשתמשים על מספר מוצרים קבועים מראש)
במשך כמה זמן ימשיך להיגרם נזק?
לפעמים יש נזק חמור שעשוי להיגרם למשתמשים או לארגון אבל אם הוא נמשך זמן קצר יחסית זה לא נורא. במקרה הזה שיטה שמאפשרת לנו להגביל את משך זמן הנזק תהיה מאד יעילה.
הדוגמה הקלאסית לזה היא A/B Testing. בטכניקה הזו אנחנו מחלקים את המשתמשים לקבוצת ניסוי וקבוצת בקרה, כאשר רק קבוצת הניסוי חווה את השינוי. במקרה שהמדדים של קבוצת הניסוי טובים פחות משל קבוצת הבקרה, מחזירים את כל התעבורה לגרסה הקודמת. ההנחה כאן היא שהנזק שנגרם בזמן הניסוי (למשל ירידה בהכנסות) הוא נסבל לזמן קצר.
שיטה אחרת שעוסקת בצד ההיפותזות הטכניות היא Green/Blue Deployment. הרעיון הוא שמתקינים את הגרסה החדשה ("ירוקה") של המערכת במקביל לגרסה הקיימת ("כחולה") ומעבירים אליה את המשתמשים (זאת סביבה שלמה ולא רכיב, ולרוב גם לא מדורג - בניגוד ל-canary testing). במקרה שמשהו לא עובד טוב, בלחיצת כפתור מחזירים את המשתמשים לעבוד על הסביבה הכחולה. זה מאפשר לנו להגביל את הנזק לזמן שלוקח לנו להבין שהוא קורה.
גם שילוב של circute-breaker עם automatic degrade (הנ"ל) , כלומר כשה-circute פתוח מפנים להתנהגות ברירת מחדל. זה מאפשר לזהות את הנזק תוך מספר מועט של פניות ולא לחשוף אותו למשתמשים.
הורדת מחיר הטעות
בארגון מודרני הבאת יכולת מהר ל-production היא הכרחית. ולכן חברות רבות אימצו תפיסות של "embrace the change" או אפילו "fail fast + embrace failure" - הן בחרו להתמקד פחות בהורדת הסיכוי להתממשות הסיכון מראש (תהליך שמעכב הגעה ל-production) ולאמץ גישה שמקטינה את הנזק שמימוש הסיכון עשוי ליצור.
עם זאת, צורת העבודה האג'ילית כן מעודדת עבודה שמקטינה את את הסיכוי להתממשות הסיכון - התקנות תכופות ומפגש מוקדם עם משתמשים. ככל שמתקינים לאחר זמן פיתוח קצר יותר נעשו פחות שינויים בתוכנה ולכן הסיכון שמביאה גרסה חדשה הוא נמוך יותר. לכן שילוב של עבודה אג'ילית עם הטכניקות שהזכרנו להקטנת מחיר הטעות מביאות לסיכון נמוך במיוחד בהבאת גרסא חדשה ל-production. אחד המאמרים הכי טובים שיצאו על הנושא הזה ואיך אפשר ליישם עלייה ל-production שבאמת עוזרת לנו לאמת היפותזות הוא Making sense of MVP של Henrik Kniberg.
הערות לסיום:
- התעלמתי כאן מהנושא של בדיקות. לבדיקות חלק חשוב בהורדת הסיכוי להתממשות מגוון רחב של סיכונים, בכל הקשור להתנהגות המערכת. עם זאת הבדיקות נותנות ערך מאד נמוך לדברים הקשורים להתנהגות משתמשים, כגון איך הם יגיבו לשינויים פונקציונאליים.
- כל זה לא אומר שלא צריך לחשוב בכלל! יש מקומות, בייחוד כשהסיכון נמוך או מדובר במשהו קטן יחסית, שבהם אנחנו מרגישים שיותר יעיל לחשוב על הדברים ולהוריד את הסיכוי להתממשות הסיכון מאשר להשקיע בטכניקה להורדת הסיכוי.
- עסקנו בהורדת הסיכון בעליה ל-production. יש נושא נפרד, יותר סובייקטיבי ואמוציונאלי, שעוסק בהורדת הסיכון לשינויים עתידיים. שם יש שתי אפשרויות דומות מאד - לעשות הרבה מחשבה upfront או ללכת על מה שצריך כרגע ולהתמקד בטכניקות שמגבילות את הנזק בשינוי בלתי צפוי. לא ניכנס לנושא הזה עכשיו, אבל למי שמעוניין יש הרצה נחמדה של ליאור בר-און בנושא.
יום רביעי, 5 באפריל 2017
The complexity that is hidden in Micro Services and Event Sourcing
ארכיטקטורת Microservices צוברת תאוצה, ועם עליית הפופולריות תמיד גם מגיע השלב שבו הרבה אנשים מתחילים להאמין שארכיטקטורה זו היא היא המפתח לפתרון כל צרות העולם, בלי להבין את המורכבות שמגיעה עם הארכיטקטורה הזו.
בהרצאה קצרה זו Satyajit Ranjeev מציג מהניסיון שלו חלק מהמורכבויות שמגיעות עם ארכיטקטורה זו. ההרצאה המלאה.
איך מפרקים מערכת לשירותים?
התשובה הנכונה: ע"פ Bounded Context. התשובה הלא נכונה (שהם ניסו): על פי ישויות. חלוקה כזו מובילה למשל למערכת בלוגים שבה יש שירות של פוסטים, שירות הודעות, שירות תגובות וכו', כאשר לאו דווקא החלוקה הזו היא חלוקה שמונעת תלויות.
הדבר השני שהם לא הבינו בהתחלה היה "מה זה מיקרו". הם הגיעו די מהר ל-73 שירותים לצוות של 5 אנשים, וזה יצר תלויות, מורכבויות וצורך לשינויים רבים. למצוא את הגבולות הטבעיים של ה-context זה לא קל.
Event Sourcing
החברה שהמרצה עובד בה עוסקת בהעברת כספים. הם בחרו להשתמש ב-Kafaka כי הוא מבטיח שכל שדר מגיע לפחות פעם אחת. עדיין בערך 1% מהשדרים מגיע פעמיים, וכדי להבטיח Idempotency צריך לשמור את השדרים. הם בחרו בשביל זה להשתמש ב-event sourcing, וזה היה להם מאד נחמד כי אפשר לראות בדיוק מה קרה וגם לשחזר מידע במקרה של בעייה. איפה שומרים את כל ה-event-ים? הם בחרו להשתמש ב-Kafka עצמו כי הוא מבטיח סדר (per partition - זה הכריח אותם לעבוד רק עם partition אחד), וכל פעם ששירות עולה הוא בונה מחדש את המצב. באופן צפוי, היה topic אחד שהגיע ל-9 מליון הודעות ולקח לשירות המון זמן לעלות. חשוב גם לזכור שב-Kafka אי אפשר למחוק מידע מ-topic (רק מההתחלה אבל לא מהאמצע), ולא בטוח שהוא המוצר הכי מתאים ל-Event Sourcing.
כדי להתמודד עם עלייה ארוכה במקרה של topic-ים מאד גדולים, הם החליטו ליצור snapshots. הם שקלו שיטות שונות והחליטו ללכת על DB לכל שירות, וזה אכן פתר את בעיית האיטיות בעלייה. הם שמרו ב-PostgreS את ה-snapshots שרלוונטיים לשירות, ומה הנקודה האחרונה שנצפתה ב-Kafka.
איך עושים אוטומציה לכל השירותים האלה?
הם עבדו עם docker ו-Fleet.
אחד הדברים החשובים להבין כשנכנסים ל-Microservices זה שיש לזה עלות גבוהה ב-operations. נדרש להשקיע הרבה מאמץ רק כדי להשאיר את המערכת רצה. הם ידעו את זה מראש, אבל לצוות קטן זו עלות מאוד גבוהה - לטפל בסביבה, לטפל בניטור...
יש מאמר של Martin Fowler שאומר You must be this tall to use microservices. מומלץ לקרוא לפני שמתחילים.
עוד משהו מעניין שהם עשו זה להוציא מה-JIRA ספירה של Task-ים שעוסקים בתשתיות הריצה. זה נראה ככה:
עוד נקודות מעניינות מה-Q&A
- הם החליטו לאחד בחזרה חלק מהשירותים, כי עכשיו הם מבינים יותר טוב מהם ה-Boundries. מהניסיון, זה קשה להבין אותם בהתחלה, ולכן גם המרצה הזה מציע להתחיל מ-monolith ואז לפרק אותו.
- "מה עושים עם שינוי בסכמה של event (ב-event sourcing)?" - הם רק הוסיפו שדות לאירועים. עוד משהו חשוב, זה שהם ניסו לשמור על האירועים כמה שיותר קטנים - אם מגיע עדכון גדול מהמשתמש מפרקים אותו להרבה אירועים קטנים לכל שדה בנפרד.
- "אילו בעיות חוויתם עם Zoo Keeper?" (בעקבות הערה שהמרצה אמר במהלך ההרצאה) - הוא עובד די טוב ב-Production אבל היה קשה להריץ אותו בסביבת הפיתוח והוא נפל כל הזמן בסביבה זו.
- "האם לדעתך 'שווה' ללכת ל-Event Sourcing?" - לא תמיד. הם למדו המון, אבל זה היה כואב.
- "איך עושים אנליזה על המידע כאשר הוא מחולק בין ה-DB השונים?" - הם עשו שירות אחד שעושה אגריגציה למידע.
יום שני, 6 במרץ 2017
Workshop - Resilient software design in theory & practice
ביום שלפני תחילת הכנס עצמו היה יום סדנאות. אני הצטרפתי לסדנא בהדרכת Uwe Friedrichsen שעסקה בנושא Resilient software design in theory & practice. סדנא מאד מעניינת. אני מביא פה כמובן רק תקציר של הנקודות החשובות, אפשר לשמוע תקציר של הסדנא ע"י Uwe עצמו בהרצאה שהוא נתן למחרת בכנס (שקפים), ומי שיצליח להשתלב בסדנא שלו בהמשך - מומלץ!
אנחנו במהלך הסדנא |
חלק 1 - What is that "resilience" thing?
אנחנו כותבים תוכנה כדי להביא ערך עסקי. אבל היא מביאה ערך אמיתי רק עם התוכנה רצה ב-production ועובדת. לכן לעובדה שתוכנה רצה ועובדת ב-production יש ערך עסקי אמיתי, שהוא לא תמיד גדול יותר או קטן יותר מדברים אחרים, ובאחריות ה-product owner להעריך אותו ולשלב משימות resilience ב-backlog שלו.
מכיוון שבלי שתוכנה רצה ועובדת היא לא מביאה ערך עסקי, הנושא של זמינות (Availablilty) הוא נושא חשוב מאד בתוכנה. אבל מהי בכלל זמינות? איך מודדים אותה?
יש נוסחא פשוטה:
Availibilty= | MTTF |
MTTF+MTTR |
(MTTF=Mean Time To Failure, MTTR=Mean Time To Recovery)
כלומר הזמן שהמערכת רצה בצורה תקינה חלקי כמות הזמן הכוללת.
כדי להגדיל את הזמינות (לגרום לנוסחא לשאוף ל-1) יש לנו שתי אפשרויות: להגדיל את ה-MTTF (למנוע כשלים) או להקטין את ה-MTTR (לשפר את היכולת שלנו להתמודד עם כשלים). קלאסית, תמיד עסקנו בהגדלת ה-MTTF ע"י שיפורי תהליכים והשקעה בתשתיות. אבל העולם מתקדם למערכות מבוזרות ולמערכות התלויות במידע מבחוץ:
A distributed system is on in which a failure of a computer you didn't even know existed can render your own computer unusable. - Leslie Lamport
במערכות כאלה מניעת כשלים היא הרבה יותר קשה, כיוון שכשלים ברמת התשתית תמיד קורים. מחקר של מייקרוסופט שבדק את כשלי החומרה ב-Data Center שלהם מדבר על חציון של 40.8 כשלי network link ליום, כאשר חציון הזמן לתיקון הכשל עומד על 5 דקות. החציון המוערך של כמות הפקטות שעובדות בכל כשל כזה הוא כ-59,000. מחקר אחר מראה שגם בין אתרים של אמזון (שמצטיינים ב-latency נמוך) יש לפעמים latency חציוני של למעלה מ-350 מילישניות.
לכן האופציה השנייה, שבה אנחנו מתמקדים במערכות מבוזרות, היא הקטנת ה-MTTR, כלומר ליצור מערכת עמידה [עמידות = היכולת של מערכת להתמודד עם מצבים לא צפויים בלי שהמשתמש מרגיש (במקרה הטוב) או תוך רידוד יכולות מבוקר (graceful, במקרה הרע)]. או בניסוח של משפט המפתח בהרצאה:
Do not try to avoid failures. Embrace them.
כשאנחנו מדברים על מצבים בלתי צפויים בתוכנה, אנחנו מדברים על 3 סוגים (או 3 שלבים) של בעיות:
- Fault (תקלה) - קיימת איזושהי בעייה במערכת
- Error (שגיאה) - הבעייה "הופעלה", כלומר היא הגיעה למצב שבו היא משפיעה על המערכת
- Failure (כשל) - הבעייה גרמה לפגיע בזמינות המערכת. חשוב להגיד שאנחנו לאו דווקא מדברים על נפילת מערכת - זה אירוע שקל יחסית להתמודד איתו. כשל יכול לבוא לידי ביטוי גם כתגובה לא יציבה (חלק מהבקשות נכשלות), בעייה של איטיות (ובמערכות מבוזרות זה יותר קשה), בעיות של תגובות שגויות (למשל בעיות קונסיסטנטיות), והכי גרוע - לפעמים שגיאה ככה ולפעמים ככה (מכונה Byzantine Failure ומחוץ לתחום של הסדנא הזאת).
כלומר, מה שאנחנו מנסים לעשות הוא למנוע מ-Fault להפוך ל-Error ובעיקר למנוע מ-Error להפוך ל-Failure.
עוד אזהרה אחת שחשוב להגיד: צריך להימנע מ-"מלכודת ה-100% זמינות". זה מאד קל ל-product owner להגיד: "זמינות זה עניין טכני באחריות המפתחים. אתם צריכים לעשות מה שצריך כדי שהמערכת תהיה תמיד זמינה". אבל זו גישה לא נכונה - האם עכשיו עדיף שלא נפתח שום יכולת במערכת כי נשקיע רק בזמינות? או לחילופין, שניתן מערכת עם המון יכולות אבל שלא עובדת חצי מהזמן?
לזמינות של המערכת יש ערך עסקי. התגובה של המערכת לכשלים היא החלטה עסקית. וצריך לתעדף את התגובות האלה ביחס ליכולות אחרות של המערכת. צריך להכניס למערכת מדידה של אובדן הכנסות (או משאב אחר) עקב כשלים במערכת או איטיות במערכת.
חלק 2 - Designing for resilience
כאמור, במערכת מבוזרת אנחנו נמצאים בסביבה מבוזרת, ולכן לא ניתן למנוע בעיות לחלוטין. אנחנו יכולים רק להוריד את ההסתברות שלהן וההשפעה שלהן. לכן נראה עכשיו כלים כדי לתכנן מערכת עמידה.
הערה כללית לסדנא: כל הדברים האלה הם לא חוקים, אלא נקודות שצריכות להיות במחשבות שלנו. צריך להתאים את הפתרונות לעולם התוכן שלנו, לנסות, למדוד, ליצור feedback loop שיאפשר לנו להבין מה עובד ומה רלוונטי, ולהבין מה מתאים לנו ולמה.
כשאנחנו ניגשים לתכנן מערכת עמידה, יש כמה שלבים שאנחנו רוצים לעבור:
- לקבל החלטות ליבה לגבי מבנה המערכת
- לזהות שגיאות (error). אחרי שזיהינו אותן ניתן להתמודד איתם באחת משתי דרכים:
- להתאושש מהן
- להתמודד עם קיומן
- בהתאם להתמודדות שבחרנו, נבחר בפתרון שיעזור לנו למנוע מהשגיאה להפוך לכשל.
- נמצא פתרונות שיעזרו לנו למנוע שגיאות אחרות.
החלטות ליבה
ישנן שתי החלטות ליבה שעלינו לקבל כאשר אנחנו מתכננים מערכת: בידוד רכיבים (isolation) ובחירת שיטת תקשורת.
בידוד רכיבים (isolation)
בהקשרי עמידות המערכת אסור שהמערכת תטופל כיחידה אחת - צריך לחלק את המערכת לאזורים מבודדים שבמקרה של כישלון כל אחד מהם כושל בנפרד. חלוקה ל-Microservices לא מספיקה אם השירותים השונים תלויים אחד בשני ויגרמו לכשל אחד של השני.
כאשר מתכננים את החלוקה לרכיבים של מערכת יוצרים משהו שנקרא bulkheads: בחירה של המחיצות ואופן החלוקה של המערכת. אפשר לעשות זאת ע"י חלוקה ל-Microservices, או על ידי חלוקה ל-Actors למשל.
אבל לעשות חלוקה שכזאת זה קשה! למה? כי אם שירות A תלוי בשירות B, שירות B עדיין יכול למנוע משירות A לעבוד (כן, אפילו אם שירות A משתמש ב-circute breaker). ואם גם שירות B תלוי בשירותים אחרים, והם תלויים בשירותים נוספים, אז גם אם כל אחד זמין ב-99.99999% מהזמן הזמינות של A יכולה לרדת משמעותית, אפילו ל-75%. להכניס את כל השירותים לאותו bulkhead רק יחזיר אותנו לבעיות המונוליט. גם שימוש בכלים אחרים שאנחנו מכירים מהנדסת קוד, כגון DRY, פירוק לפונקציונאליות, שכבות וכד' לא יעזרו לנו.
מה כן יעזרו לנו? העקרונות הבסיסיים של חלוקה לרכיבים: High Cohesion, Low Coupling יחד עם speration of concerns הם קריטיים כשחוצים את גבולות ה-process. זהו נושא שאנחנו עדיין לומדים אותו בתעשייה ולא התקדמנו הרבה מאד בתחום (למשל מאז המאמר הזה מ-1972).
איך מתחילים לעשות חלוקה שכזאת?
- צריך להבין את הגבולות הארגוניים - הגבולות הארגוניים לעיתים קרובות מסודרים סביב צורת החשיבה וצורת הפעולה המשתנה יחד.
- צריך להבין מקרים ולהבין את זרימת המידע - למשל כדי לוודא שאין יותר מדי hops בתהליך.
- צריך להבין מהם הדמיינים הפונקציונאליים (DDD. סיכום סדנא בנושא מהכנס).
- למצוא איזורים שבהם אפשר לעשות שינויים בלתי תלויים.
- לא להתחיל עם מודל המידע! - מה שיוצר תלות זה התנהגות, לא מבניות.
אחרי זה צריך למצוא את הדרך הקצרה ביותר לענות לבקשות - יותר מדי hops בדרך מקטינים את הזמינות ומגדילים את ה-delay, ולכן אנחנו נחפש תמיד את הדרך הקצרה ביותר להחזיר את התשובה ונשאף להקטין את כמות הקריאות (RMI) שאנחנו עושים בתהליך. את העיקרון הזה חשוב לאזן מול המשיכה לשבור את ה-seperation of concerns ולחזור למונוליט כדי להקטין את כמות הקריאות - צריך לאזן בין שני הדברים האלה. Cache-ים יכולים לעזור לנו לפתור חלק מהבעיות האלה, אבל חשוב לזכור ש-cache לא יתקן עיצוב לא טוב והוא מביא איתו מורכבויות משלו.
עוד משהו שחשוב בשביל לעשות isolation טוב: לוותר על Reusability מחוץ לגבולות ה-process. גם אם אפשר להשתמש בשירות מסויים בכל מיני מקומות, זה לא אומר שכדאי לעשות את זה. השימוש החוזר בשירות (לעומת האלטרנטיבה של, לדוגמא, לשתף jar) גורם לעוד hop בתהליך ולכן פוגע בזמינות. מלבד זאת, הוא גם מעודד עיצוב לא טוב.
Uwe סיפר שיש סטטיסטיקות של שימוש חוזר ברכיבים, ובסוף הממוצע הוא שימוש של 1.1 פעמים בשירות, וזה באמת לא משתלם וזו אחת מהסיבות לקריסת הרעיון של SOA. אומרים שלעשות משהו כתשתית זה עולה פי 9: פי 3 מאמץ למימוש יציב בכל המקרים, ושוב פי 3 כדי ליצור API טוב, תחזוקתי ומוגן. עדיף לוותר על העקרון של reusability, ובמקומו לאמץ רעיון אחר: replacability - אפשר להחליף את המימוש בצורה נקייה בכל נקודה. באופן לא מפתיע, replacability טוב מגיע איפה שיש seperation of concerns טוב.
שיטת תקשורת
החלטת הליבה השנייה היא לבחור את צורת התקשורת במערכת: request-replay (סינכורני) \ messaging (כמו ב-AKKA למשל) \ events וכו'. זאת החלטה חשובה שמשפיעה מאד על העיצוב, ולעיתים קרובות לא מייחסים לה חשיבות מספקת:
- בחירה במודל סינכרוני תגרום לנו לעשות חלוקה של המערכת לרוחב לעומת תקשורת אסינכרונית שתגרום לנו לעשות חלוקה לאורך.
- מודל בקשה-תשובה יוצר תלות של השולח במקבל, מודל של אירועים יוצר תלות של המקבל בשולח (או של שניהם ב-broker אם משתמשים באחד).
- מודל בקשה-תשובה מכיל רעיון טרנזקציוני מובנה, לעומת צורך בפיקוח חיצוני בתקשורת אסינכרונית.
- כל נושא הטיפול בשגיאות נעשה בצורה שונה.
- ועוד...
זיהוי בעיות
אחרי שקיבלנו את החלטות הליבה שלנו, הגיע הזמן למנוע משגיאות להפוך לכשלים. אבל כדי לעשות את זה עלינו לזהות ששגיאה קרתה. ניתן לבדוק את זה באחת מכמה דרכים:
Timeouts
אם בקשה לוקחת יותר מדי זמן, יש בעייה. עלינו לזהות שהזמן הסביר לבקשה כזאת פקע. זה נכון הן בשימוש סינכרוני והן בשימוש אסינכרוני, וקריטי יותר בסינכרוני (כי אחרת thread עלול להיתקע לנצח). רוב סביבות התכנות מכילות יכולת מובנה להתמודדות עם timeout.
במקרה שאירע timeout, ישנם כל מיני דברים שאפשר לעשות: להחזיר למשתמש שגיאה, להגיד למשתמש שהבקשה "בטיפול", להכשיל את הפעולה ולהעביר אותה לתור שיטופל אסינכרונית (ואולי לאפשר למשתמש לעקוב אחרי ההתקדמות).
Circut breaker
הדפוס הזה הוצג לראשונה בספר "!Release It" של M. Nygard, וכנראה הוא הדפוס שמוזכר לעיתים הקרובות ביותר בהקשר ל-resilience.
הדפוס הזה הוא בעצם הרחבה של דפוס ה-timeout, שאומר שאם נכשלתי בפנייה לשירות מסויים בפעמים האחרונות, סיכוי סביר שאני אכשל שוב. לכן עדיף שאני אחסוך זמן של המשתמש ולא אנסה שוב אלא אבצע את הטיפול בכשלון באופן מיידי. פעם בכמה זמן אני אנסה שוב לראות עם השירות כן מצליח לטפל בשגיאות ורק אז אחזור לעבוד מולו.
מדובר בדפוס פשוט, ניתן לממש אותו לבד או להשתמש בתשתית כדי לחסוך את בעיות ה-thread safety (כמו לדוגמא Hystrix של Netflix שמוצעת כחלק מה-OSS שלהם. היא באה גם עם Dashboard מובנה שמציג את סטטוס ה-breakers אך לא נמצא בפיתוח יותר. Uwe גם ממליץ מאד לקרוא את הקוד של Hystrix, יש הרבה מה ללמוד ממנו על בנייה של קוד cuncurrent בצורה טובה).
Acknowledgment
הרעיון: יש מישהו שאחראי על התהליך ומוודא שהוא מסתיים. יוצרים מנהל תהליך שמוודא שהתהליך שהתחיל מהמשתמש עובר בתחנות שציפינו להן, ואם לא הוא מכריז על timeout. אפשר גם שיהיו מנהלים שונים שמבקרים תהליכים שונים בתהליך. הדפוס הזה מאפשר בקרה על ביצוע של התהליך אסינכרוני.
צריך לשים לב לא לעשות בקרה על כל הנקודות בדרך - זה הופך את התהליך האסינכרוני להתנהג כמו סינכרוני. מספיק לבדוק תחנות עיקריות בדרך. כמו כן נדרש איזון בין לקחת זמן קצר מדי ולקבל התראות שווא לבין לקחת זמן ארוך מדי ואז הזמינות יורדת.
בדיקה שגרתית
בדיקה מתוזמנת לבדיקת בריאות של רכיבים ברשת. יש לזה שני חלקים: health check שבודק זמינות, וטרנזקציות סינכרוניות שלא משפיעות על המידע האמתי (למשל להשתמש במשתמש פיקטיבי או ישויות ספציפיות לצורך הבדיקה) שמפעילים מול הרכיבים כדי לוודא שהם מבצעים את עבודתם כהלכה.
ניטור
להריץ מערכת בלי ניטור זה אמיץ. להריץ מערכת מבוזרת בלי ניטור זה טיפשי.
גם בהקשרי ניטור, כדי להקטין את ה-MTTR אנחנו צריכים לתת מקסימום מענה אוטומטי למקרים ורק ליידע אנשים שקרה אירוע, אבל בלי שתומך צריך להתערב (למשל - restart לשרתים).
זה אומר שרמות הלוג שנשתמש בהם:
- Debug - מידע למפתח
- Info - הייתה בעייה אבל המערכת התמודדה
- Error - יש צורך בהתערבות תומך
הניטור יכול להיות חלק מהמערכת, זה מאפשר לו לעשות דברים יותר מורכבים. מצד שני, הוא יכול להיות חיצוני למערכת, זה עושה אותו עמיד יותר לנפילות במערכת. צריך להפעיל שיקול דעת ולבחור באופציה המתאימה לנו.
טיפול בשגיאות
יופי, אני יודע לזהות שגיאות. עכשיו הגיע הזמן לטפל בהן כדי שלא יהפכו לכשלים. יש לנו שתי אפשרויות לעשות את זה: להתאושש מהן, או להתמודד איתן.
התאוששות
איך מתאוששים משגיאה שקרתה? הנה כמה אפשרויות:
- נסיון חוזר - מנגנון ההתאוששות הכי בסיסי: לנסות שוב. צריך להגביל את מספר הנסיונות שמנסים, ומומלץ להגביל לפעם אחת נוספת. כדאי לשים לב גם שלעיתים קרובות זה דורש idempotency מהרכיבים ברשת.
- חזרה לאחור - חזרה לאחור למצב יציב קודם (בתוך השירות). יוצרים פעם בכמה זמן checkpoint או safepoint (שההבדל ביניהן מעבר לתחום ההרצאה) שאליהם אפשר לחזור אחרי שגיאה ולהתחיל ממקום שאנחנו יודעים שהוא בסדר, ולנסות להריץ את הפעולה מחדש.
גם כאן חשוב להגביל את הכמות. - "בחזרה לעתיד" - לדלג על הפעולה (Roll-Forward) - להתקדם לנקודה בריצה שהיא מעבר לפעולה שנכשלה ולהמשיך טיפול. זאת החלטה עסקית האם זה רעיון טוב או לא לעשות דבר כזה - לדוגמא אם משתמש עשה הזמנה באתר ולא הצלחנו לאשר תשלום, עדיף להכשיל את הפעולה; אבל אם לא הצלחנו להודיע למחסן להוציא משלוח ייתכן שעדיף להמשיך את התהליך ולתקן את זה יותר מאוחר. ניתן להשתמש בטכניקה הזאת גם רק על חלק מהפעולה. גם כאן ניתן להשתמש ב-checkpoints ו-safepoints.
- איתחול - כשכל השאר נכשל, אפשר לאתחל את השירות מחדש. אפשר גם לאתחל מידע במקרה הצורך. לפעמים גם התחברות מחדש למקור מידע חיצוני זה מספיק.
- Failover - העברת אחריות - אם אני לא מצליח לבצע פעולה מסויימת, אולי מישהו אחר יצליח. הדוגמא הקלאסית היא שימוש ב-Load Balancer כדי להעביר את הבקשה ל-instance אחר. זה בדרך כלל דורש יתירות בשירותים, מה שאומר שאנחנו משקיעים משאבים כדי להשיג זמינות (שוב, החלטה עסקית).
התמודדות
לפעמים לא חייבים להתאושש מהשגיאה, ואפשר פשוט להמשיך את הפעולה בנוכחות השגיאה עד שתיפתר. גם כאן יש כמה שיטות לעשות את זה:
- Fallback - אם יש שגיאה בפעולה מסויימת, אני יכול להחליט לבצע במקומה תרחיש אלטרנטיבי אחר. זה הבסיס לרוב שיטות ההתמודדות. דוגמאות: ברירת מחדל לתשובות אם השירות שאני תלוי בו לא זמין, להתעלם ולהמשיך הלאה (פגיעה מבוקרת בתהליך).
- תור משאבים - (בלתי מוגבל) מיועד בעיקר לשימוש במקרה של בעיית ביצועים ומשאבים זמניים. הבעייה עם תור משאבים זה שהוא יכול לתפוח ואז ליצור latency ארוך מדי. לכן כדאי לשלב איתו את השניים הבאים.
- Backpressure - כאשר השירות מקבל בקשות רבות מכדי שהוא יכול למלא, הוא מודיע לאלה שקוראים לו להנמיך את הקצב. הדוגמא הכי בסיסית היא החזרה של סטטוס 503 לכל בקשה נוספת. באסטרטגיה הזאת צריך לשים לב שנדרש ערוץ חוזר מהשירות אל הקוראים לו, מה שלא תמיד נכון במודל אסינכרוני.
- תור חסום - דומה לתור משאבים אבל בעל גודל מוגבל. כאשר התור מלא הודעות חדשות מקבלות Backpressure או נזרקות, או אפשרות אחרת (לדוגמא זורקים את הישנה ביותר).
- חלוקת עומסים - יוצרים עוד מופע של השרת או המשאב כדי להתמודד עם העומס. חשוב להקטין ככל האפשר את הסנכרון הנדרש בין המופעים של המשאב.
מניעת שגיאות
חוץ מאשר לזהות שגיאות ואז להתמודד איתן, אפשר גם לעשות פעילות אקטיבית למניעת תקלות.
אי אפשר למנוע ששגיאות יקרו, אפשר להקטין את ההסתברות שיקרה כשל. גם לחברות הגדולות ביותר תקלות קורות (לדוגמא, בשבוע שעבר שירותי הענן של אמזון קרסו למשך 4 שעות בגלל טעות אנוש). אבל אנחנו יכולים להקטין את ההסתברות שזה יקרה.
הדרך הכי טובה היא טיפול שגרתי - טיפול 10,000 לרכב לא מונע תקלות, אבל מקטין מאד את ההסתברות שתקלה תקרה. באותה צורה, במערכת אנחנו צריכים להשקיע זמן במניעת תקלות שאפשר למנוע, בגילוי תקלות ושגיאות שקורות ב-production ותיקונן, ומדי פעם לחזור ולאזן כמה משאבים אנחנו משקיעים בעמידות המערכת למול שאר הצרכים. כדי לעשות את זה אנחנו חייבים ליצור לעצמנו יכולת לדעת מה קורה ב-production, מתי שגיאות קורות וכו'.
עוד נושאים
עוד כמה נושאים ששווה לחשוב עליהם בהקשר לבניית מערכת עמידה:
- יתירות - זה הבסיס להרבה מדרכי ההתמודדות עם שגיאות. אבל בד"כ אנחנו מתמקדים רק ביתירות למקרה של נפילה - ניתן להחיל את השיטה הזאת גם על מקרים אחרים של שגיאות.
- idempotency - קריאה לאותה מתודה מספר פעמים ברצף תביא לאותה תוצאה, כלומר שליחה כפולה של בקשה לא תשפיע. מאד קשה לייצר מערכות מבוזרות בלי זה - זה עוזר להצמיד צימודים בין הרכיבים ולהתמודד עם שגיאות - אפשר פשוט להפעיל את הקריאה שוב בלי חשש.
דרך אחת לעשות את זה היא לתת unique request token לבקשות שדרכו אני מוודא שלא טיפלתי כבר בבקשה (זה לא תמיד קל - בעיות consisteny, ניקוי עם GC וכד'). - Stateless - קשה לתחזק state במערכת מבוזרת (CAP), לכן עדיף לוותר עליו. אבל מאחר שלרוב צריך הקשר לפנייה, אז הפתרון הוא להעביר אותו 'למעלה' או 'למטה' - אם הוא קטן, להעביר אותו ללקוח ולבקש ממנו תמיד לשלוח אותו (לדוגמא עם cookie), או להעביר אותו לתוך ה-data store.
- אסקלציה - במערכת מבוזרת ללקוח לא אכפת איזה חלק במערכת לא עובד. עדיף לשים שירות עליון שיקבל כל את כל התקלה ויחזיר משהו ללקוח. לעיתים קרובות זה נעשה בהיררכיה - כמה רמות שמרכזות שגיאות מהרמות מתחתיהן. עדיף להפריד את קוד הטיפול בתקלות מהקוד שעושה את העיבוד במערכת - האסקלציה יכולה לעשות כלפי שירות ייעודי ולאו דוקא דרך השירותים שדרכם הגענו לקריאה הנוכחית.
איך משתמשים בכל התבניות האלה?!
איך מחברים יחד את כל הדברים האלה?
קודם כל, מה לא - אנחנו לא רוצים להשתמש בכל התבניות האלה. התבניות האלה הן אפשרויות, לא חובה. עלינו לבחור באלו שמתאימות לנו, וגם לא לבחור ביותר מדי - כל תבנית מוסיפה מורכבות, וכל תבנית עולה לנו כסף בסוף. יש דוגמא לְמה ש-Netflix בחרו לממש ולְ-מה ש-Erlang בחרו לממש. בדוגמא הזו למשל, Erlang הצליחו להגיע ל-5 תשיעיות של זמינות.
חלק 3 - תרגיל עיצוב
בשלב הזה עברנו בסדנא לתרגיל מעשי בשני שלבים. התרגיל היה מערכת של חנות אינטרנט, שיש לה הזמנות, משלוחים, וקופונים. התחלקנו ל-3 קבוצות, שכל אחת עבדה על שיטת תקשורת אחרת: סינכרונית, מסרים ואירועים. אני הייתי בקבוצה שתרגלה messaging, הנה התוצאה אליה הקבוצה שלנו הגיעה:
מסקנות עיקריות מהעבודה על התרגיל:
- בפתרון מבוסס event-ים:
- היה שימוש ב-health check כדי לדעת אם המאזינים לאירועים תקינים
- ניטור שבודק את האירועים שעוברים במערכת
- ה-UI (או הרכיב שמתקשר איתו) גם כן שומע אירועים ויכול לדווח ללקוח מה הסטטוס, או בהיעדרם משהו בינתיים.
- כדאי לעשות את השירותים idempotent.
- בפתרון מבוסס תקשורת אסינכורנית (messaging):
- לדברים שיכולים לקרות יותר מאוחר (למשל - הזמנה מהמחסן, עדכון השימוש בקופון) כדאי להשתמש בתורים לא מוגבלים.
- לחליפין, במקרה של כשלון בעדכון דברים שיכולים לקרות אח"כ אפשר ליצור שירות רקע שעובר על הנתונים ומתקן אנומליות (למשל עובר על רשימת התשלומים ומחפש הזמנות שחסרות בשירות המחסן).
- אפשר לחלק את התגובה שלקוח מחכה לה ל-2: תגובה עם timeout קצר שבמקרה שעבר הזמן מחזיר ללקוח דף שמאפשר לו לבדוק את הסטטוס של התהליך, וטיפול עם timout ארוך שאם נכשל מחזיר כישלון כללי.
- ל-AKKA יש יכולות להתמודד עם וידוא של תהליכים אסינכרונים (נקרא supervision).
חלק 4 - לסיום - מהתיאוריה לפרקטיקה
ישנן עוד המון תבניות.
יש מאמר (קשה) בשם building on quicksand שמדבר על מערכות מבוזרות. הרעיון בגדול הוא שכמו שבחנות קפה בהסתברות מסויימת ישכחו להביא לך את ההזמנה ואז יתנצלו, ההבטחה במערכת מבוזרת היא שאני מבטיח לעשות משהו ומבטיח לנסות ולקיים את ההבטחה.
תהליך העיצוב
- מקבלים את החלטות הליבה.
- חלוקה לרכיבים (bulkheads). זה המקום להשקיע! זה 50% מהעבודה ומאד קשה. חשוב לשים לב שאנחנו מחפשים loose coupling ברמה העסקית ולא ברמה הטכנית.
- לבחור טכניקות לזיהוי, התאוששות והתמודדות עם שגיאות.
- לממש.
- לעלות ל-production.
- למדוד את התנהגות המערכת! "אל תנחש, תדע".
- ללמוד מהמדידות.
- להעריך באילו תבניות כדאי לבחור כדי לפתור בעיות: עלות מול תועלת. יותר זה לא תמיד יותר טוב...
- להתחיל את התהליך מהתחלה ולעשות review להחלטות שעשינו.
Adopting Resilient Software
איך גורמים לפיתוח כזה להפוך לחלק מהארגון שלנו? Uwe הציע שלושה שלבים:
- ליצור מודעות
- צריך לוודא שהאנשים העסקיים מבינים את החשיבות - למה להשקיע בזה
- ברמת המפתחים:
- ליצור feedback loop שמאפשר הבנה של המערכת
- ליצור devops
- ליצור Site Reliability Engineering - מערכת של גוגל שעוזרת לוודא מה יחס ההשקעה בין היכולות החדשות לבין שיפור הקיים
- ... או ליצור שיטה משלנו - העיקר שיש לנו feedback loop כדי שנתבסס על נתונים אמיתיים
- לבנות יכולת - ידע (הכשרה, hackathon-ים שבהם אנשים מתנסים), כלים ונסיון (לעשות!)
- לבנות עמידות רצופה - לתרגל את עמידות המערכת כדי שלא נופתע בפעם הראשונה ששגיאה באמת קוראת
- להכניס שגיאות (כמו ה-Simian Army של Netflix) ולראות איך המערכת מתנהגת
- ליצור תרגילים מתוכננים שדורשים התערבות תומך. זה מתרגל את התומכים והם ידעו להתמודד עם מקרים. עד לרגע התרגיל רק מנהל התרגיל יודע את התסריט.
רשימת קריאה
- Release it!, Michael T. Nygard
- Andrew Tanenbaum, Marten van Steen, Distributed Systems – Principles and Paradigms
- מצגת אחרת של Uwe על תבניות לבניית מערכות עמידות
נקודות מעניינות מה-Q&A בסוף הסדנא
- ככל שהמערכת קטנה יותר, יותר קל לעשות סביבת בדיקות מלאה. ככל שהיא גדולה יותר זה קשה יותר. Netflix, למשל, לא יקבלו הרבה מסביבת בדיקות כי אין להם יכולת להרים כמות גדולה כל-כך של משאבים לבדיקות ולדמות כל כך הרבה תעבורה, ככה שהם יעדיפו ככל האפשר טכניקות Test In Production.
- צריך להעדיף משאבים שמאפשרים להבין את סטטוס המערכת ב-production. זה חשוב כדי לאפשר את כל הטכניקות האחרות.
- שאלה: איזה דברים נתקלת בהם שהיו בעליל שגויים? התשובה: להתחיל ממודל המידע. הרבה אנשים מתחילים בכך שהם עושים ERD, וזאת טעיות כי יוצרים תלויות בין רכיבים שביניהם חילוקנו את הישויות. צריך תמיד להתחיל מהפונקציונאליות (התנהגות).
- ב-CQRS מפרידים בין מודל המידע בכתיבה למודל המידע בקריאה.
- מה לגבי serverless? אנחנו עדיין יותר מדי בתחילת הדרך.
- "אני לא מחנך מפתחים. אני נותן להם רעיונות לדברים שצריך לעשות"
- אם יש Product Owner שלא אכפת לו מ-"דברים טכניים", הוא לא מתאים לתפקיד.
יום שני, 20 בפברואר 2017
microXchg 2017
בשבוע שעבר הייתה לי הזכות לקחת חלק בכנס microXchg 2017, כנס ה-Microservices המתקיים בברלין זו השנה השלישית. בכנס שמענו מגוון הרצאות בנושאים מגוונים הנוגעים לתחום מכאלה שכבר התנסו ולמדו (כמו Adrian Cockcroft, Uwe Friedrichsem, Michael Plöd ואחרים), וגם עברנו סדנאות שבהן יכולנו לתרגל חלק מהדברים יחד עם היועצים הבחירים בתחום. היה מאד מעניין ומלמד.
בימים הקרובים אני אעלה סיכומים של ההרצאות המעניינות שעברנו. למי שרוצה לפנות ישר למקור, אפשר למצוא את רוב ההרצאות בערוץ ה-YouTube של הכנס.
נקודות עיקריות שעלו בצורה מאד חזקה במהלך כל הכנס:
- כמו בפעמים קודמות, כולם מדברים כל הזמן על Domain Driven Design. זה מזכיר שוב עד כמה זו התפיסה שעומדת בבסיס כל הארכיטקטורה המודרנית.
- משפט שחזר על עצמו שוב ושוב: כדי לחלק מערכת לשירותים, לא מתחילים עם מודל המידע. מה שיוצר צימוד ובאגים זה התנהגות, לא מבנה. לכן צריך להתחיל בחלוקת הדומיין ל-bounded context לפי התנהגות, לשפר את החלוקה כך שניצור high cohesion ו-low coupling. רק אח"כ אפשר להמשיך למודל המידע.
יום שני, 13 בפברואר 2017
Microservices ושירותי מידע - על מה ולמה?
מדובר במאמר שכתבתי באוגוסט האחרון וכבר הופץ ברבים מפה לאוזן. אני חושב שהגיע זמנו להתפרסם ברבים.
הקדמה - איך לקרוא את המאמר הזה
התפיסה בה לוקחים את המערכות הגדולות בארגון שלנו ומפרקים אותם לאוסף של שירותים ומאגרי מידע היא משהו שמסתובב באוויר הארגון שלנו כבר זמן רב, בצורות שונות, אבל רק לאחרונה קורם עור וגידים. הסיבה העיקרית לכך, לטעמי, היא שבעוד שחלוקה זו נותנת תועלת רבה, היא גם מציבה דרישות שרק לאחרונה הטכנולוגיה יכולה לספק.
לא כך תכננתי, אך המאמר הזה התפתח לדבר על איך שמפתחים תוכנה, כי הנהירה בכל העולם לארכיטקטורה ממין זה נובעת באופן ישיר מהשינוי בתפיסת הפיתוח וצורת המחשבה על מהו מוצר תוכנה. תופעת לוואי של ההבנה הזאת היא אורכו של המאמר, ובעקבות כך הקורא נשאר לאורך כל המאמר בתחושה שהוא לא קורא על הנושא "שבשבילו הוא בא". כדי להפיג את הקושי הזה, הנה המבנה של המאמר:
- רקע - עלייתו של הארגון הגמיש: פרק זה מתאר את עולם התוכנה כפי שהיה לפני 25 שנים, ואת הסיבה לעליית המתודולוגיות האג'יליות.
- משיטת ניהול לשיטת התנהלות: פרק זה מבהיר מדוע ליצור לוח SCRUM, להציג ספרינטים ועמידה ביעדי הספרינט אצל המנהל ושימוש ב-Safe בפני עצמם לא אומרים שאנחנו ארגון אג'ילי - כי אג'ייל זה מה אתה, ולא איך אתה עובד. בפרק יש גם נגיעה בטכנולוגיות שצצו בגלל ארגונים שבאמת הפכו להיות גמישים.
- שירותים מאגרי מידע: הפרק הזה עושה את הקשר לשירותים ומאגרי המידע, ומסביר שלמרות שהחזון של פירוק המערכות קיים שנים רבות, המימוש הוא לא יותר מאשר אימוץ של ארכיטקטורה גמישה.
- שינויים ארגוניים: הפרק הזה מתאר על רגל אחת את השינויים הארגוניים שצריכים לקרות יחד עם הפיכתו של הארגון לגמיש יותר.
- מה הלאה?: מי שגמיש מנצח. הארכיטקטורה הזאת משפרת את הגמישות, אך היא רק צעד אחד בדרך לארכיטקטורה גמישה עוד יותר. פרק זה מתאר בקצרה כמה מהרעיונות העולים בעולם התוכנה.
- ולבסוף עוד כמה קישורים שימושיים.
רקע - עלייתו של הארגון הגמיש
עולם התוכנה מטבעו עובר שינויים לעיתים קרובות. ליאור בראון ניסח את "עקרון חצי הדור" (5) שמציין שמי שרוצה להישאר רלוונטי בתחום התוכנה צריך ללמוד חצי מהמקצוע מחדש בכל עשור. גם העקרונות שמנסים להשיג השתנו לאורך השנים, אבל שתי מטרות העל של עולם זה נותרו בעינן:
- לגרום ללקוח להיות מרוצה - את זה אפשר להשיג ע"י מימוש כל דרישותיו, או על ידי זה שנממש יותר משהוא ציפה (גם אם הוא רצה יותר ממה שעשינו), או שהוא מרגיש שהמערכת "מבינה אותו" או שכיף לו להשתמש בה, או על ידי זה שמקלים עליו בביצוע משימה באופן משמעותי, ועוד. הגורם האחרון דומיננטי מאד בעולם שלנו, אבל בהחלט אינו היחיד.
- להשקיע מינימום משאבים - גם את זה ניתן להשיג בכל מיני דרכים. לדוגמא: העסקת עובדים טובים יותר, העסקת עובדים טובים פחות, מציאת נקודת המינימום שתרצה את הלקוח, הקטנת תקורות הפיתוח (הקדמת גילוי באגים, אוטומציה, הקטנת כמות הדיונים והתלות במנהלים, הקטנת כמות האינטגרציות הנדרשות כדי להתקדם וכד'), הקטנת תקורות התמיכה (שיפור מנגנוני ה-operation, מנגנוני ניטור מתקדמים, טיפול אוטומטי התקלות, טיפול אוטומטי בעומסים, הוזלת החומרה על ידי מחשוב ענן וכו') ועוד.
לגבי שתי מטרות האלה אפשר לשים לב לשני דברים חשובים: (1) הן נכונות לא רק לתוכנה, אלא לכל דבר שיש לו לקוח. (2) שתי מטרות העל האלה עומדות במתח אחת עם השנייה - לְרָצוֹת את הלקוח דורש משאבים, צמצום משאבים יכול לבוא על חשבון הלקוח. המתח הזה מגיע לקיצוניות כשמדובר במערכות מאד גדולות - הלקוח דורש הרבה מאד, והרבה פעמים כדי לרצות אותו (לפתח מערכת ענקית באיכות מלאה, שימור ידע, התאמה מלאה ללקוח וכו') אנחנו נדרשים למשאבים כל-כך רבים שלעולם לא יעמדו לרשותנו, וגם אילו עמדו לרשותנו לא היינו יודעים לנהל אותם. ההפרש בין המשאבים הנדרשים למערכות גדולות כאלה לאלה שזמינים להם, יחד עם הצורך לרצות את הלקוח בשלב השיווק, הוא אחד הגורמים המרכזיים למשבר התוכנה.
כדי ליישר קו, משבר התוכנה מדבר על זה שסדר גודל של 60% מפרויקטי התוכנה נכשלים (עמ' 9) (מקור נוסף - עמ' 5), כלומר חורגים במשאבים, לו"ז, או רידוד תכולות כדי לא לחרוג במשאבים ולו"ז.
תעשיית התוכנה מנסה להתמודד עם משבר התוכנה כבר הרבה מאד שנים בגישות שונות. גישה שתפסה בצורה מאד חזקה בשנות ה-70 היא "תפיסת המהנדסים". התפיסה הזאת הציגה גישה שדיברה על זה שמהנדס תוכנה הוא מהנדס ככל המהנדסים, ולכן עליו להשתמש באותו תהליך עבודה שעובד בצורה מוצלחת (יחסית) בעולם הבניין והחומרה כבר שנים רבות. כאשר מתכננים בניין, יש אוסף של אנשי מקצוע שעובדים על הבניין באופן טורי: איש קונספט (שמעצב את האופי של הבניין והפעילות שתקרה בו), אדריכל, מהנדס בניין, מהנדסים ספציפיים (מהנדס חשמל, מהנדס מים וכו'), קבלן - כל אחד מביא את התכנון עוד צעד מה-"מה" אל ה-"איך". אחרי זה בונים את הבניין, ואז מעבירים את הבניין אוסף של בדיקות איכות: התאמה לתוכניות, בטיחות, נגישות, כיבוי אש וכו'.
אז תפיסת המהנדסים מדברת על תהליך דומה שבו אוסף אנשי מקצוע עובדים על התוכנה באופן טורי - איש קונספט, מאפיין דרישות, מהנדס מערכת, המהנדסים הספציפיים (מאפיין פונקציונאלי, ארכיטקט, איש UX, מהנדס בדיקות), תוכניתן (אפיון טכני), אח"כ מפתחים, ובסוף מעבירים סדרת בדיקות: מילוי הדרישות, בטיחות, שימושיות, שרידות וכו'.
המתודולוגיה שהומצאה (ב-1970) כדי לממש את התפיסה הזאת היא Waterfall, על שם התהליך הטורי שמתבצע. הרעיון שהמתודולוגיה הזאת חרטה על דגלה היא הקדמת מציאת הבאגים לנקודה המוקדמת ביותר בנחל (עם הגרף האקספוננציאלי המפורסם של עלות תיקון הבאג ביחס לשלב הגילוי). השיטה הזו בוודאי שיפרה את ניצול המשאבים ובהתאם לזה העלתה את רף הגודל המקסימאלי של מערכת שאנחנו מסוגלים להרים בלי להתפשר על ריצוי הלקוח יתר על המידה.
השיטה הזו, באופן מאד צפוי, גרפה גם אהדה רבה מהמנהלים כי קל לשלוט בה, והיא נראית מצויין בגרפים: ואפשר להקים מחלקות שלמות בארגון שרק ישפרו את המדדים לראות עד כמה השיטה מתקיימת, מה האיכות שהיא מביאה, קצב ההתקדמות וכו'. במיוחד אהדו את השיטה הזאת מנהלים שבמקור באו מתחומים אחרים כמו בניין.
ברם, במבחן המציאות כעבור זמן מסויים השיטה נתקלה בקשיים משמעותיים, עד כדי כך שהרבה אנשים התחילו לחוש שהשיטה "פשטה את הרגל", והתקווה שהשיטה הזאת תפתור את משבר התוכנה נגוזה. השיטה הזאת הרימה את הרף, אבל עם האוכל בא התאבון - הדרישות עלו מהר יותר מהרף כאשר הרבה ארגונים העבירו את עיקר הפעילות שלהם להיות ממוכנת.
מעבר לזה, היו רבים שסברו שהשיטה אינה מתאימה לעולם התוכנה בגלל שהוא שונה מאד מעולמות הנדסה אחרים.
ישנם מספר מאפיינים מרכזיים שמבדלים את עולם התוכנה מעולמות הנדסה אחרים:
- מחיר הטעות - מחיר הטעות (כלומר השינוי) בתוכנה נמוך משמעותית מהמחיר בעולם הפיסי. יש לזה מספר השלכות:
- המשתמשים מרשים לעצמם לשנות את דעתם.
- בהערכות נכונה אנחנו יכולים לאפשר למשתמשים לשנות את דעתם.
- ככל שה-cycle של דרישה->מבצוע ארוך יותר, מחיר השינוי עולה.
- בהנחה שהיכולת לשנות היא בעלת ערך מבצעי שלעצמה, חברות רבות חרטו על דגלן את תפיסת "embrace the change" או אפילו "fail fast + embrace failure" - במקום לתכנן המון ולפחד מטעויות, עשה מה שצריך וצור מנגנונים שמאפשרים לך לזהות טעויות מהר, לתקן טעויות מהר, ולשתף טעויות ומסקנות מהן בארגון.
- התיישנות - תוכנה מתיישנת מהר. כשבונים בניין יש תקופת בנייה מוגדרת ואח"כ שימוש שוטף ללא בנייה (מלבד שיפוצים מדי פעם ותחזוקה שוטפת) לתקופה ארוכה הרבה יותר. בניין בנוי כהלכה יכול לעמוד מאות שנים.
תוכנה נמצאת לרוב בפיתוח שוטף, ולכן צוברת בלאי מהר. בנוסף, הטכנולוגיה מתקדמת מהר מאד וגורמת להתיישנות - כלים/תשתיות בלי תמיכה ופיתוח, מראה מיושן, חוסר תמיכה בכלים הטובים יותר שקיימים, חוסר תאימות למערכת הפעלה/חומרה, התמעטות משתמשים בעולם (ולכן מענה בפורומים) וכו'. - זה מחייב לעשות שינויי ארכ' וקוד מרחיבים מעת לעת כדי לשמור על חדשנות. זה מחייב ארכיטקטורה שמאפשרת שינוי, ומעלה את העלות היחסית של תכנון ארוך.
- דינאמיות - רכב או בניין הם מרחב עבודה, אבל מה שעושים בתוכו משתנה בקלות. לא היינו קונים בית שבו השולחן והספה ייצוקים מבטון ואי אפשר להזיז אותם אם אנחנו רוצים לעשות משהו מיוחד, או שיש בו מקום בדיוק למה שיש לנו ואי אפשר להכניס עוד גרב, כי השימוש שלנו בבניין משתנה לאורך זמן.
תוכנה, לעומת זאת, היא כלי יום יומי של עבודה. היא צריכה להיות דינאמית ולשנות את התנהגותה בעקבות השתנות המשתמשים ומשימותיהם בטווחי זמן קצרים. - מסמכים - המסמכים בבנייה הם מאד תחומים ופורמאליים - תוכניות בנייה. תוכניות הבנייה ברורות לחלוטין לכל מי שמסתכל עליהן ועונות לסטנדרטים מאד חזקים, כך שהקבלן שבונה את הבניין יכול להסתכל על התוכניות בלבד ולבנות את הבניין במדוייק, ואחרי כל צעד למדוד בפירוט שאכן מה שהוא עשה תואם את התוכניות. כמו כן, התוכניות הן פורמאליות מספיק כך שמחשב יכול להריץ אותן - להציג מודל תלת מימדי שבו הלקוח יכול להסתובב ולראות אם אכן הבניין מתאים לצרכיו.
בתוכנה כמות הפרטים גדולה בהרבה, והסטנדרטים הם הרבה פחות ברורים ומשתנים מהר. כמות האפשרויות גדולה בהרבה, ויש הרבה מקרים שלא ניתן לכסות באפיון באופן מלא - אפיון של 100% לוקח כמעט אותו זמן של יצירת התוכנה. כמו כן יש יכולת מאד מוגבלת להציג למשתמש מודל (מלבד אחד שהוא מאד מופשט ושונה ממה שיתקבל בסוף) כי בניית מודל מלא שקולה לכתיבת התוכנה כולה.
המסמכים הרבים שנדרשים לכתוב בתהליך Waterfall הם בסוף בעלי תועלת פחותה מבתחומים אחרים ועדיין גוזלים לא פחות זמן. יש לזכור בנוסף שתוכניות הבניין כוללות רק מבנה, בעוד שההתנהגות הפונקציונאלית משתנה כל הזמן, בעוד שמסמכי עיצוב תוכנה כוללים גם את ההתנהגות הפונקציונאלית.
במהלך שנות ה-90 התפתחו אוסף של מתודולוגיות "קלות משקל" (בניגוד ל-waterfall שהיא תהליך מאד כבד). בתחילת 2001 התכנסה קבוצה של 17 מהנדסי תוכנה, כמה מהם עדיין מוכרים כמהנדסים המובילים בעולם התוכנה גם היום (כמו Martin Fowler ו-Bob Martin), כולם נציגים של המתודולוגיות קלות המשקל השונות, לשלושה ימים של ניסיון "לחשוף דרך טובה יותר לפתח תוכנה ולעזור לאחרים לעשות זאת". משלושת הימים האלה יצא ה-"Manifesto for Agile Software Development", שהצהיר כך:
"אנו חושפים דרכים טובות יותר לפיתוח תוכנה תוך עבודה ועזרה לאחרים. אלו הם ערכינו ועקרונותינו:
אנשים ויחסי גומלין על פני תהליכים וכלים
תוכנה עובדת על פני תיעוד מפורט
שיתוף פעולה עם הלקוחות על פני משא ומתן חוזי
תגובה לשינויים על פני מעקב אחרי התוכנית
כלומר, בעוד שיש ערך לפריטים בצד שמאל, אנחנו מעריכים יותר את הפריטים בצד ימין."
ניתן למצוא הסבר מלא ואת 12 העקרונות שמפרט ה-manifesto באתר וב-wikipedia. בין השאר העקרונות דיברו על תהליך איטרטיבי יחד עם הלקוחות, מסירה לעיתים קרובות, עבודה פנים-אל-פנים, מדידת תוצרים, איכות, צוותים עצמאיים (במקום הנהלה שולטת) ותחקור עצמי.
כל העקרונות האלה באו לתת מענה לאותם חוסרים של Waterfall ממקודם: עבודה איטרטיבית, מסירה לעיתים קרובות וגמישות ארגונית מאפשרות ללקוחות לשנות את דעתם ולהבין את מחיר השינוי, לזהות טעויות מהר, להקטין סיכון של שינוי ארכיטקטוני, להקטין overspeck (מגלים שהמשתמשים לא רוצים משהו בשלב מאד מוקדם, לא מתכננים מנגנונים לכל מקרה שלא יהיה ואז משתמשים בהם בדיוק פעמיים), קיצור טווח הזמנים שבו המשתמש יקבל מענה וכד'. חשוב להדגיש שהשינוי הוא לא רק באיטרציות - אלא בעיקר בהתנהלות של ארגון שהוא גמיש ומתאים את עצמו לשינויים כל הזמן.
הפיתוח ע"פ עקרונות אלה אינו מבטיח פיתוח מהיר יותר, אלא מבטיח מוצר טוב יותר, כלומר מתאים יותר ללקוחות, איכותי יותר ובזמן רלוונטי יותר, לפעמים גם על חשבון מהירות הפיתוח (כלומר מגדילים את מדדי "כמות תכולות ב-production לחודש" ו-"התאמת התכולות ללקוחות" על חשבון מדד "כמות תכולות בשנה").
ע"ב תפיסה זו התפתחו מתודולוגיות שונות כגון Scrum, Kanban ו-XP, שהמטרה של כולן היא לשים במרכז את האנשים ולא את השיטה, ולאפשר לצוותים עצמאיים לכתוב תוכנה ולהריץ אותה ל-production תוך איכות מירבית והתאמה ללקוחות. זאת כדי להשיג טוב יותר את עקרונות העל: לגרום ללקוח להיות מרוצה, ולהשקיע פחות משאבים בתהליך.
בשורה התחתונה - הסטטיסטיקה מראה שאכן בארגונים עם גמישות גבוהה (כלומר שמתנהל עם הרבה מעקרונות ה-agile) יש אחוז כשלונות נמוך יותר (עמ' 16). ובביטוי אחד: מי שגמיש מנצח.
[יש מאמר מעניין של משה דיין, שמסביר למה דויד ניצח את גוליית. בין השאר, הוא אומר: "מערכת המגן הכבידה מאוד על גלית, האיטה אותו וגם סירבלה את תנועותיו… כנגד זאת השכיל דוד לוותר על מערכת הנשק הכבדה, שהציע לו שאול, והוא בחר לנוע חשוף, אך חופשי. בעוד הפלשתי "הולך" לקראת דוד, הרי דוד "ממהר ורץ" אל המערכה (פס' מח). יתרונו של דוד היה בקלות תנועתו ויכולת התמרון שלו, והיוזמה עברה לידיו". המאמר, בשם "רוח הלוחמים", התפרסם כחלק מספר בשם "עשרים שנות קוממיות ועצמאיות, עלילות גבורה". הציטוט לקוח מתוך האתר הזה.]
משיטת ניהול לשיטת התנהלות
מתודולוגיות הפיתוח האג'יליות, ו-SCRUM בראשן, תפסו תאוצה מאד גדולה. ובכל זאת הרבה ארגונים לא הצליחו להביא את השינוי במלואו בהתחלה, והמשיכו להתקל באותם הקשיים. למה? כי culture eats strategy for breakfast.
אג'ייל הוא צורת חשיבה. זה אומר לא רק שעושים איטרציות קצרות, אלא גם שנותנים מקום לגמישות לאורך התהליך (ואף מעודדים אותו). זה אומר שבמקום ליצור תוכניות עבודה גרנדוזיות ולמדוד התאמה אליהן, מודדים את קצב ההתקדמות ואת התפוקות למשתמשים - ואת רשימת התכולות אפשר תמיד לשנות (כמובן שתמיד יש דברים שצריך לתאם מול גופים אחרים, אבל כתפיסה). זה אומר שלא יושבים לסקרים בכל נקודה בתהליך, אלא מאפשרים לצוותים לרוץ בפיתוח ומקבלים באהבה טעויות, כי אנחנו גמישים ואפשר לתקן אותן. זה אומר ששמים דגש על גמישות בארכיטקטורה, ולא מתכננים ארכיטקטורה שתתאים לכל מה שהמערכת עלולה לעשות ב-20 שנה הקרובות. זה אומר שביטויים כמו KISS ו-YAGNI שמעודדים להעדיף קוד קצר, קריא ועונה על מה שצריך כרגע, הופכים להיות משהו מרכזי. זה אומר שההנהלה מאצילה לצוותים יותר סמכות, ומאפשרת להם להתקדם בלי לחכות להחלטות מנהלים בכל נקודה, כשההנהלה מתרכזת בקביעת יעדי הארגון ונותנת לצוותים להחליט איך מגיעים לשם.
אבל השינוי חילחל, וכיום החברות הגדולות בשוק שינו לא רק את צורת העבודה אלא גם את צורת החשיבה שלהן והתרבות שלהן - מאפשרים לצוותים לעבוד "כמו startup" ומאפשרים גמישות בפיתוח - איש ה-product עובד ישירות עם הצוותים והם מביאים כל רעיון שלו ישירות ל-production, תוך תחקור מתמיד והתמודדות עם בעיות.
עם המעבר של הארגונים לתרבות גמישה וחשיבה גמישה, להרבה עלתה השאלה - למה בעצם אני צריך איטרציות בכלל? למה להיות גמיש רק פעם בשבועיים? אני יכול להיות גמיש בכל יום. אם סיימתי תכולה, אני כבר יכול לדחוף אותה ל-production. וזה הביא את החברות למצב שבו הן דוחפות גרסא ל-production במרווחים של שניות (המצגת שהוצגה בסרטון).
בין אם פעם בשנייה או פעם בחודש, השיטה הזאת נתקלה בקשיים שנבעו מכך שכל הסביבה נבנתה לקצב איטי, ובהדרגה הטכנולוגיה בשוק התפתחה כדי להתאים לקצב המהיר החדש. לדוגמא: רכש של שרתים פיסיים הוא תהליך שלוקח הרבה מאד זמן, וזה מגביל את יכולת ההתפתחות של התוכנה שלנו (כי יכולת חדשה דורשת יותר משאבים, צריכה לשבת בנפרד, או תביא כמות גדולה של משתמשים שהמערכת לא ערוכה לקלוט). אז הומצאה הוירטואליזציה: נתקין שרתים וירטואליים, ואז אם צריך פתאום עוד שרת תמיד אפשר להרים עוד אחד על השרתים הפיסיים הקיימים. זה התפתח מאוחר יותר לענן, שבו אני משתף חומרה פיסית בין הרבה מערכות ומסוגל לחסוך הרבה וגם להגדיל (ולא פחות חשוב: להקטין) משאבים בקצב מהיר ועלות נמוכה. עוד דוגמא: לעיתים קרובות התקנה דורשת שינויי קונפיגורציה או התקנות על שרתים. כשמתקינים לעיתים מאד קרובות עלול להיווצר בלאגן גדול בסביבת ה-Production, וכל שרת יראה קצת אחרת ויגרום לבאגים. אז הומצאו ה-container-ים: אני מכין image של שרת מוכן קל משקל, ומתקין אותו על כל השרתים שלי. אם צריך לשנות משהו, אני יוצר גרסא חדשה שלו, בודק אותה ומתקין אותה. ורכיב אוטומטי יעשה התקנה הדרגתית ויוודא שכל השרתים מותקנים לגרסא החדשה בסופו של דבר.
עוד דוגמא: אני לא יכול ליצור downtime לשם התקנה לעיתים קרובות. אז הומצאו טכניקות של "התקנה חמה" שמאפשר להתקין בלי להוריד את המערכת, וגם להחזיר לאחור בקלות במקרה של כישלון. עוד דוגמא: אני לא יכול לעשות בדיקות מסירה כל הזמן. אז הומצאו טכניקות חדשות לבדיקות אוטומטיות ותהליכי אוטומציה באופן כללי.
אפשר להרחיב עוד הרבה, אבל הנקודה היא שעם שינוי התרבות בשוק התוכנה היו המון התפתחויות טכנולוגיות שכל מטרתן - לאפשר לארגונים להיות כמה שיותר גמישים.
ארכיטקטורה תומכת ארגון גמיש
התרבות הארגונית היא חשובה מאד אבל כדי להתקיים היא זקוקה לארכיטקטורה תומכת. ולא בכדי - יש השפעה מאד ישירה בין השניים. דוגמא לזה אפשר למצוא בחוק Conway (שקיבל אישרור ממספר מחקרים אקדמיים בשנים האחרונות) שטוען ש-"ארגונים אשר בונים מערכות… מוגבלים לייצר מערכות שהן העתקים של המבנה התקשורתי של עצמם". מחקר נוסף מראה קשר ישיר בין המורכבות הארגונית לבין איכות המוצר.
זה עובד גם בכיוון ההפוך: כדי לאפשר תרבות של מסירה לעיתים קרובות, התאמה ללקוח וכד' הארגון זקוק לארכיטקטורה שמאפשרת שינויים מהירים. אימוץ הטכנולוגיות הנזכרות מעלה הן שלב אחד, אבל זה עדיין לא השלים את המהלך.
מתוך ההבנה שהרווח של החברה תלוי ביכולת שלה להגיב מהר לשינויים, החברות בשוק התחילו לעבוד על מבנה ארכיטקטוני שונה, שמאמץ את עקרונות כתיבת התוכנה (כמו SOLID לדוגמא) לרמת הארכיטקטורה - כי הרי היכולת לכתוב קוד שקל לשנות את ההתנהגות שלו הוא הקו המנחה בכתיבת תוכנה כבר שנים רבות. העקרונות המנחים לכתיבת קוד גורמים לנו לחלק את הקוד שלנו לחלקים קטנים ככל האפשר ולהוריד את התלות בינהם, על מנת ליצור cohesion מקסימאלי ו-coupling מינימאלי:
- Cohesion גבוה אומר שלמודול יש פונקציונאליות אחת, כלומר שקוד לא קשור יושב בנפרד. נגיד שאני עובד ב-Amazon, וצריך להוסיף feature לתהליך ההזמנה. האם אני עלול לגרום לבאג בתהליך של משלוח? אם כן, זה כנראה אומר שיש לי cohesion נמוך מדי, כי יש לי קוד שעושה הזמנות וקוד שעושה משלוחים באותו מקום.
- Coupling נמוך אומר שפונקציונאליות שלמה יושבת במודול אחד, כלומר שקוד קשור יושב ביחד. אם ביקשו ממני להוסיף feature לתהליך ההזמנה ואני צריך לגעת בשני מודולים, כנראה שיש לי coupling גבוה מדי.
את סט העקרונות שמאפשר לנו להשיג cohesion גבוה ו-coupling נמוך אנחנו מיישמים בארכיטקטורת הקוד שלנו כבר הרבה שנים, שהמטרה היא שיהיה קל להכניס יכולות ושינויים בקוד שלנו.
כדי לאפשר ארכיטקטורה שבה קל להכניס שינויים, לקחו את אותם העקרונות והחלו אותם על מבנה המערכת. אנחנו רוצים לשים קוד קשור ביחד וקוד לא קשור בנפרד, ולכן מחלקים את המערכת לאוסף של רכיבים קטנים שכל אחד מהם מכיל פונקציונאליות שלמה ואת כל הפונקציונאליות הזו. שיטת הארכיטקטורה הזאת זכתה לשם Microservices (או בקיצור: MSA - Micro Services Architecture).
אז מה מאפשרת MSA?
- קודם כל, כמו בקוד: אם רוצים להכניס feature, לרוב נצטרך לגעת רק במקום אחד, שמכיל רק פונציונאליות אחת. זה אומר שהסיכוי שנכניס באג במקום אחר או בפונקציונאליות אחרת הוא נמוך מאד. וזה אומר שמספיק להתעמק בבדיקה לפונקציונאליות שנגענו בה.
- "לגעת" במקום אחד זה לא רק לשנות קוד, גם התקנה היא נגיעה. כ-80% מהתקלות בתוכנה קורות לאחר התקנה. ע"י צמצום האזור שנוגעים בו, ניתן לצמצם משמעותית את כמות הבאגים.
- גם כמו בקוד: קוד שממודל טוב, עם מעט תלויות ועם dependency inversion, מאפשר לנו לשנות את הארכיטקטורה של התוכנה (ואף לבנות תוכנה נוספת) ולהמשיך להשתמש בקוד הקיים, על ידי שינוי של רכיבים ספציפיים והקוד המקשר בלבד. עם process-ים, ניתן גם כן לשנות את ארכיטקטורת המערכת וליצור מערכות נוספות על ידי שינוי מעט מאד קוד ואת הקישור (routing) בלי לגעת ברכיבים קיימים.
- היכולת שלנו לשנות ולהתקין קטע קוד ממוקד בלי לגעת בשאר המערכת מאפשרת לנו גם להתמודד טוב יותר עם תקלות. אם נמצאה תקלה בגרסא מסויימת, ניתן להתקין תיקון מקומי. יותר מזה, קל מאד לחזור לגרסא קודמת ולהתקין את הגרסא באופן הדרגתי.
- בשימוש ב-API לתקשורת בין רכיבים, יש לנו הרבה יותר יכולות של שמירת תאימות לאחור מאשר בשימוש ב-interface תכנותי. API גם נוטה באופן טבעי להיות מתועד טוב יותר.
- תקלה בשרת מסויים, בין אם תוכנה או חומרה, תגרום לפגיעה משמעותית במונוליט אבל לפגיעה יחסית זניחה ב-process קטן שהוא אחד מיני רבים במערכת.
- ניתן לחשוף feature מסויים באופן הדרגתי - להתקין instance אחד של הגרסא החדשה, ולהפנות רק חלק מהבקשות מהשרת אל ה-instance הזה.
- ניתן להפריד את ה-lifecycle של היחידות הפונקציונאליות ולנהל אותן בנפרד. זה יתרון מאד גדול, כי מחקרים מראים שאחוז ההצלחה בפרוייקטים קטנים הוא גדול משמעותית מזה של פרוייקטים גדולים (מאמר של גרטנר שמציג 50% יותר הצלחה בפרויקטים קטנים, מאמר של CHAOS שמציג הפרש עוד יותר קיצוני (עמ' 8)). חלוקה לפרוייקטים קטנים עם תלות מינימאלית משפרת את סיכויי ההצלחה (זאת גם ההמלצה במאמר של גרטנר).
- בגדול כמות המצבים שמערכת יכולה להיות בהם היא אקספוננציאלית בכמות הזיכרון שהיא משתמשת. כשלוקחים רכיב קטן עם מרחב זיכרון נפרד, כמות המצבים היא לוגריתמית לעומת של מערכת גדולה ולכן ניתן לכסות בבדיקות פשוטות יותר כמות מצבים גדולה הרבה יותר. יותר מזה, רוב היחידות הפונקציונאליות לא צריכות להחזיק state בעצמן (אלא משתמשות ב-state של יחידה פונקציונאלית אחרת) ולכן כמות המצבים בהן קטן עוד יותר באופן ניכר.
- כאשר מערכת רצה ב-process אחד, זה גורם לתלות נוספת בן הרכיבים - תלות טכנולוגית. כאשר מפרקים מערכת לרכיבים נפרדים, הם לא חייבים להיות באותה טכנולוגיה. זה מאפשר לבחור את הטכנולוגיה הנכונה לצורך, ויותר חשוב מזה - כאשר מחליטים להחליף טכנולוגיה אפשר לעשות שידרוג הדרגתי.
אפשר להמשיך עוד הרבה עם יתרונות של השיטה, אבל זה מכסה את הדברים העיקריים.
לא נסקור זאת במאמר הזה, אבל נגענו רק בארכיטקטורה לצד ה-backend. גם בצד ה-frontend התפתחו ארכיטקטורות וטכנולוגיות שתומכות שינויים בלתי תלויים בין רכיבים, על ידי חלוקה לרכיבים קטנים שמתקשרים דרך API גמיש (תוכנתי במקרה זה) ולא מכירים זה את זה.
כמובן, שום דבר לא בא בחינם. כשאנחנו כותבים קוד טוב שמחולק למודולים ומחלקות עם אבסטרקציות טובות, זה לעיתים בא על חשבון היכולת שלנו להבין בדיוק מה קורה בקוד - כשאנחנו קוראים אותו אנחנו רואים פעולות על כל מיני אבסטרקציות ולא יודעים מה בדיוק יעשה ה-type הקונקרטי. קוד שמפורק בצורה טובה קשה לניהול, כי הוא מפוזר על כמות גדולה של תיקיות ומודולים. בקוד גנרי עלול להיות מחיר בביצועים (בגלל שימוש ב-reflection או מבני נתונים שונים, נעילות וכו'). בקוד שמפורק בצורה טובה לפעמים אנחנו נתקלים במגבלה שקשה לנו ליצור משהו בגלל שזה יצור תלות לא בריאה, וצריך להקיף את הכל במנגנון נוסף. זה עדיין משתלם בהשוואה לאלטרנטיבה - קוד 'נקניק' שקשה להתמצא בו וכל שינוי עלול לגרום לנזק.
בצורה דומה, גם ל-MSA יש את המחיר שלה. המחיר של MSA דומה לזה של קוד איכותי:
- הקוד מחולק על פני תהליכים שונים, קשה לראות במבט אחד תהליך שלם וקשה לדבג תהליך שלם.
- כדי למנוע תלויות, צריך להפריד את המידע ולהתבסס רק על API. זה אומר שיש ביזור בשמירת המידע, וצריך להתמודד איתו אחרת מאשר עבודה על DB יחיד.
- המעבר לשימוש ב-API מחייב תקשורת, שמטבעה היא איטית יותר מאשר קריאה לפונקציה.
- ניהול צי שרתים הוא קשה הרבה יותר מניהול שרתים ספורים, ודורש עבודת DevOps איכותית ועם אוטומציה. [בהקשר הזה, יש אנלוגיה מקובלת שמדברת על "שרתים הם בקר, לא חיות מחמד". אפשר לקרוא כאן וכאן, וגם בראיון המצויין (כמו תמיד) על MSA עם אדריאן קוקרופט, ארכיטקט נטפליקס לשעבר ו-VP ארכיטקטורה בשירותי הענן של אמזון בהווה]
עדיין, לטעמי לפחות (ולפי הפופולריות של MSA לא רק לטעמי), היתרונות שווים את המחיר, וככל שמדובר במערכת גדולה יותר היתרון גדול יותר. איך בכל זאת מתגברים על החסרונות? הנה כמה רעיונות:
- החלוקה לחלקים קטנים מאפשרת לנו לעקוב אחרי מה שקורה בפועל במערכת מבחוץ, וזה מאפשר לנו ליצור ויזואלוזיציה של דרכה של הודעה, החל משליפת כל השלבים שעברה הודעה ספציפית (שזה קל מאד ע"ב ELK למשל) ועד כלים שמציגים את התנועה במערכת, כמו למשל Appdynamics או חבילת הכלים של Netflix.
- כדי להתמודד עם ביזור בשמירת המידע, משתמשים ב-eventual consistency (מה שבכל מקרה הכרחי אם רוצים יכולת scaling יחד עם זמינות גבוהה - ראה CAP Theorem).
- עבודה עם תקשורת: החלוקה מאפשרת לנו לשפר ביצועים במקום אחר - Outscale קל רק בצווארי הבקבוק, מה שגורם ל-scaling לעבוד הרבה יותר טוב. זה מאפשר מקבול בין שרתים של הפעולות. ובמקרה הכי גרוע, תמיד אפשר לשבור את העקרונות כשיש בעיה, וב-MSA אפשר לשבור אותם בצורה מאד מקומית ומתוחמת.
- ניהול צי שרתים: בגלל הנפחים של התעבורה, החברות הגדולות (וגם לא כל-כך גדולות) מנהלות ציים של עשרות אלפי שרתים. לכן יש המון המון המון המון כלים לויזואליזציה, ניטור ותחזוקה של הרשת.
הפתרונות לא גורמים לבעיות להיעלם, אבל מקטינים את הנפח שלהן ומשאירים את המערכת גמישה לשינויים.
איפה נמצאת עכשיו MSA מבחינת בשלות? ה-hype cycle של Gartner שם אותו בראש הגל נכון ליולי 2015 (מה שאומר שאנחנו עוד לומדים את מגבלות). סקר של Nginx מסוף 2015 מציג אחוזי חדירה גבוהים לשוק. סקר נוסף של ZeroTurnaround מיולי 2016 מציג אימוץ של MSA בכ-34% מהשוק.
צריך לזכור, MSA היא בסך הכל חיבור של הטכנולוגיות שהתפתחו (כמו ענן, אוטומציה ו-API) עם התפיסה של ארגון גמיש.
מאגרי מידע ושירותים
הביטוי "מאגר מידע" או "שירותי מידע" נולד הרבה לפני תפיסת ה-MSA, כשכבר לפני 7-8 שנים מהנדסים רבים בארגון הרגישו שהשיטה של ממשקים בין מערכות לא עובדת, והמידע צריך להיות נחלת הכלל.
הרעיון עבר הרבה תהפוכות ומחשבות, החל מסטנדרטים אחידים לחשיפת מידע שיחייבו את כלל המערכות, דרך DB-ים מרכזיים שישבו מעל bus-ים גדולים שדרכם אפשר יהיה לצורך ולעדכן מידע ועד ארכיטקטורות "black box" של DB גדול עם API סטנדרטי לצריכת המידע וללא לוגיקה. אבל לכל הרעיונות האלה היו חסרונות מכל מיני סוגים שנתנו לנו את התחושה שהרעיונות האלה לא מחזיקים מים, תחושה מאד מתסכלת כמי שמסתכל מהצד על ארגון תקוע שיש לו את כל מה שצריך ולא מצליח להשלים תהליכים למשתמש כי הוא לא מצליח לחבר בין המערכות.
אבל כשהגיעה MSA, התחושה הייתה שהיא מכילה בדיוק את הדברים שחיפשנו: הפרדת המידע מהאפליקציה, חלוקת המידע ליחידות פונקציונאליות נפרדות ועצמאיות יחד עם הלוגיקה שלהן, תמיכה בשינויים לא מתואמים על ידי אוסף של טכניקות תאימות לאחור ולפנים, יכולת להעלות את קצב הפיתוח על ידי התקנה מהירה ב-production עם סיכון נמוך עקב יכולת חזרה לאחור מידית, ודרגת גמישות מאד גבוהה בהתפתחות הרשת כולה.
המילה "מאגר מידע" מתייחסת למה שנקרא ב-MSA בשם "Data Service" - שירות שיודע לספק מידע ולעדכן מידע. בהתאם ל-MSA, מאגר המידע מכיל את המידע ב-storage משל עצמו (כמו DB למשל) וניתן להתקנה בנפרד לחלוטין משאר העולם. הוא עצמו עשוי להיות מורכב מאוסף של רכיבים כאלה.
אז הרעיון המסדר של מאגרי המידע הוא בדיוק זה של MSA: חלוקת העולם לרכיבים פונקציונאליים קטנים השומרים על cohesion מקסימאלי ו-coupling מינימאלי.
כדי להשיג את זה, נקבעו עקרונות למאגרים:
- אם לישויות יש לוגיקה עיקרית משותפת, נשים אותם ביחד כדי להשיג cohesion גבוה - אם נרצה לעשות שינוי בלוגיקה, לא נפגע בלוגיקה של ישויות אחרות.
- אם לישויות יש מעט במשותף, נשים אותם בנפרד כדי להשיג coupling נמוך - כדי שנצטרך להתקין רק מאגר אחד מהן בעת שינוי בלוגיקה. לכן נשאף לחלק למאגרים קטנים ככל הניתן כל עוד לא פוגעים ב-cohesion יותר מדי.
- שני מאגרים לא משתפים DB ביניהם, כי זה יוצר coupling מאד גבוה.
- מותרת הצבעה (reference) ממאגר אחד למאגר אחר.
- המאגר הוא product. יש לו lifecycle עצמאי, הוא לא חלק מ-product אחר.
- המאגר הוא בעל המידע. הוא האחראי הסופי לוודא שמידע תקין, קוהרנטי, זמין ומגובה.
- המאגר חושף API סטנדרטי, ומחוייב כלפי לקוחותיו לתאימות לאחור. מאידך, הוא דורש מלקוחותיו להיות תואמים לפנים.
- המאגר דורש מלקוחותיו לחיות עם מצב של eventually consistent, לפחות בכל מה שקשור בעבודה בין מאגרים (אך לא מוגבל לזה).
המטרה בתפיסת המאגרים היא לאפשר לשנות את התרבות הארגונית לתרבות גמישה, ובהתאם לשנות את הצורה שבה אנחנו מפתחים תוכנה:
- לאפשר לצוות לקחת feature, לממש אותו ולהריץ אותו עד ה-production במינימום חיכוך עם צוותים אחרים או עם מנהלים - הם יכולים לשנות ולהשתמש בטכניקות של תאימות לאחור.
- לאפשר לנו לזהות טעויות אפיון, באגים ותקלות מהר ולטפל בהם מהר לפני שהמשתמשים נפגעים. את זה ניתן להשיג ע"י אוסף של טכניקות מעולם ה-MSA, כגון: Mirror Testing, Switch Testing, A/B Testing, ניטור מגמות (בעיקר בהבדל בין גרסאות).
- ובהמשך לקודם - מפסיקים לפחד מטעויות וכשלונות. הטכניקות הנ"ל יחד עם rollback מיידי והתקנה זולה (אפשר לתקן באג ולהתקין באותו יום ב-"לבן"), מאפשרים לנו להוריד מאד את העלות של טעות ובאג, ולכן חופש גדול יותר בפעולה.
- לא לחייב את הלקוח לבחור תכולות לשנה קדימה, אלא להיות איתו בדיאלוג מתמיד ולתת לו את מה שהוא צריך גם בטווח הקצר. כדי לעשות את זה, coupling נמוך יקטין את מספר ההתחייבויות החיצוניות שלנו, וההתקנה המקומית תעשה את תהליך ההתקנה לקל יותר ולכן אפורדבילי לביצוע לעיתים תכופות.
- לאפשר לנו להתאים טכנולוגיה לצורך, וגם להתקדם טכנולוגית בעלות נמוכה יחסית.
- שימוש ב-API סטנדרטי המבוסס על פרוטוקול סטנדרטי (כגון REST ו-MQTT) יאפשר לגורמים מכל רחבי הארגון להתחבר בלי צורך להקים "ממשק" מפורש כמו פעם.
חוץ מזה, למאגרים יש עוד אוסף של יתרונות לעומת הקיים בארגון שלנו כיום, מעצם זה שנוצרת הפרדה בין האפליקציה למאגר. מוביל תפיסת המאגרים בארגון שרטט את זה בשקף (שמובא כאן כטבלה):
הבעייה: TTM ארוך וחוסר רלוונטיות.
הסיבות:
|
המטרה: TTM קצר, תרומה מיידית ליכולות המשתמשים.
הפתרונות:
|
ריבוי מקורות מידע לאותו מידע
|
אמת אחת ואחריות על המידע
|
פורמט מידע קשיח
|
גמישות לשינויים וגרסאות
|
ייצוג שונה למידע באזורים שונים בארגון
|
ייצוג אחיד למידע בארגון
|
עבודה עם מונוליטים
|
פירוק לאפליקציות, שירותים ומאגרים
|
תהליך בדיקות מורכב
|
מכוון תפיסת הבדיקות החדשה
|
ההפרדה בין האפליקציה למאגר, במקרה הזה, מאפשרת סטנדרטיזציה של המאגרים וניהול המידע, ובכך להפוך את המידע להיות "נכס ארגוני" התואם לצרכי הארגון ולא לאפיון של אפליקציה ספציפית.
שינויים ארגוניים
השינוי התרבותי הזה, יחד עם השינוי הארכיטקטוני שמאפשר אותו, צריכים להביא גם שינויים ארגוניים שיאפשרו לנו להפיק את המיטב מהתפיסה הזאת. בין השאר:
- הַסַמְכוּת צריכה לרדת דרגה בהיררכיה (לזוז ימינה בלוח ההאצלות, עוד מידע כאן וכאן) כדי לאפשר לצוותים לנוע מהר יותר בלי לחכות למנהל על כל דבר. הורדת עלות הטעות מאפשרת את זה.
- כל השותפים למוצר צריכים לעבוד יחד - איש ה-product, המאפיין, הארכיטקט, צוות הפיתוח ואנשי ה-ops. כולם צריכים להיות חלק ממחזור העבודה ולהיות מעורבים, לא בהעברת אחריות לשלב הבא בתהליך.
- אוטומציה היא בסיס, לא אפשרות מתקדמת. Unit test מלא למוצר הוא חלק מהסטנדרט, ענן בפיתוח שמאפשר להרים סביבת בדיקה בקליק זה חלק מסביבת הפיתוח של התוכניתן, ואף אחד לעולם לא נוגע בסביבת ה-production שלא דרך push מסודר.
- במקום גופי תשתיות גדולים, מעבירים משקל לשיטת Open Source שבה הצוות שזקוק לתכולה מפתח אותה, בבקרה של צוותים אחרים או אחראי Open Source. אין גופים גדולים שמהווים צוואר בקבוק וסיכון בתוכנית העבודה, אלא אם כן זו תשתית שהיא לחלוטין מחוץ לתחום האפליקציה (כמו מנגנוני security וניטור סטנדרטיים, BI וכד') או מוצר אחד שמשרת את כל הרשת (כמו event bus).
- מנגישים מידע. ה-API ותשתיות ה-Open Source צריכים להיום חשופים בפורטל נגיש לכולם (כלומר לא sharepoint אלא משהו מהיר ונוח) שדרכו אפשר לדעת מה קיים ושימוש ב-npm ונגזרותיו כדי לאפשר לתוכניתן לעבוד בסביבת עבודה מתקדמת עם ניצול מיידי של נכסים קיימים.
- במקום המון סקרים, מעדיפים להגיע כמה שיותר מהר ל-Earliest Testable Product (ראה גם Lean Startup. יש גם הרצאה).
מה הלאה?
מאגרי מידע ו-MSA הם לא סוף הדרך. הם עוד אבני בניין בתהליך שהתחיל מהכרטיסיות המנוקבות, האסמבלר, השפות העיליות, OOP, ארכיטקטורת SOA ועוד הרבה שעברנו בדרך. MSA היא הטכנולוגיה הבשלה הנוכחית למימוש של גמישות ארגונית מקסימאלית. אבל המגמה ממשיכה ומתחזקת, וכבר יש טכנולוגיות שמציעות להיות השלב הבא של גמישות בתוכנה. שתי דוגמאות לטכנולוגיות כאלה:
- Serverless Architecture - ארכיטקטורה שמציעה "להיפתר" מהשרתים בכלל ולאפשר לענן להריץ פונקציות בודדות, ב-scale כראות עיניו, כמעין 'ענן פונקציות'. דוגמא לתשתית כזאת שכבר נמצאת בשימוש ב-production היא AWS Lambda, שבה משלמים לפי milliseconds שהקוד שלנו רץ. למרות שהארכיטקטורה הזאת זוכה לצמיחה גדולה, היא עדיין בחיתוליה ולא ברור שאכן תוכל להחליף את הארכיטקטורה מבוססת השרתים. אבל היא מקצינה עוד יותר את השאיפה של הארגונים לגמישות ולשינויים בקצב גבוה.
- Reactive Programming - תפיסה שמתיימרת להחליף את ה-flow של תוכנה כמו שאנחנו מבינים אותו היום (ובמובן מסויים את OOP כארכיקטורת הקוד הראשית). היא מצויינת בעולם שבו יש הרבה אירועים שזורמים, ומאפשר טיפול מאד גמיש וניתן להרחבה באירועים בזמן אמת. על אף שהיא בשימוש מסויים ב-.Net כבר שנים רבות, היא גם כן עוד בחיתוליה ולא ברור מה המשקל שהיא תתפוס בתוך עולם התוכנה.
עוד תפיסות כאלה ימשיכו ויעלו במעלה הדרך. זה יכול להיות ענן מידע (במקום DB), תקשורת מודולרית (במקום מבנה הרשת הנוכחי, SDN הוא צעד ראשון בכיוון) או אפילו מעבר להתנהגות סטטיסטית מבוססת אוסמוזה, בדומה למנגנונים בגוף האנושי.
הידיעה שהעולם ישתנה, וישתנה מהר, חייבת להיות בבסיס התרבות של ארגון שרוצה לתת מענה טוב ללקוחות שלו לאורך זמן, וחייבת להיות בבסיס הארכיטקטורה שסביבה הוא בונה את המערכת שלו, כדי שבפעם הבאה לא נצטרך שוב 10 שנים של פיתוח עם זמן ביניים מאד לא נעים כדי להתקדם בטכנולוגיה, שבסופם נגלה שאנחנו עדיין מאחור.
ושוב: מי שגמיש מנצח.
נספח א' - קישורים שימושיים נוספים
בנוסף על כל הלינקים שהופיעו במסמך, עוד כמה לינקים שימושיים:
קישור
|
לינק
|
דף עם ריכוז גדול של סטטיסטיקות
| |
מידע על התיאוריה של MSA
|
|
מידע פרקטי על השימוש ב-MSA
|
A page with a great collection of references to articles, thoughts and lectures about MSA. Probably the best you can find if you want to make a real decision or start implementing.
Netflix techbolg - Netflix opens it’s entire architecture to the public, including technics, performance issues and how they resolve them, distribution issues, tools and frameworks, etc.
|
מידע על ארכיטקטורת Consumer Driven Contract
|
|