Distributed Tracing: איך עוקבים אחרי בקשה בודדת דרך עשרות מיקרו-שירותים

מאת צוות מדיה דיל · 02.08.2026 · DevOps · 10 דק׳

מדריך מעמיק ל-Distributed Tracing: Trace, Span ו-Context Propagation, Sampling מבוסס tail, כלים מובילים (Jaeger, Tempo) וטעויות נפוצות.

בקשת API בודדת שנכנסת למערכת מודרנית לרוב לא נשארת בשירות אחד - היא עוברת דרך API Gateway, שירות אימות, שירות עסקי, שכבת מטמון, מסד נתונים, ולפעמים גם תור הודעות שמפעיל שירות נוסף ברקע. כשהבקשה הזו לוקחת 3 שניות במקום 200 מילישניות הרגילות, השאלה "איפה הזמן הלך לאיבוד?" הופכת כמעט בלתי אפשרית לענות עליה בלי כלי ייעודי - כי אף לוג בודד משירות בודד לא מספר את הסיפור המלא, ואף מפתח בודד בצוות לא מחזיק בראשו את התמונה השלמה של כל השרשרת. זו בדיוק הבעיה ש-Distributed Tracing פותר: שחזור מדויק, שירות אחר שירות, של המסע המלא שבקשה עברה במערכת.

הבעיה: כשארכיטקטורת מיקרו-שירותים הופכת את הדיבוג לבלתי אפשרי

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

אנטומיה: Trace, Span ו-Context Propagation

ה-Trace הוא הייצוג המלא של מסע בקשה בודדת מתחילתה ועד סופה, ומורכב מסדרת Spans - כל Span מייצג יחידת עבודה בודדת (למשל "שאילתת SQL", "קריאת HTTP לשירות חיצוני", "עיבוד לוגיקה עסקית"), עם זמן התחלה, זמן סיום, ומטא-דאטה (attributes) שמתארים מה קרה בדיוק. Spans מקוננים זה בזה בהיררכיה - Span-אב (parent) שמכיל בתוכו Spans-בן (children), כך שאפשר לראות בדיוק אילו פעולות קרו במקביל ואילו ברצף. מנגנון ה-Context Propagation הוא זה שמאפשר לחבר בין Spans שנוצרו בתהליכים שונים לגמרי - trace ID ו-span ID מקודדים בתוך HTTP header (בתקן W3C: traceparent) ומועברים בכל קריאה יוצאת, כך שהשירות הבא בשרשרת יודע לאיזה trace הוא שייך ומהו ה-span שקרא לו, ויכול ליצור span-בן משלו תחת אותה היררכיה.

// traceparent header שמועבר בין שירותים
// format: version-traceId-spanId-flags
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

// כל שירות מוסיף span משלו תחת אותו trace
const span = tracer.startSpan('fetch-inventory', {
  parent: extractContextFromHeaders(req.headers)
});

Sampling: למה לא שומרים הכל

לשמור trace מלא לכל בקשה בודדת נשמע אידיאלי, אבל בקנה מידה של מיליוני בקשות ליום, זה יקר מדי מבחינת אחסון ומשפיע על ביצועים (overhead של יצירת ורישום כל span). Head-based Sampling מחליט מיד בכניסת הבקשה, לפי הסתברות קבועה (למשל 1%), האם לעקוב אחריה במלואה - פשוט ליישום, אך עלול "לפספס" בדיוק את הבקשות הבעייתיות שהכי מעניין לחקור. Tail-based Sampling מחכה עד שהבקשה מסתיימת, ורק אז מחליט אם לשמור אותה - מה שמאפשר כלל חכם בהרבה כמו "שמור תמיד traces עם שגיאה, או עם latency מעל 1 שנייה, ורק דגום 1% מהשאר". tail-based sampling מדויק משמעותית אך דורש buffering זמני של כל ה-spans לפני קבלת ההחלטה, מה שמוסיף מורכבות תפעולית לשכבת ה-collector.

Trade-offs: פירוט מול עלות תפעולית

ריבוי מדי של spans - למשל span נפרד לכל שורת קוד או כל קריאת פונקציה פנימית - הופך את ה-trace לבלתי קריא, גם אם הוא טכנית שלם. Trace שימושי מאזן בין רזולוציה (מספיק פירוט כדי לאתר את הבעיה) לקריאות (לא כל כך הרבה spans שהתמונה הכללית נטבעת ברעש). כלל אצבע מעשי ומומלץ בפועל: span לכל גבול רשת (network boundary - קריאה לשירות אחר, שאילתת DB, קריאה ל-API חיצוני), אך לא span לכל פונקציה פנימית טריוויאלית שרצה תוך מילישניות בודדות בתוך אותו תהליך. מעבר לכך, יש trade-off אמיתי בין עלות האחסון (כל trace הוא כמות משמעותית של מטא-דאטה) לבין ערך האבחון - ולכן שילוב עם tail-based sampling וקביעת retention קצר לרוב ה-traces ה"רגילים", עם שמירה ארוכה יותר רק לחריגים, הוא הפתרון המאוזן שרוב הצוותים מגיעים אליו בסופו של דבר.

