eBPF की मदद से, कर्नल टेलीमेट्री इकट्ठा करना

इस पेज पर, Android में Extended Berkeley Packet Filter (eBPF) कर्नेल टेलीमेट्री फ़्रेमवर्क के बारे में खास जानकारी दी गई है. इसमें फ़्रेमवर्क के आर्किटेक्चर, पहले से मौजूद मेट्रिक, और नई कर्नेल मेट्रिक जोड़ने के तरीके के बारे में बताया गया है.

Android में, eBPF कर्नल टेलीमेट्री फ़्रेमवर्क शामिल है. इसका इस्तेमाल, कम ओवरहेड वाली कर्नल मेट्रिक इकट्ठा करने के लिए किया जाता है. साथ ही, इन मेट्रिक को statsd जैसी यूज़रस्पेस सेवाओं के लिए उपलब्ध कराया जाता है. पोर्टेबल ईबीपीएफ़ प्रोग्राम को कर्नेल ट्रेसपॉइंट और केप्रोब से अटैच करके, फ़्रेमवर्क एक ही जगह पर ज़्यादा फ़्रीक्वेंसी वाले कर्नेल इवेंट इकट्ठा करता है. साथ ही, सामान्य कर्नेल इमेज (जीकेआई) में बदलाव किए बिना या कर्नेल मॉड्यूल इंटरफ़ेस (केएमआई) को तोड़े बिना, कॉम्पैक्ट हिस्टोग्राम या काउंटर एक्सपोर्ट करता है.

कर्नेल टेलीमेट्री के पारंपरिक तरीके, कस्टम procfs या sysfs नोड, आउट-ऑफ़-ट्री कर्नेल पैच या वेंडर हुक पर निर्भर करते हैं. इनके लिए, कर्नेल, SELinux, Java नेटिव इंटरफ़ेस (जेएनआई), और यूज़रस्पेस सेवाओं में कस्टम प्लंबिंग की ज़रूरत होती है. eBPF कर्नेल टेलीमेट्री फ़्रेमवर्क, Android कॉमन कर्नेल (एसीके) में इस पाइपलाइन को स्टैंडर्ड बनाता है. साथ ही, ये फ़ायदे देता है:

  • कर्नेल एबीआई और केएमआई में बदलाव नहीं करता: यह कर्नेल के सोर्स कोड में बदलाव किए बिना, eBPF प्रोग्राम को मौजूदा कर्नेल ट्रेसपॉइंट या kprobe से जोड़ता है. साथ ही, यह कर्नेल ऐप्लिकेशन बाइनरी इंटरफ़ेस (एबीआई) में बदलाव नहीं करता है. इसके अलावा, यह TRACE_ANDROID_VH_* जैसे Android वेंडर हुक भी नहीं जोड़ता है. डिवाइस बनाने वाली कंपनियां और सिस्टम-ऑन-चिप (एसओसी) पार्टनर, नए टेलीमेट्री वेंडर हुक जोड़ने या मौजूदा वेंडर हुक से माइग्रेट करने के बजाय, eBPF टेलीमेट्री का इस्तेमाल कर सकते हैं.
  • कर्नेल के सभी वर्शन पर पोर्टेबिलिटी बनाए रखता है: यह BPF Compile Once - Run Everywhere (CO-RE) और BPF Type Format (BTF) का इस्तेमाल करता है. इससे कंपाइल किया गया एक eBPF प्रोग्राम, GKI के अलग-अलग वर्शन (जैसे, android14-6.1 और इससे ऊपर के वर्शन) में कर्नेल स्ट्रक्चर के लेआउट में हुए बदलावों के हिसाब से अपने-आप अडजस्ट हो जाता है.
  • सिस्टम के परफ़ॉर्मेंस पर कम असर पड़ता है: यह फ़िल्टर करता है और ज़्यादा फ़्रीक्वेंसी वाले कर्नल इवेंट को सीधे सोर्स पर, 32 बकेट वाले बेस-2 लॉगरिथमिक (log2) हिस्टोग्राम में एग्रीगेट करता है. आम तौर पर, हर बीपीएफ़ मैप के लिए 1 केबी से कम. Userspace, एक BPF_MAP_LOOKUP_BATCH बैच syscall का इस्तेमाल करके, दिन में एक बार एग्रीगेट की गई मैप एंट्री को वापस लाता है.
  • SELinux और जेएनआई इंटिग्रेशन को आसान बनाता है: डायरेक्ट्री-लेवल पर fs_bpf_kernelstats SELinux टाइप इनहेरिटेंस लागू करने के लिए, /sys/fs/bpf/kernelstats/ में टेलीमेट्री मैप पिन करता है. साथ ही, नई मेट्रिक जोड़ते समय कस्टम C++ पार्सिंग कोड को हटाने के लिए, बीटीएफ़ मेटाडेटा का इस्तेमाल करके जेएनआई में रन टाइम पर स्ट्रक्चर मेंबर ऑफ़सेट को रिज़ॉल्व करता है.

