משתמש פותח כרטיס תמיכה: האפליקציה בנייד איטית. אתם בודקים את הלוחות (dashboards) שלכם. ניצול ה-CPU יציב. קצבי השגיאות הם אפס. נוריות ה-APM שלכם ירוקות ומבטיחות. ה-backend, לפי כל מדד נראה לעין, בריא. אך המשתמש לא טועה. האיטיות היא אמיתית, והיא מתרחשת אי שם במסדרון האפל והארוך שבין מכשיר Android לשרת שלכם.
הבעיה היא שרוב כלי ה-observability נעצרים בגבול האפליקציה. הם מודדים את מה שקורה לאחר שה-framework שלכם מנתח (parses) בקשה. הם עוקבים אחר שאילתות מסד נתונים, פגיעות מטמון (cache hits) וקריאות לשירותים מורחבים (downstream). מה שהם מפספסים הוא המכניקה של הבקשה עצמה: הזמן שקריאת OkHttp מבלה בתוך ה-Android HTTP stack, המעבר ברשת ניידת תנודתית, משא ומתן של ה-TLS handshake, וההמתנה השקטה שמתרחשת בתוך תורי ה-kernel לפני שהקוד שלכם בכלל רץ. הפערים הללו בולעים מילישניות — או שניות שלמות — בעוד ה-APM שלכם נותר שקט.
ההצפנה משלימה את העיוורון. אפליקציות Android מודרניות מנתבות הכל דרך BoringSSL. עד שהחבילה (packet) מגיעה ל-kernel network stack, כותרות ה-HTTP כבר מוצפנות. פקודת tcpdump סטנדרטית או network hook יראו רק רשומות TLS אטומות. אתם יכולים להבחין בכך שהתעבורה זורמת, אך אינכם יכולים לקרוא אותה. אתם בהחלט לא יכולים לקשר segment TCP ספציפי ברמת ה-kernel לקריאת API של משתמש מסוים. הקשר המעקב (trace context) שאתם זקוקים לו לכוד בתוך הטקסט המוצפן (ciphertext).
eBPF משנה את המשוואה מכיוון שהוא מאפשר לכם לבצע instrumentation למערכת מבפנים החוצה מבלי לשנות את קוד האפליקציה שלכם. במקום לבקש מהאפליקציה לדווח על השיהוי (latency) שלה, אתם מחברים תוכניות קטנות ישירות ל-kernel וספריות userspace קריטיות. התוכניות הללו צופות באירועים בזמן התרחשותם, מחלצות את מה שאתם צריכים ושולחות אותו ל-ring buffer. אין ניפוח (bloat) של SDK בתוך ה-Android APK שלכם מעבר ל-header קל משקל, ואין סוכן instrumentation שכותב מחדש את מחלקות ה-backend שלכם.
ההגדרה בת ארבעת השלבים
בניית הצינור (pipeline) הזה כוללת ארבע שכבות תצפית נפרדות.
1. עוגן ה-traceparent. במכשיר ה-Android, אתם מוסיפים OkHttp interceptor שמזריק כותרת W3C traceparent לכל בקשה יוצאת. זהו השינוי היחיד הנדרש בצד הנייד, והוא מינימלי. הכותרת עוברת בתוך המטען המוצפן עד ל-backend שלכם. מכיוון שהיא נמצאת בתוך שכבת ה-HTTP, היא שורדת בתוך הטקסט הגלוי (plaintext) שנועד מנוע ה-TLS לחשוף בסופו של דבר.
2. תזמון הגעת TCP. בשרת ה-backend, אתם משתמשים ב-eBPF Traffic Control hooks המחוברים לממשק הרשת. התוכניות הללו פועלות ברגע ש-TCP segments בודדים מגיעים. הן לוכדות מספרי רצף (sequence numbers) וחותמות זמן (timestamps) בקצה. כעת אתם יודעים בדיוק מתי הביטים עזבו את הכבל ונכנסו למכונה שלכם, זמן רב לפני שהאפליקציה שלכם קוראת אפילו byte בודד.
3. גששי הצפנה (Decryption probes). ה-backend שלכם מסתיים (terminates) את ה-TLS באמצעות OpenSSL או BoringSSL. כאן הארכיטקטורה הופכת למעניינת. באמצעות uprobes — גששים דינמיים ב-userspace — אתם מתחברים ל-SSL_write ו-SSL_read בתוך ספריית ה-TLS. הפונקציות הללו פועלות ברגע שנתוני ה-plaintext עוברים דרך מנוע ההצפנה. תוכנית ה-eBPF שלכם קוראת את ה-buffer המוצפן הזה, סורקת אחר כותרת ה-traceparent ומחלצת אותה. ה-kernel מחזיק כעת מיפוי ישיר בין זרם TCP גולמי לבין בקשת נייד ספציפית, מבלי שתצטרכו לטפל בתעודות או במפתחות בכלי מותאם אישית.
4. תור ב-Kernel (Kernel queueing). גם לאחר שהנתונים הופכים למוצפנים ומוכנים, האפליקציה שלכם עשויה שלא לצרוך אותם מיד. אתם מחברים kprobes לפונקציות kernel רלוונטיות המטפלות ב-socket buffers ובאירועי scheduling. זה מודד את השהיית התור (queueing latency): הזמן שבו בקשות ממתינות בתוך ה-kernel land מכיוון שהתהליך שלכם מתחרה על ה-CPU או פשוט טרם קרא ל-read().
כל ארבעת מקורות האותות כותבים אירועים לתוך eBPF ring buffer. תהליך sidecar הרץ ב-userspace מרוקן את ה-buffer הזה, מתאם אירועים לפי traceparent ID ובונה ציר זמן (timeline) אחד וקוהרנטי עבור כל בקשה. מה שהיה בעבר פיזור של רעש kernel לא מחובר הופך למעקב (trace) מובנה.
קריאת המסלול המלא
הפלט המורכב מוצג בדרך כלל כ-flame graph או כעץ span מובנה שמפרק את השיהוי לארבעה חלקים מוחשיים:
- זמן מעבר ברשת: משך הזמן מרגע שרדיו ה-Android שולח את הבייט האחרון של הבקשה ועד שכרטיס הרשת (NIC) של ה-backend מקבל אותו. כאן נמצאת התנודתיות של הרשת הסלולרית.
- משך לחיצת היד של TLS: הזמן המושקע בניהול המשא ומתן על המנהרה המוצפנת. ברשתות לא יציבות, זה יכול להגדיל משמעותית את הזמן לעומת העברת הנתונים בפועל.
- שיהוי בתור ה-kernel: זמן המושקע בבאפרים של ה-kernel ובתורי המתזמן (scheduler) לאחר שהמקטע מגיע אך לפני שה-userspace צורך אותו.
- זמן עיבוד האפליקציה: החלק שבו ה-framework של ה-backend והלוגיקה העסקית שלכם צורכים בפועל.
אתם עשויים לגלות שבקשת מובייל של 800ms מבלה רק 40ms בתוך מנתח ה-JSON שלכם. עוד 200ms נעלמים בתוך לחיצת יד TLS תקועה על גבי חיבור עם אובדן נתונים (lossy). עוד 300ms נעלמים בתוך backlog של ה-kernel על מארח (host) בעל עומס יתר. ה-APM שלכם דיווח רק על ה-40ms. ללא נראות ברמת ה-kernel, הייתם מבצעים אופטימיזציה לדבר הלא נכון לחלוטין.
עומס (Overhead) שבאמת עובר סקייל
סוכני APM מסורתיים משיגים את הנראות שלהם על ידי יירוט קריאות בתוך ה-runtime של השפה. הם עוטפים מתודות, מקצים אובייקטי span, ומבצעים סריאליזציה של נתוני טלמטריה בתוך ה-heap של התהליך. תחת עומס, העומס הזה מצטבר במהירות. עלויות הסריאליזציה עולות. הלחץ על ה-Garbage collection עולה. אתם משלמים על הנראות באמצעות המשאבים של האפליקציה שלכם עצמה.
תוכניות eBPF רצות בתוך מכונה וירטואלית של ה-kernel ומתקמפלות ב-JIT להוראות מכונה טבעיות (native). verifier בודק אותן מבחינת בטיחות לפני שהן נטענות. כל probe מוסיף מיקרו-שניות למסלול, לא מילי-שניות. העבודה הכבדה של התאמת אירועים ורינדור גרפים מתבצעת ב-sidecar, מחוץ ל-hot path של השירות שלכם. אתם לא מנפחים הקצאות heap. אתם לא מוסיפים "מיסי" סריאליזציה בתוך טיפול בבקשות. עבור שירותים שמריצים אלפי בקשות בשנייה, ההבדל הזה קריטי.
מה נדרש כדי להריץ את זה
זה לא פתרון קסם, וזה לא שירות SaaS מנוהל שניתן להפעיל בלחיצת כפתור. אתם זקוקים ל-kernel ב-backend עם תמיכה מודרנית ב-eBPF, כולל מידע טיפוס BTF כדי שה-probes שלכם יוכלו לעבור בבטחה על מבני ה-kernel. ספריית ה-TLS שלכם חייבת לחשוף סמלים (symbols) ש-uprobes יכולים למקד אליהם; אם אתם מספקים קובץ בינארי מקושר סטטית (statically linked) עם גרסת OpenSSL מוסרת (stripped) או מותאמת אישית מאוד, תצטרכו לקחת זאת בחשבון. עליכם גם לוודא שכותרת ה-traceparent שלכם שורדת מעבר דרך פרוקסיים או edge gateways בין לקוח המובייל לבין נקודת סיום ה-TLS.
אך עבור צוותים שמיצו את עצמם מטיקטים של "המובייל איטי" שמתנגשים בניתוח סיבת שורש (root-cause analysis), הארכיטקטורה הזו מחליפה ניחושים טקסיים באות (signal) מוצק. אתם מפסיקים לנחש לגבי "מזג האוויר" ברשת ומתחילים למדוד בקשות ספציפיות דרך צינורות ספציפיים.
השורה התחתונה
אתם לא חייבים להתייחס לתעבורת מובייל מוצפנת כזרם אטום (opaque) שהופך לנראה באופן קסם ברגע שהוא מגיע ל-framework שלכם. על ידי שילוב של כותרת OkHttp פשוטה עם probes של eBPF המוצבים אסטרטגית ב-backend, תוכלו לעקוב אחר בקשה בודדת ממכשיר Android דרך פענוח TLS, תורי kernel, ועד ללוגיקה של האפליקציה שלכם — מבלי לשכתב את אפליקציית המובייל ומבלי לבצע instrumentation לקוד ה-backend כפי ש-APM מסורתי דורש. זה לא רק שיפור הדרגתי. זו observability מקצה לקצה לאורך
