היי! זה פוסט ראשון שאני כותב מאז שעזבתי את Databand ו-IBM. ניב ואני כבר לא עובדים יחד, אבל אל דאגה - אנחנו עדיין רבים על הכל בוואטסאפ.
בינתיים, התחלתי לעבוד ב-Cynerio בסביבות ספטמבר, ועוסק שם במשהו שהוא החלום הכי ורוד של חובב ביצועים: מערכת לעיבוד מהיר של פקטות (Packet Processing). המערכת מתמקדת בביצועים ואמינות בקנה מידה מטורף.
וברור שאנחנו כותבים הכל ב-Rust. אנחנו חבורה של היפסטרים מתנשאים.

אבל זה לא רק עניין של שפה. כל אספקט במערכת בנוי ומנוהל בצורה קפדנית כדי להתמודד עם המון תעבורה (תחשבו על מפלצת עם ארבע רגליים ו-CPU משלה). למשל אחד הדגשים הוא על צמצום syscalls בצורה אובססיבית, כמו באמצעות הקצאות זיכרון מראש (יש לנו buffer pool שחי לנצח).
לצורך הדוגמה, חשבתם למשל ש- System time זה סתם עוד קריאת מערכת?
יותר כמו לעצור באמצע מרוץ פורמולה 1 כדי לשאול את הקהל מה השעה. אז זמן המערכת נמדד ב-Thread ייעודי (כדי ששאר הרכיבים לא יבזבזו לעצמם את החיים על קריאות time). אם זה לא מעיד על מתנשא וזחוח, אז אני לא יודע מה כן.
“לא לגעת במה שלא שבור” האמנם?
אומרים שאם משהו עובד, לא כדאי לגעת בו. הבעיה היא שלפעמים המשהו הזה לא עובד, רק שאף אחד לא סיפר לנו.
המערכת שלנו רצה כמו צ’יטה על סטרואידים — עד שהיא התחילה לזוז כמו סבתא במעבר חצייה, ומתיאס, במקום לעזור, החליט שזה זמן טוב לבדוק איך עובד הכפתור של הרמזור.
האינסטינקט הראשוני: “מה נסגר?! לא סיימנו כבר עם כל האופטימיזציות?”. ואז הבנו ששינויים קטנים שנכנסו החלו לטפטף בעיות ביצועים פנימה בלי שבכלל שמנו לב. אז יצאנו לצוד אותם.
מתי כן לשפר?
רק כשתחושת הכאב בביצועים עולה. כי כל אופטימיזציה דורשת ויתורים: לפעמים מפשטים אבסטרקציות אהובות (אאוץ’), וזה דורש מכל מפתח הבנה עמוקה יותר של המערכת.
איך בכלל מעיזים לגעת במערכת כזו?
לפני שנספר על השינויים, חשוב להבין שבחברה שלנו (ובמיוחד בקוד הזה) יש מעטפת תמיכה מרשימה שעוזרת לנו לעשות אינסוף רפרקטורים בלי להזיע יותר מדי:
טסטים בשפע – המערכת שלנו מחופה במלאנתלפים טסטים (בול מלאנתלפים), ככה שכמעט תמיד אפשר לדעת מוקדם אם שוברים משהו.
מעבדת שרתים מכובדת – בואו, זה לא שירות SaaS, ואין סביבת דמו ש Devops מתחזקים לכם (״מתיאס שים לי 5 פודים בqa״). אנחנו מדברים על פאקינג on prem, כדי לבדוק משהו חייבים להוריד לשם גרסאות ולראות איך המערכת מגיבה בתנאי אמת או לפחות קרוב לזה.
יש רק צוואר בקבוק אחד – ניתחנו ושמנו לב שמאמצי שיפור על משהו לא-בעייתי הם בזבוז זמן. מתמקדים בכאב הגדול בעזרת Flame Graph (זה הדבר הכי לוהט אצלנו… סליחה).
Benchmarkים ו-Profiling – מודדים כל ספרייה, כל שינוי, ורק אז מחליטים אם גונזים או מאמצים. בלי נתונים, אנחנו סתם יורים באפלה.
התוצאה? אפשר לשחרר המון שינויים קטנים בזמן קצר, לראות את הגרפים, ולהתענג על מהירות חדשה.
מאחורי הקלעים: איך באמת בודקים ביצועים
אז איך באמת עושים את זה? הגיע הזמן לחשוף את תהליך העבודה האמיתי שלנו.
הקמת הסביבה במעבדה
עוד קטע מתנשא שאני אוהב לספר על מה שאני עושה פה זה לספר על זה שיש לנו מעבדה, ולגמרי מרגיש כמו דקסטר (החנון המצוייר לא הרוצח המוזר). אבל חסרה לנו איזו דידי.
אז בגדול מדובר בשרת שמייצר סימולציה של תעבורת רשת אותה אנחנו מנתבים לכיוון השרת שאנחנו בודקים. הרעיון? לייצר סביבת בדיקה מדויקת שתשקף את תנאי העומס האמיתיים.

