כרגע, אנחנו מנהלים את רוב התקשורת אצלנו בארגון בצורה של 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 שונה.
חושבים ששכחתי משהו? רוצים להעלות עוד שיקולים רלוונטיים? מזמנים לרשום בתגובות!