[תרגום] דגם השרשור של השליח

תרגום המאמר: מודל שרשור של Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

מצאתי את המאמר הזה די מעניין. מכיוון ש-Envoy משמש לרוב כחלק מ-"istio" או פשוט כ"בקר כניסה" של Kubernetes, רוב האנשים לא מקיימים איתו אינטראקציה ישירה כמו שהם עושים עם התקנות טיפוסיות של Nginx או Haproxy. עם זאת, אם משהו מתקלקל, יהיה טוב להבין איך זה עובד באופן פנימי. ניסיתי לתרגם כמה שיותר טקסט לרוסית, כולל מילים מיוחדות. למי שזה כואב, השארתי את הטקסט המקורי בסוגריים. ברוכים הבאים למטה.

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

אחת השאלות הטכניות הנפוצות ביותר שאני מקבל לגבי Envoy היא בקשה לתיאור ברמה נמוכה של מודל ה-threading שלה. בפוסט הזה, אתאר כיצד Envoy ממפה חיבורים ל-threads, כמו גם את מערכת ה-Thread Local Storage שהיא משתמשת בה באופן פנימי כדי להפוך את הקוד למקביל ויעיל יותר.

סקירת שרשור

[תרגום] דגם השרשור של השליח

Envoy משתמש בשלושה סוגים שונים של זרמים:

  • רָאשִׁי: רצף זה מנהל את ההפעלה והכיבוי של תהליכים, את כל עיבוד ה-API של XDS (שירות xDiscovery), כולל DNS, בדיקת תקינות, ניהול כללי של אשכולות וזמן ריצה, איפוס סטטיסטיקות, ניהול וניהול כללי של תהליכים. Linux אותות, אתחול מחדש חם וכו'. כל מה שקורה בשרשור הזה הוא אסינכרוני ולא חוסם. בסך הכל, השרשור הראשי מתאם את כל התהליכים הקריטיים שאינם דורשים מעבד גדול. זה מאפשר לכתוב את רוב קוד הבקרה כאילו היה בעל הליך יחיד.
  • עוֹבֵד: כברירת מחדל, Envoy יוצר worker thread עבור כל thread חומרה במערכת, ניתן לשלוט בכך באמצעות האפשרות --concurrencyכל worker thread מפעיל לולאת אירועים לא חסימה, שאחראית על האזנה לכל מאזין (נכון לכתיבת שורות אלה (29 ביולי 2017) אין חלוקה של מאזינים), קבלת חיבורים חדשים, יצירת מופע של מחסנית המסננים עבור החיבור וטיפול בכל פעולות הקלט/פלט למשך חיי החיבור. שוב, זה מאפשר לכתוב את רוב קוד הטיפול בחיבורים כאילו היה בעל הליך יחיד.
  • מנקה קבצים: לכל קובץ ש-Envoy כותב, בעיקר יומני גישה, יש כרגע שרשור נעילה עצמאי. הסיבה לכך היא שכתיבה לקבצים המאוחסנים במטמון על ידי מערכת הקבצים, גם בעת שימוש O_NONBLOCK לפעמים יכול לחסום (אנחה). כאשר worker threads צריכים לכתוב לקובץ, הנתונים מועברים למעשה למאגר בזיכרון, שם הם בסופו של דבר נשטפים דרך ה-thread. ניקוי קבציםזהו תחום קוד שבו מבחינה טכנית כל הליכי העבודה יכולים לחסום את אותה נעילה תוך כדי ניסיון למלא מאגר זיכרון.

טיפול בחיבור

כפי שפורט בקצרה לעיל, כל הליכי העבודה מאזינים לכל המאזינים ללא כל פילוח. לכן, הליבה משמשת לשליחת sockets שהתקבלו אל הליכי העבודה. ליבות מודרניות בדרך כלל טובות מאוד בכך, תוך שימוש בתכונות כמו הגברת קלט/פלט (IO) כדי לנסות למלא רצף עבודה לפני תזמון האזנה של רצפים אחרים באותו socket, והימנעות מ-spinlocks לעיבוד כל בקשה.
ברגע שחיבור מתקבל ב-worker thread, הוא לעולם לא עוזב את ה-worker. כל עיבוד החיבור לאחר מכן מטופל כולו בתוך ה-worker thread, כולל כל התנהגות העברה.