Android, eBPF प्रोग्राम और मैप को कैसे लोड और पिन करता है, इस बारे में ज़्यादा जानने के लिए, eBPF की मदद से कर्नेल को बेहतर बनाएं लेख पढ़ें.

आर्किटेक्चर के बारे में खास जानकारी

eBPF कर्नल टेलीमेट्री पाइपलाइन, सामान्य इन्फ़्रास्ट्रक्चर कॉम्पोनेंट को मेट्रिक के हिसाब से तय की गई परिभाषाओं से अलग करती है. इससे, नई कर्नल मेट्रिक जोड़ने के लिए, कोड में कम से कम बदलाव करने पड़ते हैं. इस टेबल में, पाइपलाइन के कॉम्पोनेंट के बारे में बताया गया है:

कॉम्पोनेंटपाइपलाइन में भूमिकादायरा
बीपीएफ़ लोडर, bpfloaderयह बूट के समय, /system/etc/bpf/kernelstats/ से कंपाइल किए गए eBPF प्रोग्राम लोड करता है. हालांकि, ऐसा तब होता है, जब प्रोग्राम, चालू कर्नेल के साथ काम करता हो. साथ ही, यह मैप को /sys/fs/bpf/kernelstats/ पर पिन करता है. इस कुकी के बंद होने या काम न करने पर, यह कुकी बिना किसी रुकावट के लोड होती है.Generic
SELinux नीति, fs_bpf_kernelstatsयह फ़ंक्शन, /sys/fs/bpf/kernelstats/ पर डायरेक्ट्री-लेवल के SELinux लेबल लागू करता है, ताकि system_server, हर मेट्रिक के लिए SELinux नीति के अपडेट के बिना टेलीमेट्री मैप पढ़ सके.Generic
system/bpfprogs/telemetry/ में eBPF प्रोग्रामयह कर्नल ट्रेसपॉइंट या kprobes से अटैच होता है, इवेंट फ़िल्टर करता है, और मेट्रिक को BPF मैप में एग्रीगेट करता है.हर मेट्रिक के लिए कस्टम
JNI BTF रीडरयह रन टाइम पर, कंपाइल किए गए BPF Executable and Linkable Format (ELF) बाइनरी से बीटीएफ़ मेटाडेटा पार्स करता है. साथ ही, स्ट्रक्चर के सदस्यों के ऑफ़सेट को कैश मेमोरी में सेव करता है. इसके अलावा, यह प्रिमिटिव Java ऐरे में बैच मैप रीड करता है.Generic
statsd में Userspace pullerयह रोज़ के शेड्यूल के हिसाब से, कुल हिस्टोग्राम मैप को पुल करता है. साथ ही, कैश मेमोरी में सेव किए गए इतिहास के हिसाब से डेल्टा की गिनती करता है और स्ट्रॉन्गली टाइप्ड StatsEvent पुल ऐटम बनाता है.हर ऐटम के लिए एक case एंट्री वाला सामान्य हेल्पर
टेलीमेट्री स्कीमायह statsd पुल ऐटम प्रोटोबफ़ और सर्वर-साइड हिस्टोग्राम या वैल्यू मेट्रिक एग्रीगेशन को मज़बूती से टाइप करता है.हर मेट्रिक के लिए कस्टम

उपयोगकर्ता स्पेस के इस्तेमाल के मॉडल

कर्नेल टेलीमेट्री मेट्रिक डिज़ाइन करते समय, दो यूज़रस्पेस इस्तेमाल करने वाले मॉडल में से किसी एक को चुनें. यह इस बात पर निर्भर करता है कि मेट्रिक, कुल स्थिति को दिखाती है या क्रम से लगाए गए इवेंट के क्रम को. यहां दी गई टेबल में, दोनों मॉडल की तुलना की गई है:

