איסוף נתוני טלמטריה של ליבת המערכת באמצעות eBPF

בדף הזה מפורטת סקירה כללית על מסגרת הטלמטריה של ליבת Extended Berkeley Packet Filter‏ (eBPF) ב-Android. מוסבר על ארכיטקטורת המסגרת, על מדדים מובנים ועל אופן ההוספה של מדדי ליבה חדשים.

‫Android כולל מסגרת טלמטריה מאוחדת של ליבת eBPF לאיסוף מדדי ליבה עם תקורה נמוכה וחשיפת המדדים האלה לשירותים במרחב המשתמש כמו statsd. המסגרת מצרפת תוכניות eBPF ניידות לנקודות מעקב (tracepoints) ול-kprobes של הליבה, וכך צוברת אירועי ליבה בתדירות גבוהה במקום ומייצאת היסטוגרמות או מונים קומפקטיים בלי לשנות את תמונת הליבה הגנרית (GKI) או לשבש את ממשק מודול הליבה (KMI).

גישות מסורתיות לטלמטריית ליבה מסתמכות על צומתי procfs או sysfs בהתאמה אישית, תיקוני ליבה מחוץ לעץ או הוקים של ספקים שדורשים חיבורים בהתאמה אישית בין הליבה, SELinux, ממשק Java Native‏ (JNI) ושירותים במרחב המשתמש. מסגרת הטלמטריה של ליבת eBPF יוצרת סטנדרטיזציה של צינור עיבוד הנתונים הזה בליבות Android נפוצות (ACK), ומספקת את היתרונות הבאים:

  • מניעת שינויים ב-ABI וב-KMI של ליבת המערכת: תוכניות eBPF מצורפות לנקודות מעקב קיימות של ליבת המערכת או ל-kprobes בלי לשנות את קוד המקור של ליבת המערכת, את ממשק ה-ABI של אפליקציית ליבת המערכת או להוסיף ווים של ספקים ל-Android, כמו TRACE_ANDROID_VH_*. יצרני מכשירים ושותפים של מערכות על שבב (SoC) יכולים להשתמש בטלמטריה של eBPF במקום להוסיף ווים חדשים של ספקי טלמטריה, או כדי להפסיק להשתמש בווים קיימים של ספקים.
  • שמירה על ניידות בין גרסאות ליבה: שימוש ב-BPF Compile Once - Run Everywhere (CO-RE)‎ וב-BPF Type Format (BTF)‎ כדי שתוכנית eBPF מקומפלת אחת תתאים באופן אוטומטי לשינויים בפריסה של מבנה הליבה הפנימי בגרסאות GKI, כמו android14-6.1 ומעלה.
  • מצמצם את התקורה של תקינות המערכת: מסנן ומקבץ אירועי ליבה בתדירות גבוהה ישירות במקור להיסטוגרמות לוגריתמיות (log2) קומפקטיות של 32 קטגוריות בבסיס 2, בדרך כלל פחות מ-1KB לכל מפת BPF. מרחב המשתמשים מאחזר רשומות מצטברות של מיפוי פעם ביום באמצעות קריאת מערכת (syscall) אחת של BPF_MAP_LOOKUP_BATCH batch.
  • פישוט השילוב של SELinux ו-JNI: הצמדת מפות טלמטריה ב-/sys/fs/bpf/kernelstats/ כדי להחיל ירושה של סוג SELinux ברמת הספרייה fs_bpf_kernelstats, ופתרון היסטים של חברי מבנה בזמן הריצה ב-JNI באמצעות מטא-נתונים של BTF כדי לבטל קוד ניתוח מותאם אישית של C++‎ כשמוסיפים מדדים חדשים.

במאמר הרחבת הליבה באמצעות eBPF מוסבר איך Android טוענת ומצמידה תוכניות ומפות של eBPF.

סקירה כללית של הארכיטקטורה

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

רכיבהתפקיד בצינור עיבוד הנתוניםהיקף
טוען BPF, ‏ bpfloaderטוען תוכניות eBPF שעברו קומפילציה מ-/system/etc/bpf/kernelstats/ בזמן האתחול אם הן תואמות לליבה הפעילה, ומצמיד מפות ל-/sys/fs/bpf/kernelstats/. אם ההגדרה מושבתת או לא נתמכת, הטעינה תתבצע בצורה חלקה.Generic
מדיניות SELinux, ‏ fs_bpf_kernelstatsהחלת תוויות SELinux ברמת הספרייה על /sys/fs/bpf/kernelstats/ כדי ש-system_server יוכל לקרוא מפות טלמטריה בלי עדכוני מדיניות SELinux לכל מדד.Generic
תוכנית eBPF ב-system/bpfprogs/telemetry/מתחבר ל-kernel tracepoints או ל-kprobes, מסנן אירועים וצובר מדדים למפות BPF.התאמה אישית לכל מדד
קורא JNI BTFמנתח מטא-נתונים של BTF מהקובץ הבינארי של BPF Executable and Linkable Format‏ (ELF) בזמן הריצה, שומר במטמון היסטים של איברים במבנה ומבצע קריאות של מפות באצווה למערכים פרימיטיביים של Java.Generic
כלי משיכה במרחב המשתמש ב-statsdשולף מפות היסטוגרמה מצטברות לפי לוח זמנים יומי, מחשב את ההפרשים ביחס להיסטוריה שנשמרה במטמון ויוצר אטומי משיכה של StatsEvent עם סוגים מוגדרים היטב.כלי עזר כללי עם רשומה אחת של case לכל אטום
סכימת טלמטריהמגדיר את ה-protobuf של ה-pull atom של statsd עם סוג מוגדר היטב ואת צבירת המדדים של ההיסטוגרמה או הערך בצד השרת.התאמה אישית לכל מדד