כלים מרכזיים בשוק

Jaeger (שמקורו ב-Uber, כיום פרויקט CNCF) ו-Zipkin הם פתרונות open-source ותיקים ונפוצים לאחסון וויזואליזציה של traces. Grafana Tempo מציע גישה חסכונית יותר באחסון (object storage זול במקום מסד נתונים ייעודי), ומתחבר טבעי לשאר מחסנית Grafana (Loki ללוגים, Prometheus למטריקות). ספקים מנוהלים כמו Datadog, Honeycomb ו-New Relic מציעים חוויית ניתוח מתקדמת יותר (correlation אוטומטי, זיהוי אנומליות) במחיר גבוה יותר. ההמלצה המעשית: להתחיל תמיד עם instrumentation מבוסס OpenTelemetry (שאינו תלוי ספק), ולבחור backend לפי תקציב וצרכי הצוות - כי המעבר בין backends, כשה-instrumentation כבר תקני, כמעט חסר חיכוך. הפרדה כזו בין שכבת האיסוף לשכבת האחסון והניתוח חוסכת גם כאב מעשי משמעותי כשספק משנה תמחור או כשצוות גדל ומחליט שהוא זקוק ליכולות ניתוח מתקדמות יותר ממה שהפתרון ה-open-source הראשוני נתן לו.

Semantic Conventions: למה שמות אחידים חשובים כל כך

אחת הבעיות המעשיות ביותר בפריסת tracing בארגון גדול היא חוסר אחידות - צוות אחד קורא ל-attribute של מזהה משתמש user_id, צוות אחר userId, וצוות שלישי uid. כשמנסים לבנות שאילתה שחוצה שירותים ("הראה לי את כל ה-traces של המשתמש X"), חוסר האחידות הזה הופך את המשימה לבלתי אפשרית בלי נרמול ידני מסורבל. OpenTelemetry פותר את זה עם Semantic Conventions - מילון מוסכם של שמות attributes סטנדרטיים לפעולות נפוצות (http.method, db.system, db.statement, messaging.system), כך שכל שירות, גם אם נכתב על ידי צוות אחר לגמרי או בשפת תכנות שונה, מייצר spans שניתנים להשוואה ולשאילתה אחידה. אימוץ המוסכמות האלה מההתחלה חוסך פרויקט נרמול כואב בהמשך, ברגע שהמערכת גדלה לעשרות שירותים ועשרות צוותים.

Baggage: העברת הקשר עסקי לאורך כל השרשרת

מעבר למזהה ה-trace עצמו, לעיתים יש צורך להעביר מידע עסקי נוסף לאורך כל שרשרת הקריאות - למשל, לאיזה A/B test experiment המשתמש שייך, מהו tier המנוי שלו, או האם הבקשה מגיעה ממקור פנימי לצורכי בדיקה. OpenTelemetry מספק מנגנון ל-Baggage בדיוק לצורך זה - מידע key-value שמוזרם יחד עם ה-trace context בכל קריאת רשת, וזמין לכל שירות בשרשרת בלי צורך להעביר אותו ידנית כפרמטר נפרד בכל API. יש להשתמש ב-baggage בזהירות - כל מידע שמוסיפים אליו מתווסף לכל בקשת HTTP יוצאת בשרשרת, כך שמידע כבד מדי ב-baggage יוצר overhead רשת מיותר; הכלל המעשי הוא להגביל אותו למספר קטן של ערכים קריטיים באמת, לא לכלי דיבוג כללי.

בדיקת Tracing לפני שהוא נדרש בפועל

הדרך הבטוחה לגלות ש-instrumentation שבור היא לא באמצע תקלת production אמיתית, אלא הרבה קודם - כחלק מתהליך בדיקות ייעודי. תרגול מומלץ הוא "game day" רבעוני: יוצרים תקלה מבוקרת (למשל מוסיפים latency מלאכותי לשירות פנימי, או מכבים שירות זמנית) ובודקים האם צוות התפעול מצליח לאתר את מקור הבעיה תוך דקות באמצעות ה-traces בלבד, בלי גישה לקוד המקור. תרגול כזה חושף מהר מאוד היכן ה-instrumentation חסר, היכן context propagation נשבר, והיכן dashboard חסר קישור ישיר בין המדדים לטרייסים. צוותים שמריצים תרגולים כאלה באופן קבוע מגלים כמעט תמיד פערי כיסוי שלא היו מודעים אליהם קודם.

