1. ספרייה
  2. TCP ו-UDP
  3. צלילה לעומק TCP

עודכן לפני חודש

בשנת 1984, ג'ון נייגל צפה באינטרנט המוקדם טובע בחבילות זעירות. משתמש שהקליד במושב Telnet היה מייצר חבילה אחת לכל הקשת מקש — בייט אחד של נתונים עטוף ב-58 בייטים של כותרות. הרשת הוציאה 98% ממאמציה על מעטפות ורק 2% על תוכן.

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

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

כיצד עובד האלגוריתם של נייגל

כל חבילת TCP נושאת תקורה: 20 בייטים של כותרת IP, 20 בייטים של כותרת TCP, 18 בייטים של מסגרת Ethernet. כאשר יישום שולח בייט אחד, אותו בייט נוסע בחבילה של 58 בייטים — יחס תקורה של 5,700%.

האלגוריתם של נייגל מיישם כלל פשוט:

  • אם הנתונים גדולים מספיק כדי למלא חבילה בגודל מקסימלי (MSS), שלח מיד
  • אם אין נתונים שטרם אושרו בדרך, שלח מיד
  • אחרת, אחסן את הנתונים במאגר עד שמגיע ACK או מצטברת חבילה מלאה

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

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

הבעיה שהוא פתר

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

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

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

ביטול האלגוריתם עם TCP_NODELAY

אפשרות ה-socket ‏TCP_NODELAY אומרת ל-TCP לנטוש את האלגוריתם של נייגל ולשדר כל כתיבה מיידית, ללא קשר לגודל. יותר חבילות, יותר תקורה, אבל מסירה מיידית.

int flag = 1;
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

לאחר הגדרתו, TCP_NODELAY נשאר פעיל לכל אורך חיי ה-socket. אין הפעלה מחדש אוטומטית, אין התנהגות מותנית. כל כתיבה הופכת לחבילה.

מתי לבטל אותו

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

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

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

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

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

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

בעיית ה-Delayed ACK

לאלגוריתם של נייגל יש אינטראקציה בעייתית עם אופטימיזציית TCP נוספת: אישורים מעוכבים. מקבלים לא מאשרים כל חבילה מיידית — הם ממתינים עד 200ms בתקווה להצמיד את ה-ACK לנתונים יוצאים, או כדי לאשר מספר חבילות יחד.

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

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

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

ברירות המחדל המודרניות

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

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

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

עבור HTTP/2 ו-HTTP/3, השאלה אינה רלוונטית. פרוטוקולים אלה מיישמים ריבוב ותיעדוף משלהם, מה שהופך את האיגוד ברמת TCP למיותר.

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

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

שאלות נפוצות על האלגוריתם של נייגל

מדוע TCP_NODELAY לא גורם לעומס ברשת אם כולם משתמשים בו?

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

האם עלי תמיד להפעיל TCP_NODELAY עבור יישומי אינטרנט?

עבור שרתי HTTP/1.1 המגישים תוכן דינמי, TCP_NODELAY לעתים קרובות עוזר. עבור HTTP/2 ו-HTTP/3, זה פחות חשוב כי פרוטוקולים אלה מטפלים באיגוד שלהם. עבור שרתים שבעיקר שולחים תגובות גדולות (תמונות, קבצים), האלגוריתם של נייגל מתאים בהחלט. אם אינך בטוח, בצע בדיקות ביצועים עם תנאי רשת מציאותיים.

האם ניתן להפעיל את האלגוריתם של נייגל על כתיבות מסוימות ולהשביתו על אחרות?

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

האם האלגוריתם של נייגל משמש ב-UDP?

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

האם דף זה היה מועיל?

😔
🤨
😃