יש לכך מספר השלכות חשובות:

  • כל מאגרי החיבורים ב-Envoy הם לפי הליך עבודה. לכן, למרות שמאגרי חיבורים של HTTP/2 יוצרים חיבור אחד בלבד לכל מארח במעלה הזרם בכל פעם, אם ישנם ארבעה הליכי עבודה, יהיו ארבעה חיבורי HTTP/2 לכל מארח במעלה הזרם במצב יציב.
  • הסיבה ש-Envoy עובד בצורה זו היא שעל ידי שמירת הכל ב-worker thread יחיד, כמעט כל הקוד יכול להיכתב ללא נעילה וכאילו היה ב-single thread. עיצוב זה מפשט כתיבת כמויות גדולות של קוד ומתרחב בצורה מדהימה למספר כמעט בלתי מוגבל של worker threads.
  • עם זאת, אחת המסקנות העיקריות היא שמנקודת מבט של מאגר זיכרון ויעילות חיבור, חשוב מאוד לכוונן את הפרמטר. --concurrencyיותר worker threads מהנדרש יבזבז זיכרון, ייצור יותר חיבורים לא פעילים ויפחית את קצב מאגר החיבורים. ב-Lyft, קונטיינרים של Envoy בצד שלנו פועלים עם מקביליות נמוכה מאוד, כך שהביצועים תואמים בערך את השירותים שהם פועלים לצדם. אנו מפעילים את Envoy כ-edge proxy רק במקביליות מקסימלית.

מה המשמעות של אי חסימה?

המונח "לא חסום" (non-blocking) שימש עד כה מספר פעמים כשדנים באופן שבו ה-main threads ו-worker threads פועלים. כל הקוד נכתב מתוך הנחה ששום דבר לא חוסם. עם זאת, זה לא לגמרי נכון (מה לא לגמרי נכון?).

Envoy משתמש במספר נעילות תהליכים ארוכות טווח:

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

אחסון מקומי של השרשור

מכיוון ש-Envoy מפרידה בין האחריות של ה-thread הראשי לבין האחריות של ה-worker thread, יש דרישה שעיבוד מורכב יוכל להתבצע על ה-headline הראשי ולאחר מכן להפיץ אותו לכל Worker Thread במידה גבוהה של מקביליות. סעיף זה מתאר את מערכת ה-Thread Local Storage (TLS) של Envoy ברמה גבוהה. בסעיף הבא, אתאר כיצד היא משמשת לניהול אשכולות.
[תרגום] דגם השרשור של השליח

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

מערכת ה-TLS (אחסון מקומי של הליכים) של Envoy פועלת באופן הבא:

  • קוד הפועל על ה-thread הראשי יכול להקצות חריץ TLS לכל התהליך. למרות שזה מופשט, בפועל זהו אינדקס לתוך וקטור, המספק גישה ל-O(1).
  • ה-main thread יכול להגדיר נתונים שרירותיים ב-slot שלו. כאשר זה נעשה, הנתונים מתפרסמים לכל Worker Thread כאירוע לולאת אירועים רגיל.
  • הליכי עבודה (worker threads) יכולים לקרוא מחריץ ה-TLS שלהם ולאחזר כל נתונים מקומיים של הליך הזמינים שם.

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

השליח משתמש בזה בשתי דרכים שונות:

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

שרשור עדכון אשכול

בסעיף זה, אתאר כיצד TLS (Thread Local Storage) משמש לניהול אשכולות. ניהול אשכולות כולל טיפול ב-xDS ו/או DNS APIs, וכן בדיקת תקינות.
[תרגום] דגם השרשור של השליח