प्रॉपर्टीऐक्युमुलेटरस्ट्रीम
परिभाषास्टेटफ़ुल, लगातार बढ़ते हुए क्यूमलेटिव काउंटर या हिस्टोग्रामटाइमस्टैंप वाले कर्नेल इवेंट का क्रम
ऐक्सेस पैटर्नउपयोगकर्ता के अनुरोध पर, उपयोगकर्ता स्पेस में पोलिंग की सुविधा. जैसे, statsd के ज़रिए हर 24 घंटे में एक बारयूज़रस्पेस, रिंग बफ़र से लगातार डेटा लेता है, ताकि इवेंट ड्रॉप न हों
BPF मैप के सामान्य टाइपBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
कॉनकरेंसी मॉडलकर्नेल मैप लुकअप, ऐटॉमिक होते हैं. कई रीडर, यूज़रस्पेस लॉकिंग के बिना पोल कर सकते हैंडेटा को बदलने वाली रीड सिमैंटिक्स के लिए, एक रीडर थ्रेड की ज़रूरत होती है, जो उपभोक्ताओं को डेटा भेजती है
इस्तेमाल के उदाहरणमेमोरी मैनेजमेंट की लेटेंसी के हिस्टोग्राम, डायरेक्ट मेमोरी ऐक्सेस बफ़र (डीएमए-बफ़) के साइज़ का बंटवारा, वेकअप सोर्स इटरेटरफ़िल्टर किए गए शेड्यूलर ट्रेस इवेंट, हाई-फ़्रीक्वेंसी सबसिस्टम पावर ट्रेस

पहले से मौजूद कर्नल टेलीमेट्री मेट्रिक

Android में पहले से मौजूद eBPF कर्नेल टेलीमेट्री प्रोग्राम शामिल हैं. ये GKI 6.1 और इसके बाद के वर्शन वाले कर्नेल पर काम करते हैं. इनमें android14-6.1, android15-6.6, android16-6.12, और android17-6.18 शामिल हैं. हर लेटेंसी और साइज़ मेट्रिक, 32-बकेट एक्सपोनेंशियल (log2) हिस्टोग्राम का इस्तेमाल करती है. इसे 24 घंटे की विंडो में एग्रीगेट किया जाता है. यहां दी गई टेबल में, पहले से मौजूद कर्नेल टेलीमेट्री मेट्रिक की सूची दी गई है:

मेट्रिक ऐटमसबसिस्टमब्यौराडाइमेंशन कुंजी
KswapdLatencyमेमोरी मैनेज करनाकर्नेल स्वैप डीमन, kswapd, वेक-टू-स्लीप मोड में जाने में लगने वाला समय, माइक्रोसेकंड मेंनॉन-यूनिफ़ॉर्म मेमोरी ऐक्सेस (एनयूएमए) node_id
DirectReclaimLatencyमेमोरी मैनेज करनासिंक्रोनस डायरेक्ट मेमोरी रिक्लेम लेटेंसी, माइक्रोसेकंड मेंप्रोसेस आउट-ऑफ़-मेमोरी (ओओएम) oom_score_adj स्थिति
CompactionDurationमेमोरी मैनेज करनासिस्टम-वाइड मेमोरी कंपैक्शन को पूरा होने में लगने वाला समय, माइक्रोसेकंड में. यह लेगसी कंपैक्शन वेंडर हुक की जगह लेता हैसिस्टम-वाइड
CmaAllocationLatencyमेमोरी मैनेज करनालगातार मेमोरी का बंटवारा करने वाले (सीएमए) पेज को ऐलोकेट करने में लगने वाला इंतज़ार का समय, माइक्रोसेकंड मेंसीएमए हीप का नाम
DmabufRequestSizesमेमोरी मैनेज करनाडीएमए-बीयूएफ़ के लिए मेमोरी के अनुरोध का साइज़, बाइट मेंएक्सपोर्टर हीप का नाम
SuspendResumeLatencyपावर मैनेजमेंटकर्नेल के बंद होने और फिर से शुरू होने के चरणों के दौरान लेटेंसी, माइक्रोसेकंड मेंकार्रवाई के चरण को निलंबित करना

कर्नल टेलीमेट्री मेट्रिक जोड़ना

eBPF कर्नल टेलीमेट्री पाइपलाइन में नई एक्युमुलेटर मेट्रिक जोड़ने के लिए, इस क्रम में यह तरीका अपनाएं:

  1. system/bpfprogs/telemetry/ में eBPF प्रोग्राम लिखें और बाइनरी को /system/etc/bpf/kernelstats/ में बनाएं.
  2. जेनरिक जेएनआई बीटीएफ़ रीडर के लिए, बीटीएफ़ स्ट्रक्चर लेआउट के साथ काम करने की सुविधा की पुष्टि करें.
  3. statsd में पुल ऐटम को रजिस्टर करें.
  4. पुल ऐटम प्रोटोबफ़ स्कीमा तय करें.