Flame Graph - איך מאתרים צווארי בקבוק

מריצים Flame Graph כדי לזהות איזה חישובים לוקחים יותר זמן מדי. זה לא קסם, זו שיטתיות. מחפשים את "החריגים" - קטעי קוד שנראים לא יעילים.
יצירת Benchmark מדויק
ברגע שמזהים חוסר יעילות מבודדים את החלק הרלוונטי בקוד, ומודדים זמנים מדויקים עם benchmarks, ותודה לrust עלbuilt in benchmark tests שהם מעולים לגמרי.
בשלב הבא אנחנו מחפשים חלופות פוטנציאליות - סורקים ספריות ומימושים אלטרנטיביים. מפעילים את benchmarks ובוחרים את הפתרון המהיר ביותר.
הטמעה וולידציה
מבצעים את השינוי בקוד, מריצים את מלאנטלפים הטסטים שלנו ואז מורידים למעבדה. שם נמדוד את הזמנים החדשים.
הערה: כל המדידות שתארו פה נעשו על השרתים הקטנים שלנו. הביצועים בפרודקשן? אלה נשמרים בסוד.
זה לא סיפור דרמטי, זו עבודת הנדסה שיטתית. מדידה, ניתוח, שיפור - וחוזר חלילה.
דברי בציונים גברת, דברי במספרים.
זה רק עניין של זמן
הבעיה: אנחנו מאוד מקפידים לא למדוד זמנים בנתיבי הקוד הקריטיים (בגלל syscall יקר), אבל לclient של Prometheus זה לא ממש אכפת. כל הרעיון של Prometheus זה מדידות, דגימות, וסטטיסטיקות, ופספסנו את העלויות של מדידות הזמנים שנדחפו לנו ל-Hot Path.
מה עשינו:
ראינו שהמדידות צורכות משאבי CPU ומעכבות תהליכים. אז אמרנו יפה שלום למדידות האלה (לא לכולן, אבל לאזורים הקריטיים).
הגרפים ב-Prometheus היו נחמדים, אבל אם זה בא על חשבון מהירות האור — עדיף לוותר.
מסקנה: לפעמים הבעיה מגיעה מספריות צד שלישי, אז כדאי להכיר אותן לעומק כשביצועים רגישים בסכנה.
שיפורים במעבדה - 700 Mb/s