ניהול זרימת אשכולות כולל את הרכיבים והשלבים הבאים:

  1. מנהל האשכולות הוא רכיב בתוך Envoy שמנהל את כל האשכולות הידועים במעלה הזרם, את ה-API של CDS (Cluster Discovery Service), את ממשקי ה-API של SDS (Secret Discovery Service) ו-EDS (Endpoint Discovery Service), DNS ובדיקות תקינות חיצוניות פעילות. הוא אחראי על יצירת תצוגה עקבית בסופו של דבר של כל אשכול במעלה הזרם, הכוללת מארחים שהתגלו ומצב תקינותם.
  2. בודק התקינות מבצע בדיקת תקינות פעילה ומדווח על שינויים במצב התקינות למנהל האשכול.
  3. שירות גילוי אשכולות (CDS) / שירות גילוי סודי (SDS) / שירות גילוי נקודות קצה (EDS) / DNS משמשים לקביעת חברות באשכול. שינוי המצב מוחזר למנהל האשכול.
  4. כל הליך עבודה (worker thread) מפעיל באופן רציף לולאת עיבוד אירועים.
  5. כאשר מנהל האשכול קובע שמצב האשכול השתנה, הוא יוצר תמונת מצב חדשה לקריאה בלבד של מצב האשכול ושולח אותה לכל הליך עבודה.
  6. במהלך תקופת השקט הבאה, הליך העבודה יעדכן את תמונת המצב בחריץ TLS שהוקצה.
  7. במהלך אירוע קלט/פלט שמזהה מארח לצורך איזון עומסים, מאזן העומסים יבצע שאילתה לחריץ TLS (Thread Local Storage) כדי לקבל מידע על המארח. פעולה זו אינה דורשת נעילות. שימו לב גם ש-TLS יכול גם להפעיל אירועי עדכון, כך שמאזני עומסים ורכיבים אחרים יכולים לחשב מחדש מטמונים, מבני נתונים וכן הלאה. זה חורג מהיקף הפוסט הזה, אך משמש במקומות שונים בקוד.

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

תת-מערכות אחרות המשתמשות ב-TLS

TLS (אחסון מקומי של הליכים) ו-RCU (עדכון קריאה של עותק) נמצאים בשימוש נרחב ב-Envoy.

דוגמאות לשימוש:

  • מנגנון לשינוי פונקציונליות במהלך הביצוע: הרשימה הנוכחית של פונקציונליות מופעלת מחושבת על הליך המשנה הראשי. לאחר מכן, כל ליך עובד מקבל תמונת מצב לקריאה בלבד באמצעות סמנטיקה של RCU.
  • החלפת טבלאות מסלולעבור טבלאות ניתוב המסופקות על ידי RDS (שירות גילוי מסלולים), טבלאות הניתוב נוצרות בשרשור הראשי. לאחר מכן מסופקת תמונת מצב לקריאה בלבד לכל שרשור עובד באמצעות סמנטיקה של RCU (עדכון קריאה). זה הופך את שינויי טבלת הניתוב ליעילים מבחינה אטומית.
  • אחסון כותרת HTTP במטמון: כפי שמתברר, חישוב כותרת ה-HTTP עבור כל בקשה (ב-~25K+ RPS לליבה) הוא די יקר. Envoy מחשבת באופן מרכזי את הכותרת בערך כל חצי שנייה ומגישה אותה לכל עובד דרך TLS ו-RCU.

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

מלכודות ביצועים ידועות

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

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

מַסְקָנָה

מודל ה-threading של Envoy נועד להבטיח קלות תכנות ותפעול מקביליות מסיבי, על חשבון בזבוז פוטנציאלי של זיכרון ושימוש בחיבורים אם לא מוגדר כהלכה. מודל זה מאפשר לו לבצע ביצועים יוצאי דופן בספירת thread ותפוקה גבוהים מאוד.
כפי שציינתי בקצרה בטוויטר, העיצוב יכול גם לרוץ על גבי מערך רשתות בעל תכונות מלאות במצב משתמש כמו DPDK (ערכת פיתוח מטוס נתונים), שיכולה לאפשר לשרתי קונבנציונליים לטפל במיליוני בקשות בשנייה עם עיבוד L7 מלא. יהיה מעניין מאוד לראות מה ייבנה בשנים הקרובות.
הערה קצרה אחרונה: נשאלתי פעמים רבות מדוע בחרנו ב-C++ עבור Envoy. הסיבה לכך היא שזו עדיין השפה היחידה המאומצת באופן נרחב ובעלת עוצמה תעשייתית המסוגלת לבנות את הארכיטקטורה המתוארת בפוסט זה. C++ בהחלט לא מתאימה לכל הפרויקטים או אפילו להרבה מהם, אבל עבור מקרי שימוש מסוימים, היא עדיין הכלי היחיד לביצוע העבודה.

קישורים לקוד

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

מקור: www.habr.com

קנה אירוח אמין לאתרים עם הגנת DDoS, שרתי VPS VDS 🔥 קנה אחסון אתרים אמין עם הגנת DDoS, שרתי VPS VDS | ProHoster