Fault-Tolerance בארכיטקטורת Microservices, ובכלל

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

למה בלתי צפויה?

ובכן, אתחיל במעט רקע: לאחרונה פירקנו מתוך המערכת הראשית שלנו (\"המונוליט\") כ 7 מיקרו-שירותים חדשים, חלקם קריטיים לפעולה תקינה של המערכת. כל שירות הותקן בסביבה של High Availability עם שניים או שלושה שרתים ב Availability Zones שונים באמזון. חיברנו Monitoring לשירותים – פעולות סטנדרטיות. בהמשך תכננו לפתח את היציבות אפילו יותר, ולהוסיף Circuit Breakers (דפוס עיצוב עליו כתבתי בפוסט קודם) – בכדי להתמודד בצורה יפה יותר עם כשלים חלקיים במערכת.

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

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

– \"נראה כמו\" ?

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

  • Latency בתוך ה Data Center (בין AZs) שקופץ מ 2 מילי-שניות בממוצע לכ 80 מילי-שניות בממוצע. 80 מילי-שניות הוא הממוצע, אבל גם לא נדיר להיתקל ב latency של 500 מילי-שניות – כאילו השרת נמצא ביבשת אפריקה (ולא ממש ב Data Center צמוד, עם קווי תקשורת dedicated).
  • גלים של אי-יציבות, בהם אנו חווים אחוז גבוה מאוד של timeouts (קריאות שלא נענות בזמן סביר), במקרים הקיצוניים: יותר מ 1% מניסיונות ה tcp connections שאנו מבצעים – נכשלים (בצורת timeout – לאחר זמן מה).
אנחנו רגילים לתקשורת-לא מושלמת בסביבה של אמזון – אבל זה הרבה יותר ממה שהיינו רגילים אליו עד עתה. בניגוד לעבר – אנו מתנסים לראשונה בכמות גדולה של קריאות סינכרוניות שהן גם קריטיות למערכת (כמה אלפי קריאות בדקה – סה\"כ).
זה נקרא \"ענן\" – אבל ההתנהגות שאנו חווינו דומה הרבה יותר ללב-ים: לעתים שקט ונוח – אבל כשיש סערה, הטלטלה היא גדולה.
מקור: http://wallpoper.com/

סימני הסערה

החוויה החריגה הראשונה שנתקלנו בה – היא starvation: בקשות בשירותים השונים הממתינות בתור לקבל CPU (ליתר דיוק: להיות מתוזמנות ל process של ה Application Server – אולי אספר עוד בפוסט נפרד), וממתינות זמן רב – שניות. אותן קריאות שבד\"כ מטופלות בעשרות מילי-שניות.

כאשר מוסיפים nodes ל cluster, למשל 50%, 100% או אפילו 200% יותר חומרה – המצב לא משתפר: זמני ההמתנה הארוכים (שניות ארוכות) נותרים, ויש אחוז גבוה של כישלונות.

בדיקה מעמיקה יותר גילתה את הסיבה ל starvation: שירות A מבקש משהו משירות B, אך כ 3% מהקריאות לשירות B לא נענות תוך 10 שניות (להזכיר: זמן תגובה ממוצע הוא עשרות בודדות של מילי-שניות).
תוך זמן קצר, כמעט כל התהליכים עסוקים ב\"המתנות ארוכות\" לשירות B – והם אינם פנויים לטיפול בעוד בקשות.
כאשר כל טרנזקציה בשירות B מבצעת כ 3 קריאות לשירות A (\"מה זה משנה? – הן כ\"כ מהירות\"), הסבירות ל\"תקיעת\" הטרנזקציה על timeout עולה מ 1% לכ 3% – כאשר יש כ 1% התנתקויות של connections.

מדוע \"המזל הרע\" הזה? מדוע רובים של התהליכים דווקא עסוקים ב\"המתנות ארוכות\"?
התשובה היא הסתברותית: הם הצליחו לענות במהירות לכמה עשרות קריאות בהן הייתה המתנה קצרה (התנהגות רגילה) – ואז הגיעו ל\"המתנה הארוכה\" שלהם, אורכה של \"המתנה ארוכה\" היא כאורך טיפול כב 100-500 קריאות מהירות (תלוי ב endpoint הספציפי).

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

הפעולה המידית הייתה:

  • הפחתת ה timeouts ל-2 שניות, במקום 10 שניות. הקשר: השירות הכי אטי שלנו עונה ב 95% מהקריאות, גם בשעות עומס – בפחות מ 500 מילי-שניות.
  • צמצום מספר הקריאות בין השירותים: בעזרת caches קצרים מאוד (כ 15 שניות) או בעזרת קריאות bulk. למשל: היה לנו שירות D שבכל טרנזקציה ביצע 12 קריאות לשירות E, למה? – חוסר תשומת לב. שינוי הקוד לקריאת bulk לא הפך את הקוד למסורבל.
    בעצם ריבוי הקריאות, השירות בעצם הכפיל את הסיכוי שלו בפי-12, להיתקע על timeout כלשהו. קריאה אחת שמטפלת ב 12 הבקשות היא יעילה יותר באופן כללי, אך גם מצמצת את סבירות ההתקלויות ב timeouts.
    בכל מקרה: קריאה שלא נענתה בזמן סביר – היא כבר לא רלוונטית.
כששטים במים עמוקים – חשוב לבנות את כלי-השיט כך שיהיה יציב.
מקור: http://www.tutorvista.com/

המשך הפתרון

הסיפור הוא עוד ארוך ומעניין, אך אתמקד בעיקרי הדברים:

  • באופן פרדוקסלי, השימוש ב timeouts (כישלון מהיר) – מעלה את רמת היציבות של המערכת.
  • השימוש ב timeouts + צמצום מספר הקריאות המרוחקות, כמו שהיה במקרה שלנו, בעצם מקריב כ 1% מהבקשות (קיצרנו את ההמתנה ל-2 שניות, אך לא סיפקנו תשובה מלבד הודעת שגיאה) – על מנת להציל את 99% הבקשות האחרות, בזמני סערה (אין ל timeout השפעה כאשר הכל מתנהג כרגיל).

הקרבה של כ 1% מהבקשות הוא עדיין קשה מנשוא, ולכן אפרט את המנגנון שהתאמנו לבעיה.
זו דוגמה נפלאה כיצד דפוס העיצוב המקובל (למשל: Circuit Breaker) הוא כמעט חסר חשיבות – למקרה ספציפי שלנו (הכישלונות הם לא של השרת המרוחק – אלא בדרך הגישה אליו). אם היינו מחברים circuit breakers בכל נקודה במערכת – לא היינו פותרים את הבעיה, על אף השימוש ב \"דפוס עיצוב מקובל ל Fault-Tolerance\".

המנגנון שהרכבנו, בנוי מכמה שכבות:

  1. Timeouts – על מנת להגן על המערכת בפני starvation (ומשם: cascading failures).
  2. Retries – ביצוע ניסיון תקשורת נוסף לשירות מרוחק, במידה וה connection התנתק.
  3. Fallback (פונקציה נקודתית לכל endpoint מרוחק) – המספקת התנהגות ברירת מחדל במידה ולא הצלחנו, גם ב retry – לקבל תשובה מהשירות המרוחק.
  4. Logging and Monitoring – שיעזרו לנו לעשות fine-tune לכל הפרמטרים של הפתרון. ה fine-tuning מוכיח את עצמו כחשוב ביותר.
  5. Circuit Breakers – המנגנון שיעזור לנו להגיע ל Fallbacks מהר יותר, במידה ושרת מרוחק כשל כליל (לא המצב שכרגע מפריע לנו).

אני מבהיר זאת שוב: Timeouts ללא Fallback או Retries זו בעצם הקרבה של ה traffic. היא יותר טובה מכלום, במצבים מסוימים – אך זו איננה התנהגות רצויה כאשר מדובר בקריאות שחשובות למערכת.

בגדול מאוד, בכל קריאה מרוחק – הוספנו מנגנון שמתנהג בערך כך:

===================================================

  1. בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות, בד\"כ פחות).
    1. אם הקריאה הצליחה – שמור אותה ב fallback cache (הסבר – מיד).
  2. אם היה timeout – ספק תשובה מתוך ה fallback cache (פשרה).
===================================================

חשוב להבהיר: ה Fallback cache הוא Cache נפרד מ Cache רגיל (אם יש כזה). הוא ארוך טווח (כרגע: אנו מנסים 24 שעות), ולעתים הוא שומר מידע פחות ספציפי – כדי שיישאר נהיל ולא יהיה גדול מדי.

ע\"פ המדידות שלנו, בזמנים טובים רק כ 1 מ 20,000 או 25,000 קריאות תגיע ל fallback – כלומר משתמש אחד מקבל תשובה שהיא פשרה (degraded service).
בזמני סערה, אחוז הקריאות שמגיעת מתוך ה fallback מגיע ל 1 מ 500 עד 1 ל 80 (די גרוע!) – ואז הפשרה היא התנהגות משמעותית.

באחוזים שכאלו – הסיכוי שבקשה לא תהיה ב fallback cache היא סבירה (מאוד תלוי בשירות, אבל 10% הוא לא מספר מופרך). למקרים כאלו יש לנו גם התנהגות fallback סינתטית – שאינה תלויה ב cache.

מצב ברור שבו אין cache – כאשר אנו מעלים מכונה חדשה. ה caches שלנו, כיום, הם per-מכונה – ולא per-cluster ע\"מ לא להסתמך על קריאות רשת בזמן סערה. למשל: יש לנו שירות אחד שניסינו cache מבוזר של רדיס. זה עובד מצוין (גם ביצועים, ו hit ratio מוצלח יותר) – עד שפעם אחת זה לא עבד מצוין…. (והמבין יבין).

כלומר, המנגנון בפועל נראה דומה יותר לכך:

===================================================

  1. בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות, בד\"כ פחות).
    1. אם הקריאה הצליחה – שמור אותה ב fallback cache (הסבר מייד).
  2. אם היה timeout – ספק תשובה מתוך ה fallback cache (פשרה), או שתספק fallback סינתטי.
===================================================
איך מגדירים fallback סינתטי? זה לא-פשוט, ותלוי מאוד בתסריט הספציפי. כקו-מנחה יש לחשוב על:
  • ערכי ברירת-מחדל טובים.
  • התנהגות שתמנע מפיצ\'רים פחות חשובים לעבוד (למשל: לא להציג pop-up פרסומי למשתמש – אם אין מספיק נתונים להציג אותו יפה)
  • להניח שהמצב שאנו לא יודעים לגביו – אכן מתרחש (כן… המונית עדיין בדרך).
  • להניח שהמצב שאנו לא יודעים לגביו – התרחש בצורה הרעה ביותר (למשל: לא הצלחנו לחייב על הנסיעה)
  • אפשרות: הציגו הודעת שגיאה נעימה למשתמש ובקשו ממנו לנסות שוב. המשתמש לרוב מגיב בטווח של כמה שניות – זמן ארוך כ\"כ (בזמני-מחשב), שמספר דברים עשויים להשתנות לטובה במצב ה caches / הידע שלנו בפרק זמן שכזה.

תמונה מלאה יותר

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

כיוון שהבעיות הן בעיות של אקראיות (כמעט) – ל retry יש סיכוי גדול לעזור.
גילינו למשל, תוך כדי ניסיונות ו tuning את הדבר הבא: רוב הכישלונות שאנו חווים הם ברמת יצירת ה connection (\"לחיצת-יד משולשת\") והרבה פחות בעת קריאת נתונים ברשת.
עוד דבר שגילינו, הוא שלמרות שהצלחות בפתיחת connection מתרחשות ב 99.85% בפחות מ-3 מילי-שניות, הניסיון לעשות retry לפתיחת connection מחדש לאחר כ 5 מילי-שניות – כמעט ולא שיפר דבר. לעומת-זאת, ניסיון retry לפתיחת connection מחדש לאחר כ 30 מילי-שניות עשה פלאים – וברוב הגדול של הפעמים הסתיים ב connection \"בריא\".
האם מדובר בסערה שאיננה אקראית לחלוטין, או שיש לכך איזה הסבר מושכל שתלוי בסביבת הריצה (AWS, וירטואליזציה, וכו\')? קשה לומר – לא חקרנו את העניין לשורש. נסתפק בכך כ retry לאחר 30 מילי-שניות – משפר את מצבנו בצורה משמעותית.
עניין חשוב לשים לב אליו הוא שעושים קריאות חוזרות (retry) רק ל APIs שהם Idempotent, כלומר: לא יהיה שום נזק אם נקרא להם פעמיים. (שליפת נתונים – כן, חיוב כרטיס אשראי – לא).
שימוש ב Cache קצר-טווח הוא עדיין ואלידי ונכון
כל עוד לא מערבבים אותו עם ה fallback cache.
מכאן התוצאה היא:
===================================================

  1. בצע קריאה מרוחקת
    1. נסה קודם לקחת מה cache הרגיל
    2. אם אין – בצע קריאה מרוחקת (עם timeout של עד 2000 מילי-שניות לקריאה, בד\"כ פחות + timeout של 30ms ליצירת connection).
      1. אם היה timeout + במידת האפשר – בצע קריאה שנייה = retry.
      2. אם הקריאה הצליחה – שמור אותה ב fallback cache.
  2. אם לא הצלחנו לספק תשובה – ספק תשובה מתוך ה fallback cache (פשרה), או שתספק fallback סינטתי.
    1. ספק למשתמש חווית-שימוש הטובה ביותר למצב הנתון. חוויה זו ספציפית לכל מקרה.
===================================================
תהליך זה מתרחש לכל Endpoint משמעותי, כאשר יש לעשות fine-tune ל endpoints השונים.

שימו לב להדרגה שנדרשת ב timeouts: אם שירות A קורא לשירות B שקורא לשירות C – ולכל הקריאות יש timeout של 2000 מילי-שניות, כל timeout בין B ל C –> יגרור בהכרח timeout בין A ו B, אפילו אם ל B היה fallback מוצלח לחוסר התשובה של C.

בגלל שכל קריאה ברשת מוסיפה כ 1-3 מילי-שניות, ה timeouts צריכים ללכת ולהתקצר ככל שאנו מתקדמים בקריאות.
ה retry – מסבך שוב את העניין.

כרגע אנחנו משחקים עם קונפיגורציה ידנית שהיא הדרגתית (cascading), קרי: ה timeout בקריאה C <– B תמיד יהיה קצר מה timeout בקריאה בין B <– A.

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

באופן זה השירות שמקבל את הבקשה יוכל באופן מושכל לחלק את הזמן שניתן לו בין ה endpoint מהם הוא זקוק לתשובה, או שאולי אפילו יחליט לעובר ל fallback ישר – מחוסר זמן. זה פשוט… ומסובך – באותו הזמן.

כמובן שאת כל המנגנון יעטוף גם Circuit Breaker – זה קצת פחות דחוף עכשיו….

כך נראתה השמדה-עצמית בשנות השמונים.
מקור: http://tvtropes.org/

סיכום

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

מה שחשוב הוא להבין מה הבעיה – ולפתור אותה. לא לפתור את הבעיות שהיו ל Amazon, נטפליקס או SoundCloud.

באופן דומה, אני ממליץ לכם לא לרוץ ולממש את המנגנון שתיארתי – אלא אם אתם נתקלים בהתנהגות דומה.

אם למישהו מכם, יש ניסיון בהתמודדות דומה – אשמח לשוחח על כך. אנא כתבו לי הודעה או שלחו לי מייל (liorb [at] gett.com) ואשמח להחליף תובנות.

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

שיהיה בהצלחה!

לינקים רלוונטיים:

AWS Noiseness

HTTPS חלק ג': שיקולים ליישום באפליקציה

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

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

Default Port שונה

בניגוד ל HTTP בו פורט (port) ברירת המחדל הוא 80, ב HTTPS, פורט ברירת המחדל הוא 443. כלומר: אם ב URL של HTTPS לא צויין port – אזי מדובר בפורט 443.

Overhead

כפי שציינתי בפוסט הראשון בסדרה, ל TLS ,בסה"כ, אין תקורה משמעותית:

  • כ 6% יותר נתונים עוברים ברשת (Headers נוספים וחתימה דיגיטלית מוסיפים, TLS דוחס את ה HTTP headers – מה שקצת חוסך).
  • שרת מודרני, משקיע כ 2.5ms זמן CPU לצורך TLS handshake של מפתח א-סימטרי ארוך (2K) – לא זמן משמעותי. בעבר היה מקובל להוציא את משימת ה TLS handshake לחומרה חיצונית (Load Balancer או CDN) שעשתה עבודה זו בעלות נמוכה יותר – אך נראה שמגמה זו פוחתת.
  • עבודת ה CPU בצד הלקוח היא לא משמעותית, אולי מלבד מכשירי מובייל (?!)
הנקודה היחידה בה יש הבדל ביצועים משמעותי בין HTTP ל HTTPS הוא יצירת ה connection ההתחלתי.

בפוסט הקודם הסברתי על מנגנון שנקרא Session Ticket שפוטר מהצורך שלנו לבצע TLS handshake לכל connection, כך שבסך הכל אנו יכולים לצפות ל 2 roundtrips נוספים לכל origin איתו אנו יוצרים קשר.

האמנם?

מקור: הבלוג של איליה גריגוריק. שווה לעקוב.

פעמים רבות תתקלו ב TLS שהעלות שלו גדולה מ"התאוריה".

הנה, בתרשים למעלה, רואים גישה לאותו המשאב ב HTTP (שורה עליונה) מול HTTPS (שורה תחתונה), בהינתן RTT של כ 380ms.
ע"פ התאוריה, ניתן לצפון לעלות נוספת של 760ms (פעמיים 380ms) בביצוע TLS handshake, אבל הנה בדוגמה למעלה, ההפרש בפועל הוא כמעט 2000ms, כמעט פי 3 (!) ממה שציפינו.

בפוסט מעניין ויפה איליה גריגוריק מראה כיצד הוא מנתח ומתקן את המצב המתואר לעיל. הוא מזהה, ומתקן, "באפר" (congestion window) שקונפג בשרת בצורה שלא מטיבה עם ה TLS Handshake ואז הוא מוסיף עוד אופטימיזציה של TLS בכדי לצמצם את התקורה ל roundtrip בודד. סה"כ 2000ms הופכים ל 380ms – שיפור מרשים מאוד!
לא צריך להכיר את nginx (השרת המדובר) בכדי לקרוא את הפוסט.

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

עוד גורם שיכול לעכב קריאות HTTPS הוא הפעלה של פרוטוקול בשם Online Certificate Status Protocol (בקיצור: OCSP). זהו פרוטוקול המגביר את אמינות וידוא הזהות של HTTPS – במחיר ביצועים. ע"פ הפרוטוקול, כאשר הדפדפן מקבל Certificate הוא פונה ל CA שהנפיק אותו ומבקש לוודא שה Certificate עדיין תקף. סיבה ש Certificate לא יהיה תקף הוא אם המפתח הפרטי שהיה בשימוש ליצירת ה Certificate נגנב איכשהו, או שהתגלה שבעל ה Certificate הוא מתחזה / פועל בצורה לא כשרה. בחלק ב' של הפוסט שעסק ב Certificates לא כיסיתי את נושא ביטול (revocation) של Certificates – מפאת חוסר מקום / עניין נמוך לקורא.
אם המשתמשים שלכם מפעילים OCSP – אתם יכולים לבדוק את ה CA שלכם, עד כמה הוא מהיר בתגובות שלו לבקשת OCSP באזורים הגאוגרפים הרלוונטיים עבורכם. במידת הצורך, ניתן להחליף CA באחד זמין / מהיר יותר.

שווה לציין ששירותי (CDN (Content Delivery Networks, יכולים לקצר את זמני ה TLS handshake. ל CDNs יש תחנות הקרובות גאוגרפית למשתמש הקצה (ולכן יש להן latency נמוך יותר). משתמש הקצה מבצע TLS handshake מול התחנה הקרובה, של ה CDN, בה כל round-trip הוא זול יותר. מרגע שנוצר ה connection התחנה משתמשת כפרוקסי (מאובטח) לשרת שלכם (על גבי connection מאובטח שנפתח מבעוד מועד) או סתם מעבירה לכם TLS Session Ticket שאתו תוכלו לעבוד. ייתכן ובכלל התוכן זמין על cache של ה CDN ואין טעם להגיע בכלל לשרת היעד.
שירות זה נקרא TLS Early Termination, כאשר הכוונה במילה Termination היא "קיצור המסלול" ולא "סיום התקשורת".
שירות זה, עולה כסף – כמובן, ולא תמיד הוא זמין בכל אתר (או Point of Presence, בקיצור PoP) בה ה CDN פעיל.

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

Connection Termination

מתי שהוא, סיימנו את העבודה המאובטחת מול השרת, ואנו רוצים לסיים את תקשורת ה HTTPS.
דרך מחשבה אחת אומרת כך: "HTTPS חי על גבי TCP, לכן פשוט אפשר לסיים את connection ה TCP בעזרת הודעת TCP FIN – ולסיים עניין".
אולם, הודעת TCP FIN יכולה בעצם להישלח ע"י כל אחד, גם תוקף פוטנציאלי המנסה לשבש את התקשורת. לצורך כך נקבע שתקשורת TLS תסתיים ע"י הודעה ברמת ה TLS (הדלקת flag בשם close_notify) ורק לאחר מכן TCP FIN.

לא לבלבל נושא זה עם Early Termination.

הימנעות מ Mixed Content

אם אתם זוכרים את הפוסט הקודם, נרצה להימנע באתר שלנו מ mixed content: תוכן HTTP בתוך דף HTTPS.
כיצד עושים זאת?
הרבה פעמים ראיתי שפשוט כותבים קוד דומה לזה:

var url = location.url.startsWith('https')) ? HTTPS_URL : HTTP_URL 

זה עובד, אבל זה מעט מסורבל. ניתן פשוט לכתוב url יחסי יחיד שלא מגדיר פרוטוקול – והוא ישתמש, בהגדרה, בפרוטוקול של הדף, יהיה זה HTTP או HTTPS. למשל:

var url = "//myserver/resources/style.css";

זה נראה קצת מוזר, אבל זה תקני לחלוטין. ניתן למצוא עוד פרטים על URL יחסי בפוסט שלי על URLs.
מעניין לציין ששירותים שונים (למשל Google Libraries API) מגישים לנוחיותכם את אותם המשאבים גם על גבי HTTP וגם על גבי HTTPS – וממליצים פשוט להשתמש בסכמה שתארתי.

כדאי לציין שחלק משירותי האירוח יציעו לינק HTTPS כברירת מחדל (כדי להבטיח שקיבלתם מהם אכן את הקובץ שביקשתם, ולא ממתחזה). HTTPS בתוך HTTP לא יוצר בעיית "Mixed Content" – אך הוא פוגע בביצועים של הדף (כפי שצוין למעלה בפוסט).

HTTPS כפרוטוקול "VPN"

למרות ש HTTPS הוא לא פרוטוקול VPN, יש לו יתרון שהוא "עובר מסך" אצל firewalls ורכיבי רשת שונים בקלות יחסית:
"אם הצלחת ליצור קשר על גבי TLS עם השרת שלי – אני סומך עליך. אפילו שאין לי מושג (או יכולת לדעת) מה אתם מדברים ביניכם" אומר ה Firewall או ה Transparent Proxy לעצמו.

לאחר שה TLS Connection נוצר הוא מוצפן – ולכן הוא שקול לפרוטוקול בינרי, low level, בין הצדדים. לדוגמה: ניתן להפוך את כיוון התקשורת, להעביר מידע בינרי, להעביר הודעות בחלקים לא מסודרים וכו'. סוג של "TCP על גבי HTTPS" (ה HTTPS משמש בעיקר ל handshake, אח"כ ניתן לרדת חזרה לרמת ה TLS היותר בסיסית).

תכונה זו של HTTPS בשימוש ע"י רכיבים של Content Delivery Networks, תקשורת בין Cloud למערכת ארגונית, ופרוטוקולים כגון SPDY (שהופך להיות HTTP 2.0) ו Web Sockets.

קצרים

  • האם זה נכון שהדפדפן לא עושה Caching לתוכן HTTPS?
    לא. לכו בדפדפן ל about:cache לראות בעצמכם. אפשר בעזרת HTTP Headers מתאימים להגביל את ה Cache ואת אופן השמירה שלו ע"י הדפדפן / ה CDN.
  • האם רכישת Certificate הוא דבר יקר? אלפי דולרים בשנה?
    תלוי בסוג ובמקור ה Certificate, אבל זה יכול להיות גם עשרות דולרים בשנה.
  • האם זה נכון ש HTTPS מונע Virtual Hosting (לארח כמה אתרים עם Hostnames שונים על אותו שרת פיסי)?
    לא. עקרונית יש בעייה, אבל פותרים אותה בעזרת הרחבה ל TLS בשם Server Name Indication (בקיצור SNI). אולי יש פתרונות נוספים.
  • האם HTTPS מבטיח פרטיות למשתמש הקצה?
    נו, אתם אמורים לענות על זה לבד בשלב זה: הוא מגן מפני sniffing ברשת, או בפני התחזות (phishing) – אבל אין לו קשר לאיזה מידע השרת שומר עליכם, כיצד הוא מגן על מידע זה או מה הוא עושה אותו (אולי הוא מפרסם אותו לכולם בגוגל על גבי HTTP …?)

סיכום

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

שיהיה בהצלחה!

—-

לינקים רלוונטים:

אבני הבנין של האינטרנט: SOP ותקשורת Cross-Domain

כלל ה Same Origin Policy, או בקיצור SOP, הופיע לראשונה בדפדפן Netscape 2.0. הוא הופיע בד-בבד עם שפת התכנות JavaScript וה (Document Object Model (DOM. כמו JavaScript וה DOM, הוא הפך לחלק בלתי-נפרד מהדפדפן המודרני.

SOP הוא עקרון האבטחה, כנראה המרכזי ביותר בדפדפן. הרעיון הוא דיי פשוט: שני Execution Contexts שונים של ג\'אווהסקריפט לא יוכלו לגשת על ה DOM אחד של השני אם הם לא מגיעים מאותו המקור (origin).
מתי יש לנו Execution Contexts שונים? כאשר יש iFrames שונים או טאבים שונים.

איך מוגדר origin? שילוב של שלושה אלמנטים:

  • סכמה / פרוטוקול (למשל http או https)
  • Fully Qualified Domain Name (בקיצור: FQDN, למשל news.google.co.il)
  • מספר port [א].

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

עולם האינטרנט השתנה מאז 1995, והיום יש הרבה יותר אינטראקציה משותפת (mashups) בין אתרים שונים. SOP מגביל את היכולת שלנו לשתף מידע בין מקורות שונים.

אז מה עושים? האם נחרץ גורלו של האתר שלנו להיות מוגן, אך מבודד – לעד?

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

פוסט זה שייך לסדרה אבני הבניין של האינטרנט.

תזכירו לי מה המשמעות של SOP?

נניח שדף ה HTML של האפליקציה / אתר שלנו נטען מהכתובת http://www.example.com/home/index.html.
משמע: ה origin הנוכחי שלנו הוא http://www.example.com:80.

הנה המשמעות של מדיניות ה SOP בנוגע לגישה ל origins אחרים (\"Compared URL\"):

מקור: http://en.wikipedia.org/wiki/Same_origin_policy

הערה מעניינת: SOP, כמו מנגנוני הגנה אחרים של הדפדפן, מתבססים על ה FQDN ללא אימות מול ה IP Address. המשמעות היא שפריצה לשרת DNS יכולה להשבית את ה SOP ולאפשר קשת רחבה ויצירתית של התקפות על המשתמש.

SOP הוגדר במקור עבור גישה ל DOM בלבד, אך עם השנים הוא הורחב:

XMLHttpRequest (המשמש לקריאות Ajax) מוגבל גם הוא. קריאות Ajax יוגבלו רק ל origin ממנו נטען המסמך (כלומר: קובץ ה HTML). בנוסף, בעקבות היסטוריה של התקפות שהתבססו על \"זיוף\" קריאות ל origin הלגיטימי עם Headers מטעים – נוספו מספר מגבלות על Headers אותם ניתן לשנות בקריאות Ajax (למשל: Referer או Content-Length וכו\').

Local Storage (יכולת חדשה ב HTML5 לשמור נתונים באופן קבוע על הדפדפן) גם היא מוגבלת ע\"פ ה SOP. לכל origin יש storage שלו ולא ניתן לגשת ל storage של origin אחר.

Cookies – האמת שמגבלות על Cookies החלו במקביל להתפתחות ה SOP. התוצאה: סט מגבלות דומה, אך מעט שונה. הדפדפן לא ישלח Cookies לדומיין אחר (למשל evil.com) אך הוא ישלח את ה cookies לתת-domain למשל:
evil.www.example.com, תת-domain של www.example.com.
בדפדפנים שאינם Internet Explorer, ניתן לדרוש אכיפה של domain מדויק ע\"י השמטה של פרמטר ה domain (תיעוד ב MDN, קראו את התיאור של הפרמטר domain).

כיוון ש Cookie יכולים להכיל מידע רגיש מבחינת אבטחת מידע (למשל: אישור גישה לשרת), הוסיפו עליהם עוד 2 מנגנוני הגנה נוספים:
httponly – פרמטר ב HTTP header של set-cookie שהשרת יכול לסמן, המונע גישה מקוד ג\'אווהסקריפט בצד-הלקוח ל cookie שטמן השרת. כלומר: ה cookie רק יעבור הלוך וחזור בין השרת לדפדפן, בלי שלקוד הג\'אווהסקריפט תהיה גישה אליו.
secure – פרמטר בצד הג\'אווהסקריפט של יצירת cookie (שוב, התיעוד ב MDN) שאם נקבע ל true – יגרום לדפדפן להעביר את ה cookie רק על גבי תקשורת HTTPS (כלומר: מוצפנת).

Java Applet, Flash ו Silverlight כוללים כללים שונים ומשונים הנוגעים ל SOP. חבורה זו היא זן נכחד – ולכן אני מדלג על הדיון בעניינה.

SOP מתיר חופש במקרים הבאים:

Cross domain resource loading – שימו לב, זהו כלל חשוב: הדפדפן כן מאפשר לטעון קבצים מ domains אחרים. לדוגמה: קבצי ג\'אווהסקריפט, תמונות, פונטים (בעזרת font-face@) או קבצי וידאו. על קבצי CSS יש כמה מגבלות [ב]. כלומר: האתר שלנו, http://www.example.com יכול בלי בעיה לטעון קובץ ג\'אווהסקריפט מאתר אחר suspicious.com. טעינת הג\'אווהסקריפט הינה הצהרת אמון במקור – ועל כן קוד הג\'אווהסקריפט שנטען מקבל את ה origin שלנו ועל כן הוא יכול לגשת ל DOM שלנו ללא מגבלה. מצד שני: הוא אינו יכול לגשת ל DOM של iFrame אחר שמקורו מ suspicious.com או לבצע קריאות Ajax ל suspicious.com – למרות שהוא נטען משם.

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

Location – סקריפט שמגיע מ origin שונה מורשה לבצע פעולות השמה (אך לא קריאה) על אובייקט ה Location (ה URL הנוכחי של ה frame) כגון ()location.replace. זה נשמע מעט מוזר, אך הסיבה לכך היא לאפשר שימוש בטכניקה בשם iFrame busting הנועדה להילחם בהתקפה בשם clickjacking. כלומר: כדי להבטיח שאתר זדוני לא מארח את האתר שלנו ומראה רק חלקים מסוימים ממנו כחלק מהונאה, מותר לנו לגשת לכל frame אחר בדף (למשל ה Top Frame) ולהפוך אותו לכתובת האתר שלנו. התוצאה: האתר המארח יוחלף באתר שלנו – ללא אירוח.
דרך מודרנית יותר למניעת clickjacking היא שימוש ב HTTP Header בשם X-Frame-Options.

מתקפת clickjacking בפעולה. מקור.

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

טכניקות התמודדות

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

רשימת הטכניקות אותן נסקור בפוסט זה
בגדול ניתן לחלק את הטכניקות ל2 משפחות:
  1. תקשורת בין Frames מ Origins שונים באותו הדף.
  2. תקשורת בין לקוח לשרת ב Origin שונה (כלומר: קריאות Ajax).
אני אפתח במשפחה הראשונה, למרות שהשימוש בה פחות נפוץ בימנו – מכיוון שחלק מהטכניקות שלה מהוות בסיס לטכניקות במשפחה השנייה.

Cross-Origin inter-Frame Communication




להלן נסקור מספר טכניקות מקובלות כדי לתקשר בין iFrames מ origins (או domains) שונים.



Domain Relaxation – הטכניקה הזו אופשרה ע\"י הדפדפנים.
בטכניקה זו קוד הג\'אווהסקריפט משנה את ה origin הנוכחי ע\"י השמת ערך חדש למשתנה: document.domain.
המגבלה: הוא יכול רק \"לקצץ\" תתי-domains ולא להחליף את ה domain לגמרי. לדוגמה:
  • מ \"login.example.com\" ל \"example.com\" – מותר.
  • מ \"login.example.com\" ל \"login.com\" – אסור.
  • כמו כן, מ \"login.example.com\" ל \"com\" – מותר, אבל מסוכן!!
התוצאה של פעולת ה domain relaxation היא הבעת אמון גדולה בכל אתר מה domain המעודכן, והרשאה לג\'אווהסקריפט של אותו האתר לערוך את ה DOM שלנו. אם אפליקציה זדונית שאנו מארחים ב iFrame, מסוגלת לבצע Domain Relaxation לאותו Relaxed Domain כמו שלנו – היא יכולה לגשת ל DOM שלנו ללא מגבלה.
חשוב לציין ש Domain Relaxation לא משפיע על אובייקט ה XmlHttpRequest. קריאות Ajax יתאפשרו רק ל origin המקורי ממנו נטען ה Document שלנו.
בנוסף, בחלק מהדפדפנים Domain Relaxation ישפיע רק על גישה בין Frames באותו הטאב ולא על גישה בין טאבים נפרדים.
סיכום: Domain Relaxation היא טכניקה נוחה בתוך ארגון בו כל המחשבים הם תחת דומיין-על אחיד, אך היא לא נחשבת לבטוחה במיוחד. שימוש ב Domain Relaxation פותח פתח למרחב לא ידוע של תתי-domains שאנו לא מכירים – לגשת ל DOM שלנו.

Encoding Messaged on the URL Fragment Id

טריק מלוכלך זה מתבסס על 2 עובדות:

  • SOP מתיר ל Frame אחד לשנות את ה Location (כלומר URL) של Frame כלשהו אחר בדף.
  • שינוי של FragmentID (החלק ב URL שלאחר סימן ה #) לא גורם ל reload של המסמך (כפי שהסברנו בפוסט על ה URL)
התרגיל עובד כך: פריים (frame) א\' משנה את ה FID (קיצור של Fragment ID) של פריים ב\'. הוא מקודד הודעה כלשהי שהוא רוצה להעביר.
פריים ב\' מאזין לשינויים ב FID שלו, מפענח את ההודעה ומחזיר תשובה ע\"י קידוד הודעה ע\"י שינוי ה FID של פריים ב\'.
וריאציה נוספת של הטכניקה הזו היא שכל פריים משנה את ה window.name של עצמו. SOP מתיר ל frames שונים לקרוא את ה property הזה מ frames אחרים ללא הגבלה.
סיכום: מלוכלך, מוגבל, לא בטוח (אני לא ידוע מי באמת שינה לי את ה FID) – אבל יכול לעבוד.
ישנן עוד כמה טכניקות דומות (בעזרת Flash, למשל) – אך אני אדלג עליהן.

Post Message

בשלב מסוים החליטו הדפדפנים לשים סוף לבאלגן. כשהרבה מפתחים משתמשים בטכניקות כגון ה Encoding על ה FID, עדיף פשוט לאפשר להם דרך פשוטה ובטוחה. כאן נכנסת לתמונה יכולת ה \"Post Message\", שהוצגה כחלק מ HTML5.

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

מנגנון ה Post Message (בקיצור: PM) מאפשר לשלוח הודעות טקסט בין iFrames או חלונות שונים בדפדפן, בתוספת מנגנון אבטחה שמגביל את שליחת / קבלת ההודעות ל domain ידוע מראש.

הנה דוגמה:

כדאי לציין שניתן לשלוח הודעה עם target domain של \"*\" (\"לכל מען דבעי\"). לא ברור לי מדוע איפשרו יכולת זו – אבל היא מדלגת על מנגנון האבטחה החשוב של PM. כמו כן חשוב לבצע בדיקת domain בקבלה ולבדוק רק Domain מדויק.
מימוש אפשרי הוא בדיקה חלקית, למשל:

if (msg.origin.indexOf(\".example.com\") != -1) { … }

מזהים מה הבעיה פה?
תוקף מ Domain בשם example.com.dr-evil.com (השייך לחלוטין לדומיין dr-evil.com) יוכל גם הוא לשלוח לנו הודעות!

סיכום: טכניקה בטוחה וקלה לשימוש. כדאי תמיד להעדיף אותה – אם היא זמינה.

שווה לציין ספריה בשם EasyXDM לשליחת הודעות Cross Domain.
EasyXDM ישתמש ב Post Message, אם הוא זמין (IE8+) או יבצע fallback למגוון שיטות היכולות לעבוד על IE6 ו IE7 – אם אין לו ברירה. מומלץ להשתמש רק אם תמיכה ב IE6-7 היא חובה.

Cross-Origin Client-To-Server Communication

סקרנו את המשפחה הראשונה של הטכניקות: תקשורת בין Frames בדפדפן. טכניקות אלו שימושית כאשר אנו מארחים אפליקציות או widgets ב iFrame.

הצורך הנפוץ יותר הוא בביצוע קריאות לשרת שנמצא ב origin שונה. תזכורת: אם האפליקציה שלנו נטענה מ origin A והיא תנסה לבצע קריאת Ajax ל origin B – הקריאה לא תצא, הדפדפן יחסום אותה.

בואו נעבור על מספר טכניקות מקובלות המאפשרות לבצע קריאת Ajax ל originB – וננסה לעשות זאת בצורה מאובטחת ככל האפשר.

Server Proxy

כאשר יש לנו דף / document המשויך ל Domain A, אין לו בעיה לייצר קריאות Ajax ל Domain A – הרי זה ה origin שלו. אם אנו מנסים להוציא קריאת Ajax ל Domain B (כלומר: domain אחר), הדפדפן יחסום את הקריאה כחלק ממדיניות ה SOP:

אפשרות אחת להתמודד עם הבעיה היא לבקש מהשרת ב Domain A, לשמש עבורנו כ \"Proxy\" ולבצע עבורנו את הקריאה:

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

וריאציה אחרת של שיטה זו היא להציב Reverse Proxy בין הדפדפן ל 2 השרתים. ניתן לקבוע חוקים ב Reverse Proxy שייגרמו ל-2 השרתים להראות כאילו הם באותו ה Domain.

גישת ה Server Proxy היא פשוטה, אולם יש לה כמה חסרונות:

  • השרת צריך לבצע עבודה נוספת של העברת הודעות בין הדפדפן לשרת ב\' , מה שיכול בהחלט לפגוע לו ב Scalability (עוד חומרה = עוד כסף). במקרה של Reverse Proxy – העלות הנוספת היא ברורה יותר.
  • שרת ה Proxy שלנו הוא לא דפדפן, הוא לא יעביר באופן טבעי User Agent או Cookies, אלא אם נוסיף קוד שיעשה זאת.
  • \"הערמנו\" על הדפדפן ועל מנגנון האבטחה שלו, אבל האם יצרנו אלטרנטיבה בטוחה מספיק? (האם יצרנו בכלל אלטרנטיבה בטוחה במידה כלשהי?)
סיכום: פתרון פשוט אבל בעייתי: יש לשים לב למחיר הנוסף ב Scalability, ולוודא שלא יצרנו פרצת-אבטחה.
JSONP (קיצור של JSON with Padding)
טכניקת ה JSONP מבוססת על העובדה הבאה:

  • SOP לא מגביל אותנו לטעון קבצי JavaScript מ Domains אחרים.

מה היה קורה אם קוד הג\'אווהסקריפט, שנטען מ Domain אחר, היה כולל קוד שיוצר במיוחד עבור הקריאה שלנו? למשל, קוד המבצע השמה של שורת נתונים לתוך משתנה גלובלי שאנו יכולים לגשת אליו? – משמע שהצלחנו להעביר נתונים, Cross-domain, לדפדפן!

הנה דוגמת קוד כיצד אנו קוראים מצד-הלקוח ל API מסוג JSONP:

והנה התשובה שהשרת מייצר (דינמית) = הקובץ info.js:

הערך \"jsonpCallBack\" הגיע כפרטמר על ה URL של ה Request, ושאר הנתונים הגיעו מהשרת (מבנה נתונים, DB וכו\').
הקוד הנוסף שעוטף את ה data, בין אם זו השמה למשתנה גלובלי או קריאה ל callback ששמו נשלח – נקרא \"Padding\". זהו ה P בשם JSONP.
לרוב אנו נעדיף Padding מסוג callback, מכיוון ש callback מייצר trigger ברגע המידע חזר מהשרת. כאשר משתמשים ב Padding מסוג \"השמה למשתנה גלובלי\" אנו נאלץ לדגום את המשתנה שוב ושוב בכדי לדעת מתי הוא השתנה…

ל JSONP יש מספר חסרונות:

  • נדרשת תמיכה מהשרת: על השרת לייצר Padding מתאים לנתונים. ה Padding חייב לקרות בצד השרת ואין דרך \"להשלים\" אותו בצד הלקוח עבור קובץ ג\'אווהסקריפט שלא כולל אותו.
  • מכיוון שלא ניתן לטעון קובץ ג\'אווהסקריפט יותר מפעם אחת, אם אנו רוצים לבצע מספר קריאות JSONP יהיה עלינו לייצר שם חדש לקובץ ה script בכל קריאה = מעמסה.
  • JSNOP מוגבל לפעולות HTTP GET בלבד (לא ניתן לטעון scripts במתודת HTTP אחרת) – עובדה שמונעת מאתנו להשתמש ב JSONP כדי לקרוא ל REST API.
  • אין Error Handling. אם קובץ הג\'אווהסקריפט לא נטען (404, 500 וכו\') – אין לנו שום דרך לדעת זאת. פשוט לא יקרה שום דבר.
  • אבטחת מידע: השרת ממנו אני טוען את הנתונים ב JSONP לא מעביר רק נתונים – הוא מעביר קוד להרצה. אני צריך לסמוך על השרת הזה שלא ישלח לי קוד זדוני.

יתרון משמעותי של JSONP הוא הפשטות (היחסית) כאשר יש לנו קריאה בודדת והעובדה שהוא יעבוד גם עם דפדפנים ישנים. ניתן למצוא הרבה WEB APIs של חברות חשובות כגון גוגל או טוויטר – החשופים ב JSONP.

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

iFrame Proxy / Relay
טכניקת ה iFrame Proxy (או iFrame Relay) מבוססת על תקשורת בין iFrames, וספציפית על Post Messages.
המנגנון עובד כך:

  1. פתח iFrame שה URL שלו מצביע עד דף (שהכנו מראש) הנמצא על הדומיין איתו אנו רוצים לתקשר. ה origin של המסמך ב iFrame יהיה אותו ה domain.
  2. הדף הנ\"ל יטען קובץ ג\'אווהסקריפט (שהכנו מראש), נקרא לו proxy.js.
  3. נבצע קריאות Post-Message ל iFrame שייצרנו. proxy.js יאזין לקריאות אלו, כאשר מנגנון ה Post-Message מספק לנו אבטחה.
  4. proxy.js יבצע קריאת Ajax לדומיין המרוחק, אין לו מגבלות – כי הדומיין הזה הוא ה origin שלו.
אם השרת מחזיר תשובה, היא תגיע ל Proxy.js.
  1. proxy.js מקבל את התשובה מהשרת ומבצע Post-Message בחרזה ל Frame / לקוד שלנו.
לגישת ה iFrame Proxy יש כמה חסרונות:
  • היא מורכבת למימוש.
  • נדרשת תמיכה מצד השרת.
  • היא דורשת תמיכה ב PM, קרי IE8+.
מצד שני היא טובה מבחינת Security:
  • בעזרת מנגנון ה PM אנו מוודאים ש proxy.js מגיע מה domain הרצוי.
  • proxy.js מבודד בתוך iFrame ואינו יכול להשפיע על הקוד שלנו – כך שלא חייבים לסמוך ב 100% על שרת היעד.
סיכום: אופציה מורכבת למימוש – אך טובה מבחינת Security.

CORS (קיצור של Cross Origin Resource Sharing)
מאחר ו JSONP ו iFrame Proxy הם אלתורים, החליטו הדפדפנים לפתור את בעיית הקריאה לשרת cross-domain בצורה שיטתית. התוצאה היא פרוטוקול ה CORS.

CORS מאפשר לנו בקלות יחסית לבצע קריאת \"Ajax\" לשרת בדומיין אחר. השרת צריך לממש מצדו את הפרוטוקול (כמה כללי התנהגות ו Headers על גבי HTTP).

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

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

גרסת ה IE הראשונה שממשה את CORS היא IE10 (וגם לו יש באג משמעותי – הוא לא שולח cookies כמו שצריך).
בפועל התמיכה ב CORS מתחילה מ IE10 אם אינכם זקוקים ל cookies, ואם אתם זקוקים ל Cookies – היא מתחילה ב IE11. סוג האפליקציות שיכולות להסתדר עם תנאי סף שכאלו הם בעיקר אפליקציות מובייל.

עדיין אפשר לבצע מימוש יותר מורכב שישתמש ב XDomainRequest ב IE8-10 וב CORS בכל שאר המקרים.

חסרונות סה\"כ:

  • ה API של CORS מעט מסורבל, מימוש בצד השרת דורש מעט עבודה.
  • CORS עשוי להזדקק ל 2 HTTP Requests (מה שנקרא \"preflight\") בכדי להשלים קריאה בודדת. אלו הדקויות של המימוש הפנימי.
  • התמיכה של IE היא בעייתית.
ל CORS יש גם יתרונות:
  • סטנדרטי
  • אבטחה טובה
  • לא מצריך להמציא מנגנון שלם, כגון iFrame Proxy.
סיכום: אופציה טובה, שתהיה טובה יותר בעוד מספר שנים.

סיכום

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

סקרנו והשוונו את הטכניקות הנפוצות לביצוע תקשורת Cross-Domain תחת מנגנון ה SOP, טכניקות מ-2 משפחות:

  • תקשורת בין iFrame ל iFrame.
  • תקשורת מול שרת ב Domain אחר.
סה\"כ, הבנת נושא ה SOP היא חשובה למדי עבור מערכות המתקשרות בין Domains שונים, צורך ההולך וגובר עם השנים.
שיהיה בהצלחה!

—-

[א] דפדפן IE לא מחשיב את ה port כחלק מה origin עבור גישה ל DOM. בפועל נדירים מ המקרים בהם דף HTML ייטען מ port לא סטנדרטי.

[ב] המגבלות שונות מדפדפן לדפדפן: IE, פיירפוקס, כרום, ספארי (יש לגלול ל CVE-2010-0051) ואופרה (לפני השימוש ב blink). אינני מתחייב שהרשימה מלאה ו/או מעודכנת.

ביצועים של אפליקציות ווב: מבוא לצד האפל

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

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

פוסט זה מתארח בסדרה: אבני הבניין של האינטרנט.

פשט

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

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

הערה: כמובן שבעת שעוסקים ברמה של קישור בין מערכות, ב High Level – זו רמת הפשטה נאותה.

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

האנטומיה של קריאת HTTP

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

פרוטוקול HTTP הוא פרוטוקול אפליקטיבי – הוא שולח נוסח טקסט מוסכם מראש המתאר התנהגות אפליקטיבית על גבי פרוטוקול TCP.

פרוטוקול TCP – הוא פרוטוקול לניהול תקשורת (Transmission Control Protocol) שמנהל Connection לשליחת רצף של הודעות, דואג שכולן יגיעו ושיגיעו בסדר הנכון.
מי שעושה באמת את העבודה הוא פרוטוקול (IP (Internet Protocol שאחראי לשליחת הודעות, ללא הבטחת סדר וללא אמינות.

כל קריאה של פרוטוקול IP היא בעצם הודעה שעוברת ממחשב אחד (לקוח) למחשב אחר (שרת). למרות שנדמה מהתרשים לעיל שהייתה בעצם הודעה אחת ותשובה אחת בין הלקוח לשרת – בפועל, ברמת ה IP, היו הרבה קריאות ותשובות
.
הנה תיאור מפורט / מדויק יותר של מה שמתרחש בפועל:

תרשים 1: קריאת HTTP לשרת כפי שהיא מתבצעת בפועל
DNS Lookup
כשאנחנו מקלידים כתובת (למשל http://www.ynet.co.il) יש קודם כל ללמוד מה כתובת ה IP של שרת היעד לפני שאנו יכולים לשלוח לו הודעות. כתובות אלו הן דינמיות ועשויות להשתנות לאורך הזמן. תהליך ההפיכה של הכתובת הטקסטואלית (hostname) לכתובת IP היא תהליך שנקרא DNS Lookup. בפועל הדפדפן שלנו פונה לשרת (Domain Name Servers (DNS, שהוא קורא לסדרה של שרתים אחרים, כל אחד בתורו, בכדי לפרש את הכתובת. תחילה קוראים לשרת ה ROOT כדי ללמוד על כתובת ה IP של שרת ה "il" (מפרשים את כתובת ה hostname מהסוף להתחלה). בשלב הבא עלינו לפנות לשרת "il" וללמוד ממנו את כתובת ה IP של שרת ה "co" (קיצור של commercial – הרי ynet הוא אתר מסחרי) ורק אז מקבלים את הכתובת ה IP של השרת של YNET בכבודו ובעצמו.
תהליך ה DNS Lookup לוקח בממוצע 130ms (עבור כל השרתים שבדרך). התקשורת נעשית באמצעות פרוטוקול UDP שהוא מהיר יותר מ TCP – אך איננו אמין. הסטטיסטיקה אומרת שבכ 5% מתהליכי ה DNS Lookup הודעה כלשהי הולכת לאיבוד. על הדפדפן לזהות מצב זה (ע"י קביעת timeout) ולשלוח הודעה מחדש – מה שיכול לגרום לתהליך ה DNS Lookup להתעכב זמן רב (עד כשתיים-שלוש שניות). כל זאת לפני שעוד בכלל ביצענו קריאה לשרת שלנו.
Establish TCP Connection
יצירה של TCP Connection כוללת תהליך שנקרא "לחיצת יד משולשת" שבמהלכו מתבצעות 3 קריאות של פרוטוקול IP רק כדי להסכים על ה connection. רק לאחר 3 קריאות אלו ניתן בעצם לשלוח את הודעת ה HTTP. בתקשורת מאובטחת (קרי HTTPS) יצירת ה connection מערבת "לחיצת ידיים" ארוכה בהרבה.
תיאור סכמטי של ה Three Way Handshake
HTTP Request and Response
רק לאחר שהוקם ה TCP Connection ניתן לשלוח הודעה אפליקטיבית (HTTP) לשרת. השרת מבצע את הקוד הנדרש (קורא לבסיס הנתונים או מבצע לוגיקה) ומחזיר את התשובה. זמן זה מסומן בתרשים 1 כ Server Time. יש לזכור שהשרת משקיע עבודה גם ביצירת ה TCP Connection ושליחת ההודעה עצמה – מה שמצוין בתרשים 1 כצבע הירוק ברקע.
ה HTTP Request הוא לרוב טקסט קצר למדי (כמה שורות בודדות) בעוד שהתשובה היא לרוב ארוכה למדי (קובץ HTML או קובץ JPG למשל) – ולכן אורכת זמן רב יותר.
נתון מעניין הוא ש "אינטרנט מהיר יותר" -כמעט ולא משפר גלישה רגילה באינטרנט. האינטרנט המהיר מאיץ רק את החלק של ה "HTTP Response" וכפי שניתן לראות בתרשים 1 – זהו חלק קטן מהזמן הכולל.
הערה: חיבור אינטרנט מהיר הוא כן משמעותי כאשר מדובר בהורדה של קבצים גדולים או צפייה בוידאו.
Rendering Time on the Browser
כאשר דף ה HTML מגיע לדפדפן, על הדפדפן לפענח את הפורמט (ולפעמים לתקן בו טעויות), לתרגם אותו למבנה שנקרא (DOM (Document Object Model ואז לרנדר ממנו את תצוגת הדף. תהליך דומה קורא לקבצי CSS ולקבצי javaScript. מנועי הרינדור של הדפדפנים השתפרו פלאים בשנים האחרונות – אך עדיין שלב זה גוזל זמן רב. זמני פעולה זו תלויים בחומרה של מחשב הלקוח, מנוע הרינדור של הדפדפן – וכמובן בדרך בה נכתב ה HTML / CSS / JavaScript.

Latency

התיאור שהשתמשנו בו עד עכשיו, של תקשורת IP או TCP כתקשורת של נקודה לנקודה, לקוח לשרת, איננו מדויק.
האינטרנט איננו רשת אחת, אלא מערך של רשתות שמחוברות בניהן בנתבים (Routers). מקור השם Internet הוא קיצור של inter-connected networks. בפועל כל הודעת IP מדלגת מרשת לרשת עד שהיא מגיעה לרשת של מחשב היעד – ושם היא מנותבת מקומית.
כל הודעה מנותבת באופן פרטני כך שייתכן שהודעות עוקבות ישלחו בכלל במסלולים שונים.
לקוח שולח הודעת IP, המנותבת על פני מספר רשתות עד שהיא מגיעה ליעד. התשובה (הודעת IP נוספת) מנותבת בדרך קצת אחרת.
הנה הרצה של פקודת "trace route", באמצעותה אני בוחן את הדרך שהודעת IP עוברת מהמחשב האישי שלי עד לשרת עליו מאוכסן הבלוג:
כפי שאתם יכולים לראות היו 17 תחנות בדרך. התחנה הראשונה, אגב, היא הראוטר הביתי שלי (הרשת הביתית היא עוד רשת). כל תחנה בדרך מוסיפה כמה מילי-שניות ובסה"כ זמן ה Round Trip, כלומר הודעת IP הלוך וחזור, היא בסביבות ה 100ms. זהו זמן דיי טוב שמאפיין את שרתי גוגל מחוץ לארה"ב (blogger שייך לגוגל).
לאתרים ללא "נוכחות" מקומית (כמו netflix או rottentomatoes) זמן ה (Round Trip (RTT הוא כ 200-300ms בד"כ, כלומר חצי מזה לכל כיוון. בשעות העומס, המספרים גבוהים יותר.
RTT של 250ms אינו נשמע מטריד – הרי זה זמן שבן-אנוש כמעט לא יכול להבחין בו. הבעיה היא כמובן במספר ה roundrtips הנדרשים על מנת לטעון דף אינטרנט מודרני. כיום, דף אינטרנט ממוצע מורכב מ 82 קבצים ו 1MB של מידע.
בצורה הנאיבית ביותר מדובר ב 82 x יצירת TCP connections ו 82 x בקשות HTTP (כאשר HTTP הוא פרוטוקול סינכרוני ולא ניתן להוציא בקשה חדשה לפני שהתשובה מהבקשה הקודמת חזרה) = 164 המתנות של רבע שנייה (כ 40 שניות) לא כולל DNS Lookup, זמן שרת, זמן רינדור בדפדפן וללא זמן הורדת הקבצים (= Bandwidth אינסופי).
מניסיון החיים, ברור שזה לא המצב בפועל. יש הרבה אופטימזציות ברמות השונות שמשפרות את נתוני הבסיס הבעייתיים. הנה כמה אופטימיזציות חשובות:
  • פרוטוקול HTTP 1.1 הוסיף מצב של keep-alive בו ניתן לבקש לא לסגור ולעשות שימוש חוזר ב TCP connection שנוצר. השיפור: כ 100%
  • דפדפנים החלו לפתוח מספר Connections מקבילים לשרת על מנת להאיץ את הורדת הקבצים. זה החל ב 2-3 connections לשרת והיום מגיע ל 6 ואף 8 connections לשרת. השיפור: עד 800%
  • רשתות Content Delivery Networks של שרתים הפזורים ברחבי העולם מחזיקים עותקים מקומיים של שרתים ששילמו על השירות – וכך מקצרים את ה latency בצורה משמעותית. השיפור: עד 1000%. במקרי קיצון: אפילו יותר. ניתן לקרוא על CDN בפוסט תקשורת: מיהו התוכי האמיתי של האינטרנט המהיר?
  • לדפדפנים יש cache בו הם שומרים קבצים אחרונים שנטענו מקומית על המחשב וכך חוסכים את כל הסעיפים הנ"ל.  ה cache יעיל החל מהגישה השנייה לאתר. השיפור: אדיר.
ויש עוד…
עדיין – יש הרבה שאתרים / אפליקציות ווב יכולות לעשות על מנת לשפר את הביצועים שלהם ולנצל טוב יותר את התשתיות הקיימות.
ל Latency יש השפעה רבה בהרבה על ביצועי אתרי אינטרנט מאשר ה Bandwidth.
הקשר בין זמן טעינה של דף אינטרנט כתלות ב RTT
לרוע המזל, שיפור של Bandwith הוא זול יחסית (יותר כבלים) בעוד שיפור ב Latency הוא יקר הרבה יותר: כיום התעבורה למרחקים נעשית במהירות האור, ונתיבי הכבלים מוגבלים מתוואי השטח. לדוגמה: את הכבלים התת-ימיים בין ארה״ב לאוסטרליה ניתן לקצר בכ 40% באורך, וכך לשפר את ה Latency בכ 40% – אך לצורך כך יש למתוח אותם בקו ישר, מה שיגרום לעלויות אסטרונומיות. לא פלא שספקי האינטרנט מתמקדים בפרסום ה Bandwidth שלהם ולא כ"כ ב Latency (אם כי גם הוא משתפר עם השנים).

ההיבט המובילי

מכשירי מובייל, מלבד מעבד חלש יותר ו bandwidth נמוך יותר, סובלים גם מ Latency גבוה יותר. כאשר עובדים ברשת 3G, על התקשורת לעבור כמה תאים סלולריים ותחנות ממסר לפני שהם מתחברים לרשת האינטרנט מה שיכול להוסיף כ 200ms ל RTT.
כפי שאתם אמורים כבר להבין, המשמעות היא כבדה מאוד – ועל כן אפליקציות מובייל משקיעות הרבה יותר באופטימיזציות, לעתים עד האסקטרים.
הבשורה הטובה היא שרשתות דור רביעי (LTE) לא רק משפרות משמעותית את ה Bandwidth אלא גם את ה Latency – החשוב יותר. מדובר על תוספת של כ 50ms ל RTT במקום כ 200ms – שיפור גדול מאוד.

סיכום

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

RESTful Services – כיצד מיישמים בפועל? (2)

תזכורת: REST הוא סגנון ארכיטקטוני, המתואר כסט אילוצים שעל המערכת לציית להם. הוא מקדם שימוש נכון ומדוייק בפרוטוקול HTTP וה Web Standards. הוא "חי בהרמוניה עם ה Web" ולא רק "משתמש ב Web כאמצעי תעבורה". ארכיטקטורה ירוקה : )

קצת להכניס אתכם לסלנג של שועלי ה REST המשופשפים (לא הייתי מגדיר את עצמי ככזה):
  • ראשי התיבות REST מייצגים Representational state transfer
  • POX הוא Plain Old XML, על משקל POJO. על מנת לציין ש REST הוא XML פשוט על הנשלח על גבי [HTTP[1
  • ל REST אפשר לקרוא גם (WOA (Web Oriented Architecture – על מנת לתאר את הקשר ל SOA או (ROA  (Resource Oriented Architecture – על מנת לתאר את הקשר ל Resource-Based Distributed Systems.
  • WS-* מוקצה לחלוטין. עדיף למלמל "השם ירחם" כל פעם שמזכירים אותם ולהזכיר מיד שהם מפרים עקרונות רבים של HTTP ו ה Web (כמו למשל – ביצוע queries לקריאה בלבד ב POST).
  • שימוש רק ב URI ולא ב URL. יש הבדל קטן (URI הוא ללא ה filename), אבל מי שחי REST לרוב מקפיד לדייק.
זהו, עכשיו לא נביך את עצמנו בקרב המומחים, ואנו מוכנים לצלול לפרטים.
The REST Uniform Interface
REST מציג את החוקים הבאים:
שימוש ב URI כמתאר של resources
דוגמאות ל resources הם: instance של הזמנה, לקוח או פוסט בבלוג – המקביל ל instance של class ב OO.
כמה כללים צריכים להשמר:
  • ה URL (אני אשתמש ב URI ו URL לסרוגין. סליחה) צריך לספק שקיפות על מבנה ה Resources, כלומר:
  • כל "/" מתאר בהכרח רמה היררכית של מבנה המשאבים.
  • יש להשתמש במקף ("-") ולא בקו תחתון ("_") להפרדת מילים. כדרג אגב ע"פ התקן (RFC 3986) החלק הרלטיבי של ה URL מוגדר כ case sensitive.
  • וכו'
מידול Resource-Based
חלק זה הוא בעל ההשפעה הגדולה ביותר על ארכיטקטורת המערכת, והוא לעיתים הסיבה מדוע מימוש REST אינו ישים במערכת קיימת ללא Refactoring מקיף.
כמו שציינתי בפוסט הקודם, בעזרת ה URI אני ניגש ל Resource ישירות ומבצע עליו פעולה. ה Resource אינו מגדיר מתודות (כמו service), אלא אני יכול לבצע עליו רק את פעולות ה HTTP הסטדרטיות:
  • GET = קריאת ה resource. מקביל לקריאות פונקצניונליות כגון getOrderDetails אולי getOwner או findBid.
  • PUT = במקור מתואר: פעולת rebind של resource. הוספת resource חדש או עדכון resource קיים.
  • POST = שליחת מידע ("post") ל resource קיים. מקביל לקריאות פונקציונליות כגון executeOrder או updateOrder. שינוי ה state הקיים.
  • DELETE = מחיקת ה resource. ביצוע פעולת Delete על משאב Subscription הוא מה שהיינו מתארים ב WS כ unsubscribe().
עודכן בעקבות תיקון של אלון.למי שמכיר HTTP, מוגדרות בו יותר מ 4 הפעולות הבסיסיות הנ"ל. לדוגמא: HEAD, TRACE, OPTIONS וכו'. הגדרת ההתנהגות שלהן היא קצת פחות ברורה ופתוחה לפרשנות של מפתח מערכת ה REST.

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

אזי גם:
צריכים להיות משאבים נגישים.
ובכן, על פניו היצמדות למודל זה נראית לא מעט טרחה! מה היתרונות שאני מקבל מהם:
  • Cache: ה Scale של האינטרנט מבוסס לחלוטין על קיומם של Caches. ה Cache יכול להיות באפליקציה, שרת ה Web (למשל IIS או Apache), רכיבי הרשת או רשת ה CDN (כמו Akamai שסיפרתי עליה כאן). הכלל מאוד פשוט: קריאות GET (וגם HEAD או OPTIONS) הן cached וקריאות POST, PUT וכו' מוסיפות dirty flag על ה cache של אותו resource. אם כתבתם אפליקציות רשת רבות ואינכם זוכרים שכתבתם קוד כזה – זה מובן. המימוש נעשה ע"י שרת האינטרנט וברמות שונות של ה network devices השונים. כולם מתואמים ומצייתים לאותם חוקים. תארו לכם איזה שיפור אתם מקבלים כאשר ה router דרכו עובר משתמש באוסטרליה מספק לו את התשובה לאפליקציה שלכם מתוך cache אוסטרלי איי שם במקום להעמיס על המערכת שלכם. כל זאת, מבלי שאתם כותבים שורת קוד בודדת.
    אפליקציות רבות נוהגות להשתמש ב POST תמיד (משיקולי אורך URL אפשרי, או שקר כלשהו לגבי אבטחה) וכך מאבדות את הייתרון המשמעותי הזה. מצד שני, אם הגדרתם קריאת GET שמשנה את ה State – אכלתם אותה: ה caches לא יהיו מעודכנים[2]
  • ציוד רשת כגון Proxies, Firewalls, Web Application Firewalls מנועי חיפוש ושירותי רשת שונים מכירים את כללי ה WEB / HTTP ופועלים לפיהם. אנו נהנה מאבטחה טובה יותר, פחות בעיות לגשת לשירותים שלנו, diagnostics משופרים, ביצועים וכו'. השיפור שיושג משימוש ב CDN למשל יהיה משמעותי יותר.
  • היכולת להשתמש ב hyperlinks כשפת referencing. זה נשמע ייתרון קטן, אבל אני יכול להחזיר בתשובה לקריאה link למשאב אחר, אותו לינק הוא יציב – ניתן לשמור אותו ולהשתמש אח"כ. הוא תמיד מעודכן. זהו כלי מאוד שימושי. דוגמאות: פעולת GET על הזמנה נותנת לי links ל100 פריטים. אני יכול לסקור ולקרוא רק את הפריטים שמעניינים אותי ומתי שמתאים לי. פעולת POST שמייצרת דו"ח (או שאילתא – ברמת ה data זה בערך אותו הדבר) מחזירה לי URL שאפשר לגשת אליו כל פעם שאני רוצה לקבל את הדוח.
  • נגישות / תפוצה רחבה: כל פלטפורמה, וכל שפת תכנות כמעט יכולה לגשת למערכת שלי בקלות ללא שימוש בספריות מיוחדות (מה שלא כ"כ נכון ל WS-*). ניתן לגשת בקלות מ JavaScript או Flash. ניתן אפילו להפעיל ידנית מה Browser (למי שקצת יותר טכני).
  • פשטות: אחרי שנכנסים לראש REST הוא לא קשה במיוחד. קל לתחזק ולהרחיב את המערכת.
שימוש ב Hypermedia לניהול שינויי state
טוב, זהו נושא קצת יותר מורכב וקצת שנוי במחלוקת. אין מחלוקת שהוא חלק הגדרת ה REST, אבל פעמים רבות בוחרים לדלג עליו ולא להשתמש בו מכיוון שהוא מורכב יותר למימוש.
עקרון זה אומר שאחרי שביצעתי פעולות (לדוגמא קריאת GET של הזמנה), הפעולות האחרות הרלוונטיות האפשריות יהיו חלק מתשובת ה GET. למשל, הנה תשובה אפשרית לקריאת ההזמנה:

23
<link rel='edit'
ref='http://example.com/order-edit/ACDB' />
אני מקבל תיאור מפורש של סט הפעולות בעזרתן אני יכול להמשיך: edit עם לינק מתאים, ואת הפרטים של המוצר והלקוח.

היתרונות הם:
  • על הלקוח להחזיק / לעקוב אחר URL יחיד (Entry Point) למערכת שלי. מכאן והלאה הוא יובל בעקבות פעולותיו.
  • קוד הלקוח יכול לדעת באופן דינמי מהן הפעולות האפשריות ולאפשר אותן. השרת מצידו יכול להרחיב ולצמצם את סט הפעולות, עם הזמן, כרצונו[3].
  • אינני צריך לבצע עוד rountrip בנוסח קריאת GET ל OrderDetails על מנת לקבל קישורים למוצר או הלקוח.
כפי שאמרתי, זה נושא מורכב (מורכב = סיבה טובה להזהר) – אך הרעיון מקורי ומעניין.
Self descriptive message
יש גם עניין של הודעות שמתארות את עצמן, כלומר קריאות ללא ידע מוקדם. לדעתי זה עוזר בעיקר ל visibility ו debug – עקרון שהייתי מגדיר כנכון אוניברסלית ולא רק ל REST.
טעויות נפוצות של מימושי REST
  • שימוש ב POST לביצוע פעולות read (היה צריך להיות GET) או כל פעולה שאינה "create new". העניין הוזכר כבר למעלה. תתי בעיות:
    • התעלמות מ Caches (הוזכר למעלה)
    • נסיון להעביר XML שכולל מידע / פעולות מעורבות או שלא קיימות בסט המצומצם. לדוגמא פרמטר POST בשם operation ואז חזרה לעולם ה Services הוא טעות נפוצה מאוד של מתחילים.
  • בניית URI בעזרת Query Parameters (רמז: Query param אמור לשמש ל… Query)
  • שימוש לא נכון או שיכפול יכולות שקיימות כבר ב HTTP. דברים כמו:
    • Status / Error Code. תזכורת: פרוטוקול HTTP מותיר להוסיף לשגיאה טקסט חופשי (= גוף ההודעה).
    • Cookies
    • Headers
    • MIME Types
  • נסיון לשמור Server Side State על כל client. בעיה אוניברסלית ל Web.
  • המנעות מהוספת לינקים לתשובה: אמנם שימוש בלינקים (hypermedia) לניהול כל ה state הוא עקרון שנוי במחלוקת, אך המנעות מלינקים בכלל – נשמעת טעות. לא טוב להסתמך על קוד ב client שמתאר את מבנה ה API לשוא וגם חבל ליצור קריאות מיותרות.
  • נסיון לבצע Implicit Transactions.
    במוקדם או במאוחר תזדקקו ל Transactions. הדרך המומלצת לטעמי הוא לייצר resource שמתאר את ה transaction. כל דרך אחרת לעשות זאת בצורה לא מפורשת שנתקלתי בה – נגמרה בכאב.
מקווה שנהנתם.
 
[1] על מנת לדייק זה לא חייב להיות XML, ועקרונות ה REST יכולים להיות מיושמים גם ללא HTTP – פשוט אפשר להשתמש בהרמוניה בפרוטוקול אחר ובאותה הרוח ש REST "מתלבש" על HTTP.
[2] יצא לי להיתקל במערכת "REST" דיי גדולה ומורכבת שלא הקפידה על הכלל והיו לה בעיות עם Caches לא מעודכנים. המפתחים שלה התחכמו והוסיפו HEADER לכל קריאות ה GET שציין שאסור לשמור את הקריאה ב Cache. בעולם ה Security קוראים לזה (SDoS (Self Denial of Service
[3] ניתן להסתכל על זה כ interface דינאמי. ב Web Services הייתי קורא WSDL וה IDE היה יוצר לי stub – שזה מאוד נחמד. מצד שני, לכאורה, לא הייתי יכול להגיב לשינויי Interface ללא שינויי קוד.
אני אומר לכאורה מכיוון שיש כמה דרכים לבצע זאת בכל זאת (בצורה קצת יותר מסורבלת)

לינקים רלוונטים:
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api