מודלים של צריכה במרחב המשתמש

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

מאפיין (property)מצבריםהאזנות
הגדרההיסטוגרמות או מונים מצטברים עם מצב (stateful) שגדלים באופן מונוטונירצף מסודר של אירועי ליבה עם חותמות זמן
תבנית גישהסקרים לפי דרישה במרחב המשתמש, למשל פעם ב-24 שעות דרך statsdמרחב המשתמשים צורך באופן רציף ממאגר נתונים מעגלי כדי למנוע אירועים שהושמטו
סוגים אופייניים של מפות BPFBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
מודל בו-זמניות (concurrency)חיפושים במפת הליבה הם אטומיים; קוראים מרובים יכולים לבצע סקר בלי נעילה של מרחב המשתמשסמנטיקה של קריאה הרסנית דורשת שליחה של שרשור קורא יחיד לצרכנים
תרחישים לדוגמההיסטוגרמות של זמן האחזור של ניהול הזיכרון, גדלים של הקצאת מאגר נתונים זמני של גישה ישירה לזיכרון (DMA-BUF), איטרטורים של מקור ההתעוררותאירועי מעקב מסוננים של לוח הזמנים, מעקבי חשמל של תת-מערכת בתדירות גבוהה

מדדי טלמטריה מובנים של ליבת המערכת

‫Android כולל תוכניות טלמטריה מובנות של ליבת eBPF שפועלות בליבות GKI מגרסה 6.1 ואילך, כולל android14-6.1,‏ android15-6.6,‏ android16-6.12 ו-android17-6.18. כל מדד של זמן אחזור וגודל משתמש בהיסטוגרמה אקספוננציאלית (log2) של 32 קטגוריות, שנצברת לאורך חלון של 24 שעות. בטבלה הבאה מפורטים מדדי הטלמטריה המובנים של ליבת המערכת:

אטום מטריתת-מערכתתיאורמפתח מאפיין
KswapdLatencyניהול זיכרוןדימון החלפת ליבה, kswapd, זמן אחזור של ביצוע התעוררות-לשינה במיקרו-שניותגישה לזיכרון לא אחיד (NUMA) node_id
DirectReclaimLatencyניהול זיכרוןזמן האחזור של החזרת זיכרון ישירה סינכרונית במיקרו-שניותמצב של מחסור בזיכרון (OOM) בתהליך oom_score_adj
CompactionDurationניהול זיכרוןמשך הזמן של דחיסת הזיכרון בכל המערכת במיקרו-שניות, במקום ה-hooks של ספק הדחיסה הקודםבכל המערכת
CmaAllocationLatencyניהול זיכרוןזמן האחזור של הקצאת דפים ב-Contiguous Memory Allocator ‏ (CMA) במיקרו-שניותשם הערימה של CMA
DmabufRequestSizesניהול זיכרוןגודל בקשות ההקצאה של DMA-BUF בבייטיםשם המאגר של המייצא
SuspendResumeLatencyניהול צריכת החשמלזמן האחזור בשלבי ההשהיה וההמשך של הליבה במיקרו-שניותהשעיית שלב הפעולה

הוספת מדד טלמטריה של ליבת המערכת

כדי להוסיף מדד מצטבר חדש לצינור עיבוד הנתונים של הטלמטריה של ליבת eBPF, מבצעים את השלבים הבאים לפי הסדר:

  1. כותבים את תוכנית ה-eBPF ב-system/bpfprogs/telemetry/ ויוצרים את הקובץ הבינארי ב-/system/etc/bpf/kernelstats/.
  2. בדיקת התאימות של פריסת המבנה של BTF לקורא BTF JNI הגנרי.
  3. רושמים את ה-pull atom ב-statsd.
  4. הגדרת סכימת ה-protobuf של אטום המשיכה.

בקטעים הבאים נשתמש ב-DirectReclaimLatency (directReclaim.c) כדוגמה.

כתיבת תוכנית eBPF

