המעבר של טינדר לקוברנטס

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

למה

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

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

זה לא היה קל. במהלך ההגירה שלנו בתחילת 2019 הגענו למסה קריטית בתוך אשכול קוברנט והתחלנו להיתקל באתגרים שונים בגלל נפח התנועה, גודל האשכול ו- DNS. פתרנו אתגרים מעניינים להעביר 200 שירותים ולהפעיל אשכול קוברנט בקנה מידה הכולל 1,000 צמתים, 15,000 תרמילים ו 48,000 מכולות ריצה.

איך

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

בניית תמונות לקוברנט

ישנם יותר מ- 30 מאגרי קוד מקור עבור שירותי המיקרו הפועלים באשכול Kubernetes. הקוד במאגרים אלה כתוב בשפות שונות (למשל, Node.js, Java, Scala, Go) עם מספר סביבות זמן ריצה עבור אותה שפה.

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

איור 1–1 תהליך בנייה סטנדרטי דרך מיכל Builder

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

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

עבור שירותים מסוימים, היינו צריכים ליצור מיכל אחר בתוך הכלי לבניית התאמה לסביבת הקומפילציה לסביבת זמן הריצה (למשל, התקנת ספריית bcrypt של Node.js מייצרת ממצאים בינאריים ספציפיים לפלטפורמה). דרישות זמן קומפילציה עשויות להיות שונות בין השירותים וה- Dockerfile הסופי מורכב תוך זמן קצר.

אדריכלות והגירה של קובברנט אשכול

גודל אשכול

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

הסתפקנו ב:

  • m5.4xlarge לניטור (Prometheus)
  • c5.4xlarge עבור עומס העבודה של Node.js (עומס עבודה חד-הברגה)
  • c5.2xlarge ל- Java ו- Go (עומס עבודה רב-הברגה)
  • c5.4 הגדלה למישור הבקרה (3 צמתים)

הגירה

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

נקודות קצה אלה נוצרו באמצעות ערכות רשומות DNS משוקללות עם CNAME שמצביע לכל ELB חדש. כדי לחתוך, הוספנו רשומה חדשה שהצבענו על שירות ה- Kubernetes ELB החדש, עם משקל של 0. לאחר מכן הגדרנו את Time To Live (TTL) ברשומה שנקבעה ל 0. המשקולות הישנות והחדשות הותאמו לאט לאט בסופו של דבר עם 100% בשרת החדש. לאחר סיום הקיצוץ, הוגדר ה- TTL למשהו יותר סביר.

מודולי ה- Java שלנו כיבדו TTL DNS נמוך, אך יישומי הצומת שלנו לא עשו זאת. אחד המהנדסים שלנו כתב מחדש חלק מקוד בריכת החיבורים כדי לעטוף אותו במנהל שירענן את הבריכות בכל שנות ה -60. זה עבד טוב מאוד עבורנו ללא להיט ביצועים בולט.

למידה

מגבלות בד רשת

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

ישנם שלושה ערכי לינוקס הרלוונטיים למטמון ה- ARP:

אשראי

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

אנו משתמשים בפלנל כמארג הרשת שלנו בקוברנט. המנות מועברות באמצעות VXLAN. VXLAN היא ערכת שכבת שכבה 2 על גבי רשת שכבה 3. הוא משתמש במתקני MAC-in-UDP (MAC Address-in-User Data Protocol) כדי לספק אמצעי להרחבת פלחי רשת שכבה 2. פרוטוקול התובלה ברשת מרכז הנתונים הפיזי הוא IP פלוס UDP.

איור 2–1 תרשים פלנל (אשראי)

איור 2–2 מנות VXLAN (אשראי)

כל צומת עובדי Kubernetes מקצה / 24 מרחב כתובות וירטואלי משלו מחסימה גדולה יותר / 9. עבור כל צומת, התוצאה היא כניסה לטבלת מסלול אחת, ערך ARP טבלה 1 (בממשק פלנל 1) ובסיס נתונים 1 להעברת נתונים (FDB). אלה מתווספים כאשר צומת העובד מושקת לראשונה או כשמתגלה כל צומת חדש.

בנוסף, תקשורת צומת לפוד (או תרמיל לפוד) בסופו של דבר זורמת על ממשק eth0 (מתואר בתרשים הפלנל למעלה). זה יביא לכניסה נוספת בטבלת ה- ARP עבור כל מקור הצומת ויעד הצומת המתאים.

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

בזמן ההפסקה היו 605 צמתים בסך הכל באשכול. מהסיבות שפורטו לעיל, זה הספיק בכדי להפיל את ערך ברירת המחדל gc_thresh3. ברגע שזה קורה, לא רק שמנות נופלות, אלא שטנדרים שלמים של פלנל / 24 של שטח כתובות וירטואלי חסרים בטבלת ה- ARP. תקשורת עם צומת לתיקיית פוד וחיפושים ב- DNS נכשלים. (DNS מתארח בתוך האשכול, כפי שיוסבר ביתר פירוט בהמשך מאמר זה.)