אתם לא באמת צריכים קריפטוגרפיה
הבעיה: נכנסה דרישה לייצר UUID כדי לשלוח לצוות אחר. נחמד, עד שה-Flame Graph הראה שקריאות אקראיות (OS Random) חונקות לנו חלקים קריטיים.
מה עשינו:
חיפשנו crate מהיר יותר ל-UUID, שמאפשר להחליף את מקור ה-RNG.
זרקנו את os random מכל המדרגות לטובת אקראיות אחרת (עם פחות Overhead).
מסקנה: אם אתם צריכים UUIDים בנתיב ביצועים גבוה, כדאי לבדוק טוב מה קורה שם. מסתבר ש-os random יכול להיות צוואר בקבוק מפתיע.
שיפורים במעבדה - 400 Mb/s
קבוצה ריקה
הבעיה: השתמשנו ב-U128 כמבנה נתונים דמוי Set (כל ביט מייצג איבר). כשגדלנו ל-U256 (כי יש יותר ערכים), גילינו שאנחנו עושים המון המרות טייפים וחישובי Mask שמבזבזים זמן.
מה עשינו:
בדקנו (ב-Benchmarkים) ספריות אחרות שעושות דבר דומה.
מצאנו ספרייה פופולרית יותר (ועדיין מהירה!) והחלטנו להשתמש בה - אנחנו לא היפסטרים כשלא צריך.
מסקנה: שינוי קטן כביכול (החלפת U128 ל-U256) יכול לגרור מלא בעיות מתחת לפני השטח. Flame Graph שוב הציל אותנו וחשף שהאש בוערת (חיחי) בדיוק שם.
שיפורים במעבדה - 1.5 Gb/s
עובי הקורה
הבעיה: כאן צריך קצת רקע - המערכת שלנו עושה סינון מהיר של פקטות, ואז מעבדת אותן בצורה “איטית” יותר (למרות שגם האיטי כאן די סילון). בשלב מסוים, אחד מתהליכי העיבוד השתנה והפך להיות הרבה יותר ספציפי, אבל עדיין קיבל המון פקטות לא רלוונטיות — כי הסינון המוקדם לא עודכן.
מה עשינו:
עשינו התאמה בין מה שהחלק השני מצפה לקבל מהשלב של הסינון הראשון.
צמצמנו כמות עצומה של נתונים זבל שעוברים קדימה והופכים את החלק ה”איטי” לעוד יותר איטי.
מסקנה: המערכת משתנה עם הזמן, ונוצרה הזמדנות מדהימה לשיפור, אך ללא הבנה עמוקה של הלוגיקה במערכת מקצה לקצה לא היינו יכולים לייצר את השינוי הזה.
שיפורים במעבדה - 2.15 Gb/s

מחט בערימת שחת
הבעיה: אנחנו מחפשים תדיר רצפים של Bytes בתוך פקטות. השתמשנו ב-jetscii (שמנצל SIMD), וזה היה אחלה - עד שהחלטנו לבדוק ספריות אחרות.
מה עשינו:
השווינו benchmarks בין jetscii ל-memchr וגילינו שmemchr מהירה פי כמה ברוב המקרים.
למזלנו, הכל היה מאחורי אבסטרקציה אחת, אז המעבר היה מהיר וכמעט בלי כאבי ראש.
מסקנה: פעם נוספת, תודות לארכיטקטורה נקייה וכיסוי טסטים נרחב, יכולנו להחליף ספרייה שלמה באפס סיכון ולגזור רווח אדיר בביצועים.
שיפורים במעבדה - 1.1 Gb/s
לסיכום
אל תשפרו סתם – אם אין כאב, אל תלחמו עליו. כל אופטימיזציה דורשת ויתורים ועומק הבנה.
אבל ברגע שיש כאב, תתקפו מהר – Flame Graph, סביבת בדיקות ו-Benchmarks הם החברים הטובים שלכם.
אבסטרקציה נכונה וטסטים בכל פינה – מאפשרים לכם לזוז מהר בלי לשרוף את המערכת.
תהיו עם אצבע על הדופק – ספריות צד שלישי, שינויי לוגיקה, או הסתמכות מיותרת על syscalls יכולים להיות סוס טרויאני בהמתנה, אבל גם הזדמנויות מדהימות לשיפורים.

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