Correlation עם Logs ו-Metrics

trace לבדו לא תמיד מספיק - לפעמים צריך לראות את השורה המדויקת בלוג שמסבירה למה קריאה מסוימת נכשלה. הפתרון הוא לצרף את ה-trace ID לכל שורת לוג שנכתבת בזמן עיבוד אותה בקשה, כך שממסך ה-trace אפשר "לקפוץ" ישירות ללוגים הרלוונטיים של אותה בקשה בדיוק, בלי לחפש ידנית לפי חותמת זמן. פלטפורמות observability מודרניות (Grafana, Datadog) תומכות ב-deep linking כזה באופן טבעי כשה-instrumentation עקבי בין השכבות. שילוב שלם כזה - trace שמקשר ללוגים שמקושרים למטריקות - הוא בדיוק המשמעות המעשית של Observability, כפי שמפורט במאמר על Observability Architecture. חשוב להדגיש שהקישור הזה לא נוצר "אוטומטית" מעצם השימוש בכלים המתאימים - הוא דורש משמעת הנדסית: כל שירות צריך להטמיע את ה-trace ID בפועל בפורמט הלוגים שלו (לרוב כשדה JSON ייעודי), אחרת גם עם כל התשתית הנכונה במקום, הקישור בפועל בין המסכים השונים פשוט לא יתקיים כשהוא הכי נחוץ.

טעויות נפוצות

  • שבירת שרשרת ה-propagation: שירות שקורא לשירות נוסף בלי להעביר את ה-trace context (למשל דרך תור הודעות שלא נועד לשאת headers) - יוצר "אי" מנותק בתוך ה-trace.
  • Instrumentation חלקי: תוספת tracing רק לחלק מהשירותים משאירה "black box" בדיוק במקום שהוא לרוב הכי חשוב לבדוק.
  • שכחת async boundaries: כשבקשה יוצרת job בתור לעיבוד ברקע, ה-trace context צריך להישמר גם בתוך ה-job עצמו, אחרת חוט העקיבה נקטע ברגע שהעיבוד עובר לרקע.
  • Overhead לא מבוקר: instrumentation כבד מדי (spans עודפים, attributes גדולים) יכול בעצמו להפוך לגורם latency במערכות עם עומס גבוה.
  • נתונים רגישים ב-attributes: הוספת מספרי כרטיס אשראי, סיסמאות או מידע אישי מזהה כ-attribute של span, בלי לשים לב שהוא נשמר בפלטפורמת observability חיצונית - סיכון privacy ותאימות רגולטורית של ממש שקל לפספס בזמן פיתוח מהיר.

מתי כדאי ומתי לא, וסיכום

Distributed Tracing הוא הכרחי כמעט לכל מערכת עם יותר משירות אחד או שניים שקוראים זה לזה, ובוודאי במערכות שמערבות תורי הודעות, קריאות אסינכרוניות, או תלות בשירותי צד שלישי איטיים. הוא פחות דחוף למונוליט קטן שבו stack trace רגיל מספיק לרוב החקירות, ושם ההשקעה בתשתית tracing מלאה עלולה להיות תקורה מיותרת ביחס לתועלת בפועל. ההשקעה משתלמת ברגע שזמן האבחון (MTTR) של תקלות שחוצות שירותים מתחיל להימדד בשעות במקום בדקות, או ברגע שיש יותר מצוות אחד שבעלות על חלקים שונים מהשרשרת ואף אחד מהם לא רואה את התמונה המלאה לבד.

מבחינה מעשית, כדאי להתחיל את הפריסה לא מכל השירותים בבת אחת, אלא מהנתיב העסקי הקריטי ביותר - למשל תהליך התשלום או ההזמנה - ולוודא שהוא מכוסה מקצה לקצה עם instrumentation מלא ו-context propagation תקין, לפני שמרחיבים לשאר המערכת. גישה הדרגתית כזו נותנת ערך אבחוני מיידי בדיוק במקום שהכי כואב, ומאפשרת לצוות ללמוד את הכלים והתהליכים על שטח מוגבל לפני שהם נדרשים להתמודד עם עשרות שירותים בבת אחת. הכלל המעשי לסיכום: אם יש ביניכם לבין תשובה לשאלה "איפה בדיוק בשרשרת הקריאות הבקשה הזו נתקעה?" יותר מכמה דקות חקירה ידנית, tracing מבוזר הוא ההשקעה הבאה שצריך לתעדף - ואם כבר יש observability בסיסית אך בלי חיבור בין השכבות, tracing הוא לרוב החוליה החסרה שהופכת נתונים גולמיים לתשובות מהירות.

תגיות: Distributed Tracing · OpenTelemetry · Spans · Sampling · Jaeger · Microservices · Observability

← חזרה לבלוג · צור קשר