ב-system/bpfprogs/telemetry/, כוללים את <bpf_telemetry.h> ומגדירים את מפתח המפה ואת מפת הצבירה של הפלט. כדי לצמצם את הזיכרון שבשימוש במפה ולשמור על הפרטיות, צריך להשתמש במזהים של מספרים שלמים או בסוגי enum במפתחות של מפות BPF, ולא במחרוזות גולמיות:

#include <android_bpf_defs.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf_telemetry.h>
#include <stdint.h>

struct direct_reclaim_key {
    uint32_t process_state;
};

#define DIRECT_RECLAIM_START_MAP_ENTRIES 128
#define DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES 8

// Temporary map to store start time. Key: pid (thread_id), Value: start_time_ns
DEFINE_BPF_MAP_GRO(direct_reclaim_start_map, HASH, uint32_t, uint64_t,
                   DIRECT_RECLAIM_START_MAP_ENTRIES, AID_SYSTEM);

// Output map. Key: direct_reclaim_key, Value: accumulator_value (32 log2 buckets)
DEFINE_BPF_MAP_GRO(direct_reclaim_latency_map, HASH, struct direct_reclaim_key,
                   struct accumulator_value, DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES,
                   AID_SYSTEM);

ב-tracepoint handlers, מתעדים את חותמת הזמן של ההתחלה בכניסה, מחשבים את log2 latency bucket ביציאה באמצעות GET_LATENCY_BUCKET, ומגדילים את הערך של הרשומה במפת המצבר באמצעות INCREMENT_ACCUMULATOR:

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_begin", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_begin)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    SAVE_FIRST_START_TIME(direct_reclaim_start_map, &pid);
    return 0;
}

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_end", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_end)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    uint32_t bucket = GET_LATENCY_BUCKET(direct_reclaim_start_map, &pid);
    if (bucket == BPF_TELEMETRY_INVALID_BUCKET) return 0;

    struct task_struct* task = (struct task_struct*)bpf_get_current_task();
    int oom_score_adj = BPF_CORE_READ(task, signal, oom_score_adj);
    enum process_state state = get_process_state(oom_score_adj);

    struct direct_reclaim_key key = {.process_state = (uint32_t)state};
    INCREMENT_ACCUMULATOR(direct_reclaim_latency_map, &key, bucket);
    return 0;
}

אימות התאימות של פריסת המבנה של BTF

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

  • שיוך ELF למפה: שכבת ה-JNI מנתחת את המטא-נתונים של BTF ישירות מקובץ ה-BPF ELF שעבר הידור בדיסק, כמו /system/etc/bpf/kernelstats/directReclaim.bpf, באמצעות הפונקציה libbpf btf__parse.
  • פתרון סוג BTF: בגישה הראשונה, הקורא בונה ומאחסן במטמון ייצוג מבני של מפת המפתח ומבני הערך C, ומחשב את ההיסטים המצטברים של בייטים עבור חברי המבנה, רוחבי הבייטים של המספרים השלמים והספירות, וגדלי רכיבי המערך ממצביע הבסיס.
  • חיפוש וסדרות באצווה: כש-statsd שולף את המפה, שכבת ה-JNI מנפיקה פקודת BPF_MAP_LOOKUP_BATCH אחת ל-syscall של sys_bpf כדי לקרוא את כל רשומות הגיבוב בכמות גדולה, ומעתיקה את מזהי המפתחות שנפתרו ואת מערכי הדליות ישירות למערכי Java פרימיטיביים של long[] באמצעות פונקציית ה-JNI של SetLongArrayRegion.

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

רישום של אטום המשיכה ב-statsd

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

// 1. Dispatch the atom tag to pullKernelTelemetry() in onPullAtomLocked():
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY:
    return pullKernelTelemetry(atomTag, data);

// 2. Add the atom tag to the registered atoms list in registerKernelTelemetry():
int[] atoms = {
    FrameworkStatsLog.DIRECT_RECLAIM_LATENCY,
    // ...
};

// 3. Map the atom tag to its pinned BPF map path:
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY ->
    "/sys/fs/bpf/kernelstats/map_directReclaim_direct_reclaim_latency_map";

הגדרת ה-protobuf של עדכון Atom מסוג pull

הגדרת הודעת משיכה אטומית עם הקלדה חזקה ב-frameworks/proto_logging/ כך שתתאים למפתח המאפיין ולמערך ההיסטוגרמה של 32-bucket log2 שנוצר על ידי מפת ה-BPF:

message DirectReclaimLatency {
    enum ProcessState {
        PROCESS_STATE_UNSPECIFIED = 0;
        STATE_NATIVE = 1;
        STATE_SYSTEM = 2;
        STATE_PERSISTENT = 3;
        STATE_FOREGROUND = 4;
        STATE_VISIBLE = 5;
        STATE_PERCEPTIBLE = 6;
        STATE_SERVICE_BACKGROUND = 7;
        STATE_CACHED = 8;
    }
    optional ProcessState process_state = 1;
    repeated int32 duration_log2_buckets_micros = 2;
}