כדי לפתור, ערכי gc_thresh1, gc_thresh2 ו- gc_thresh3 מוגדלים ויש להפעיל מחדש את פלנל כדי לרשום מחדש רשתות חסרות.

באופן בלתי צפוי מפעיל DNS בקנה מידה

כדי להתאים את ההגירה שלנו, מינופנו את DNS בכבדות כדי להקל על עיצוב התנועה וקיצוץ מצטבר ממורשת לקוברנטס עבור השירותים שלנו. קבענו ערכי TTL נמוכים יחסית בכבישי התקליטים Route53 המשויכים. כאשר ניהלנו את התשתית שלנו מדור קודם במופעי EC2, תצורת הרזולוציה שלנו הצביעה על ה- DNS של אמזון. לקחנו את זה כמובן מאליו והעלות של TTL נמוך יחסית עבור השירותים שלנו ושירותיה של אמזון (למשל DynamoDB) לא נעלמה מעיניהם.

עם העלאתנו ליותר ויותר שירותים לקוברנטס, מצאנו את עצמנו מנהלים שירות DNS שעונה על 250,000 בקשות בשנייה. נתקלנו בפסק זמן של בדיקות DNS לסירוגין ומשפיעות ביישומים שלנו. זה קרה למרות מאמץ כוונון ממצה וספק DNS עבר לפריסה של CoreDNS שבזמן מסוים הגיע לשיא ב -1,000 תרמילים שצרכו 120 ליבות.

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

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

  • SNAT אינו הכרחי, מכיוון שהתנועה נשארת באופן מקומי בצומת. זה לא צריך להיות מועבר דרך ממשק eth0.
  • DNAT אינו הכרחי מכיוון ש- IP היעד הוא מקומי לצומת ולא תרמיל שנבחר באופן אקראי לכל כללי ה- iptables.

החלטנו להתקדם עם גישה זו. CoreDNS נפרס כ- DaemonSet ב- Kubernetes והזרקנו את שרת ה- DNS המקומי של הצומת ל resolv.conf של כל פוד על ידי קביעת התצורה של דגל הפקודה kubelet - cluster-dns. הדרך לעקיפת הבעיה הייתה יעילה עבור פסק זמן של DNS.

עם זאת, אנו עדיין רואים מנות שהושלכו והתוספת הנגדית של insert_failed של ממשק הפלנל. זה יימשך גם לאחר הדרך לעקיפת הבעיה כיוון שנמנענו רק מ- SNAT ו / או DNAT לתעבורת DNS. תנאי המירוץ עדיין יתרחש לסוגים אחרים של תנועה. למרבה המזל, מרבית המנות שלנו הן TCP וכאשר המצב מתרחש, המנות ישודרו בהצלחה. תיקון לטווח הארוך עבור כל סוגי התעבורה הוא משהו שאנחנו עדיין דנים בו.

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

כשאנחנו העברנו את שירותי ה- backend לקוברנטס, התחלנו לסבול מעומס לא מאוזן על פני תרמילים. גילינו שבגלל HTTP Keepalive, חיבורי ELB נדבקו לתרמילים המוכנים הראשונים של כל פריסה מתגלגלת, כך שרוב התנועה זרמה באחוז קטן מהתרמילים הזמינים. אחת ההקלות הראשונות שניסינו הייתה להשתמש ב- MaxSurge 100% בפריסה חדשה עבור העבריינים הגרועים ביותר. זה היה אפקטיבי באופן שולי ולא ארוך טווח עם חלק מההתקנות הגדולות יותר.

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

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

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

לאחר מכן השתמשו בשליחי חזית השירות במנגנון גילוי שירות זה עם אשכול ותוואי במעלה הזרם. קבענו קביעת פסק זמן סביר, שיפרנו את כל הגדרות מפסק המעגל ואז הכנסנו תצורה מחדש מינימאלית כדי לעזור בכישלונות חולפים ופריסות חלקות. חזינו כל אחד משירותי שליח החזית הללו באמצעות מכשיר TCP ELB. אפילו אם הרס-אפ משכבת ​​ה- Proxy הקדמי הראשי שלנו הוצמד על תרמילי שליחים מסוימים, הם היו מסוגלים הרבה יותר להתמודד עם העומס והוגדרו להתאזן דרך לפחות_בקשה לנדנד.

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

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

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

איור 3-1 התכנסות מעבד של שירות אחד במהלך קיצוץ לשליח

התוצאה הסופית

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

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

זה לקח כמעט שנתיים, אך סיימנו את הגירתנו במרץ 2019. פלטפורמת טינדר פועלת אך ורק על אשכול קוברנט המורכב מ 200 שירותים, 1,000 צמתים, 15,000 תרמילים ו 48,000 מכולות פועלות. תשתית אינה עוד משימה שמורה לצוותי הפעילות שלנו. במקום זאת, מהנדסים ברחבי הארגון חולקים באחריות זו ויש להם שליטה על האופן בו היישומים שלהם נבנים ונפרסים עם כל דבר כקוד.