यहां दिए गए सेक्शन में, उदाहरण के तौर पर DirectReclaimLatency (directReclaim.c) का इस्तेमाल किया गया है.

eBPF प्रोग्राम लिखना

system/bpfprogs/telemetry/ में, <bpf_telemetry.h> को शामिल करें. साथ ही, मैप की और आउटपुट इकट्ठा करने वाले मैप को तय करें. बीपीएफ़ मैप की कुंजियों में, रॉ स्ट्रिंग के बजाय पूर्णांक आइडेंटिफ़ायर या एनम का इस्तेमाल करें. इससे मैप की मेमोरी फ़ुटप्रिंट कम हो जाएगी और निजता बनी रहेगी:

#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);

ट्रेसपॉइंट हैंडलर में, एंट्री पर शुरू होने का टाइमस्टैंप रिकॉर्ड करें. इसके बाद, GET_LATENCY_BUCKET का इस्तेमाल करके, एग्ज़िट पर log2 लेटेंसी बकेट का हिसाब लगाएं. साथ ही, 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;
}

बीटीएफ़ स्ट्रक्चर लेआउट के साथ काम करने की पुष्टि करना

नया हिस्टोग्राम मैप जोड़ते समय, आपको कस्टम जेएनआई C++ कोड लिखने की ज़रूरत नहीं है. ऐसा तब तक है, जब तक आपके मैप की कुंजी और वैल्यू स्ट्रक्चर में स्टैंडर्ड इंटिजर, एनम, और फ़िक्स्ड अरे टाइप का इस्तेमाल किया जाता है. कंपाइल के समय, BPF मैप एंट्री को हार्डकोड किए गए C स्ट्रक्चर में कास्ट करने के बजाय, जेनरिक जेएनआई रीडर, रन टाइम के दौरान स्ट्रक्चर लेआउट को डाइनैमिक तौर पर तीन चरणों में रिज़ॉल्व करता है:

  • ईएलएफ़-टू-मैप असोसिएशन: जेएनआई लेयर, डिस्क पर मौजूद कंपाइल की गई बीपीएफ़ ईएलएफ़ फ़ाइल से बीटीएफ़ मेटाडेटा को सीधे पार्स करती है. जैसे, libbpf btf__parse फ़ंक्शन का इस्तेमाल करके /system/etc/bpf/kernelstats/directReclaim.bpf.
  • बीटीएफ़ टाइप रिज़ॉल्यूशन: पहली बार ऐक्सेस करने पर, रीडर मैप की कुंजी और वैल्यू सी स्ट्रक्चर का स्ट्रक्चरल रिप्रज़ेंटेशन बनाता है और उसे कैश मेमोरी में सेव करता है. साथ ही, स्ट्रक्चर के सदस्यों के लिए कुल बाइट ऑफ़सेट, पूर्णांक और एनम बाइट की चौड़ाई, और बेस पॉइंटर से ऐरे एलिमेंट के साइज़ का हिसाब लगाता है.
  • बैच लुकअप और सीरियलाइज़ेशन: जब statsd मैप को पुल करता है, तो जेएनआई लेयर, sys_bpf syscall को एक BPF_MAP_LOOKUP_BATCH कमांड जारी करती है. इससे सभी हैश एंट्री को एक साथ पढ़ा जाता है. साथ ही, रिज़ॉल्व किए गए कुंजी आईडी और बकेट कलेक्शन को सीधे प्रिमिटिव Java long[] कलेक्शन में कॉपी किया जाता है. इसके लिए, SetLongArrayRegion जेएनआई फ़ंक्शन का इस्तेमाल किया जाता है.

अगर आने वाले समय में किसी रिलीज़ में बीपीएफ़ सी स्ट्रक्चर में बदलाव होता है, तो फिर से कंपाइल किया गया बीटीएफ़ सेक्शन अपने-आप अपडेट हो जाता है. साथ ही, जेएनआई रीडर, रन टाइम के दौरान अडैप्ट हो जाता है. इसके लिए, जेएनआई को फिर से कंपाइल करने की ज़रूरत नहीं पड़ती.

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";

पुल ऐटम प्रोटोबफ़ को तय करना

frameworks/proto_logging/ में स्ट्रॉन्गली टाइप्ड पुल ऐटम मैसेज को तय करें, ताकि डाइमेंशन कुंजी और बीपीएफ़ मैप से जनरेट किए गए 32-बकेट log2 हिस्टोग्राम कलेक्शन को मैच किया जा सके:

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;
}