รวบรวมการวัดและส่งข้อมูลทางไกลของเคอร์เนลด้วย eBPF

หน้านี้แสดงภาพรวมของเฟรมเวิร์ก Telemetry ของเคอร์เนล Extended Berkeley Packet Filter (eBPF) ใน Android โดยอธิบายสถาปัตยกรรมของเฟรมเวิร์ก เมตริกในตัว และวิธีเพิ่มเมตริกใหม่ของเคอร์เนล

Android มีเฟรมเวิร์ก Telemetry ของเคอร์เนล eBPF แบบรวมสำหรับการเก็บรวบรวมเมตริกของเคอร์เนลที่มีค่าใช้จ่ายต่ำ และแสดงเมตริกเหล่านั้นไปยังบริการ Userspace เช่น statsd เฟรมเวิร์กจะรวมเหตุการณ์ของเคอร์เนลที่มีความถี่สูงไว้ในที่เดียว และ ส่งออกฮิสโตแกรมหรือตัวนับแบบย่อโดยไม่ต้องแก้ไข Generic Kernel Image (GKI) หรือทำให้ Kernel Module Interface (KMI) เสียหาย โดยการแนบโปรแกรม eBPF แบบพกพาเข้ากับ Tracepoint และ Kprobe ของเคอร์เนล

แนวทางแบบเดิมในการทำ Telemetry ของเคอร์เนลอาศัยโหนด procfs หรือ sysfs ที่กำหนดเอง, แพตช์ของเคอร์เนลแบบ Out-of-Tree หรือฮุกของผู้ให้บริการที่ต้องใช้การเชื่อมต่อที่กำหนดเองในเคอร์เนล, SELinux, Java Native Interface (JNI) และบริการ Userspace เฟรมเวิร์ก Telemetry ของเคอร์เนล eBPF ทำให้ไปป์ไลน์นี้เป็นมาตรฐานใน Android Common Kernels (ACK) และมีประโยชน์ดังนี้

  • หลีกเลี่ยงการเปลี่ยนแปลง ABI และ KMI ของเคอร์เนล: แนบโปรแกรม eBPF เข้ากับ Tracepoint หรือ Kprobe ของเคอร์เนลที่มีอยู่โดยไม่ต้องแก้ไขซอร์สโค้ดของเคอร์เนล เปลี่ยนอินเทอร์เฟซไบนารีของแอปพลิเคชัน (ABI) ของเคอร์เนล หรือเพิ่มฮุกของผู้ให้บริการ Android เช่น TRACE_ANDROID_VH_* ผู้ผลิตอุปกรณ์และพาร์ทเนอร์ System-on-Chip (SoC) สามารถใช้ Telemetry ของ eBPF แทนการเพิ่มฮุกของผู้ให้บริการ Telemetry ใหม่ หรือเพื่อย้ายข้อมูลออกจากฮุกของผู้ให้บริการที่มีอยู่
  • รักษาความสามารถในการพกพาข้ามเวอร์ชันเคอร์เนล: ใช้ BPF Compile Once - Run Everywhere (CO-RE) และ BPF Type Format (BTF) เพื่อให้โปรแกรม eBPF ที่คอมไพล์ครั้งเดียวปรับให้เข้ากับการเปลี่ยนแปลงเลย์เอาต์ Struct ภายในของเคอร์เนลในเวอร์ชัน GKI โดยอัตโนมัติ เช่น android14-6.1 ขึ้นไป
  • ลดค่าใช้จ่ายด้านสถานะของระบบ: กรองและรวมเหตุการณ์ของเคอร์เนลที่มีความถี่สูงที่แหล่งที่มาโดยตรงลงในฮิสโตแกรมลอการิทึมฐาน 2 (log2) ขนาด 32 บัคเก็ตแบบย่อ ซึ่งโดยปกติจะมีขนาดไม่เกิน 1 KB ต่อ BPF Map Userspace จะดึงข้อมูลรายการ Map ที่รวมไว้ 1 ครั้งต่อวันโดยใช้ Syscall แบบกลุ่ม BPF_MAP_LOOKUP_BATCH รายการเดียว
  • ลดความซับซ้อนของการผสานรวม SELinux และ JNI: ปักหมุด Map Telemetry ไว้ที่ /sys/fs/bpf/kernelstats/ เพื่อใช้การสืบทอดประเภท fs_bpf_kernelstats SELinux ระดับไดเรกทอรี และแก้ไขออฟเซ็ตสมาชิก Struct ใน JNI ที่รันไทม์โดยใช้ข้อมูลเมตา BTF เพื่อไม่จำเป็นต้องใช้โค้ดการแยกวิเคราะห์ C++ ที่กำหนดเองเมื่อเพิ่มเมตริกใหม่

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีที่ Android โหลดและปักหมุดโปรแกรมและ Map eBPF, ดู ขยายเคอร์เนลด้วย eBPF

ภาพรวมสถาปัตยกรรม

ไปป์ไลน์ Telemetry ของเคอร์เนล eBPF แยกคอมโพเนนต์โครงสร้างพื้นฐานทั่วไปออกจากคำจำกัดความเฉพาะของเมตริก เพื่อให้การเพิ่มเมตริกใหม่ของเคอร์เนลต้องมีการเปลี่ยนแปลงโค้ดน้อยที่สุด ตารางต่อไปนี้อธิบายคอมโพเนนต์ของไปป์ไลน์

คอมโพเนนต์บทบาทในไปป์ไลน์ขอบเขต
BPF Loader, bpfloaderโหลดโปรแกรม eBPF ที่คอมไพล์แล้วจาก /system/etc/bpf/kernelstats/ เมื่อบูตเครื่องหากเข้ากันได้กับเคอร์เนลที่กำลังทำงาน และปักหมุด Map ไว้ที่ /sys/fs/bpf/kernelstats/ ข้ามการโหลดอย่างราบรื่นเมื่อปิดใช้หรือไม่รองรับทั่วไป
นโยบาย SELinux, fs_bpf_kernelstatsใช้ป้ายกำกับ SELinux ระดับไดเรกทอรีกับ /sys/fs/bpf/kernelstats/ เพื่อให้ system_server อ่าน Map Telemetry ได้โดยไม่ต้องอัปเดตนโยบาย SELinux ต่อเมตริกทั่วไป
โปรแกรม eBPF ใน system/bpfprogs/telemetry/แนบเข้ากับ Tracepoint หรือ Kprobe ของเคอร์เนล กรองเหตุการณ์ และรวมเมตริกไว้ใน BPF Mapกำหนดเองต่อเมตริก
ผู้อ่าน BTF ของ JNIแยกวิเคราะห์ข้อมูลเมตา BTF จากไบนารี BPF Executable and Linkable Format (ELF) ที่คอมไพล์แล้วที่รันไทม์ แคชออฟเซ็ตสมาชิก Struct และอ่าน Map แบบกลุ่มลงในอาร์เรย์ Java ดั้งเดิมทั่วไป
ตัวดึงข้อมูล Userspace ใน statsdดึงข้อมูล Map ฮิสโตแกรมสะสมตามกำหนดการรายวัน คำนวณค่าเดลต้าเทียบกับประวัติที่แคชไว้ และสร้างอะตอม StatsEvent แบบดึงข้อมูลที่มีการพิมพ์อย่างเข้มงวดตัวช่วยทั่วไปที่มีรายการ case รายการเดียวต่ออะตอม
สคีมา Telemetryกำหนดอะตอม statsd แบบดึงข้อมูล protobuf ที่มีการพิมพ์อย่างเข้มงวด และการรวมเมตริกฮิสโตแกรมหรือเมตริกค่าฝั่งเซิร์ฟเวอร์กำหนดเองต่อเมตริก

โมเดลการใช้งาน Userspace

เมื่อออกแบบเมตริก Telemetry ของเคอร์เนล ให้เลือกระหว่างโมเดลการใช้งาน Userspace 2 แบบ โดยขึ้นอยู่กับว่าเมตริกแสดงถึงสถานะสะสมหรือลำดับเหตุการณ์ที่เรียงลำดับ ตารางต่อไปนี้เปรียบเทียบโมเดลทั้ง 2 แบบ

พร็อพเพอร์ตี้ตัวสะสมสตรีม
คำจำกัดความตัวนับสะสมหรือฮิสโตแกรมแบบมีสถานะและเพิ่มขึ้นเรื่อยๆลำดับเหตุการณ์ของเคอร์เนลที่มีการประทับเวลา
รูปแบบการเข้าถึงUserspace โพลตามความต้องการ เช่น ทุกๆ 24 ชั่วโมงผ่าน statsdUserspace ใช้ข้อมูลอย่างต่อเนื่องจากบัฟเฟอร์แบบวงแหวนเพื่อหลีกเลี่ยงเหตุการณ์ที่ถูกทิ้ง
ประเภท BPF Map ทั่วไปBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
โมเดลการทำงานพร้อมกันการค้นหา Map ของเคอร์เนลเป็นแบบอะตอม ผู้อ่านหลายคนสามารถโพลได้โดยไม่ต้องล็อก Userspaceความหมายของการอ่านแบบทำลายต้องใช้เธรดผู้อ่านเดียวที่ส่งไปยังผู้ใช้
ตัวอย่าง Use Caseฮิสโตแกรมเวลาในการตอบสนองของการจัดการหน่วยความจำ, ขนาดการจัดสรรบัฟเฟอร์การเข้าถึงหน่วยความจำโดยตรง (DMA-BUF), ตัววนซ้ำแหล่งที่มาของการปลุกเหตุการณ์การติดตามตัวกำหนดตารางเวลาที่กรองแล้ว, การติดตามการใช้พลังงานของระบบย่อยที่มีความถี่สูง

เมตริก Telemetry ของเคอร์เนลในตัว

Android มีโปรแกรม Telemetry ของเคอร์เนล eBPF ในตัวที่ทำงานในเคอร์เนล GKI 6.1 ขึ้นไป ซึ่งรวมถึง android14-6.1, android15-6.6, android16-6.12 และ android17-6.18 เมตริกเวลาในการตอบสนองและขนาดแต่ละรายการใช้ฮิสโตแกรมลอการิทึมฐาน 2 (log2) ขนาด 32 บัคเก็ตที่รวมไว้ในหน้าต่าง 24 ชั่วโมง ตารางต่อไปนี้แสดงเมตริก Telemetry ของเคอร์เนลในตัว

อะตอมเมตริกระบบย่อยคำอธิบายคีย์มิติข้อมูล
KswapdLatencyการจัดการหน่วยความจำเวลาในการตอบสนองของการดำเนินการแบบ Wake-to-Sleep ของ Daemon การสลับของเคอร์เนล kswapd ในหน่วยไมโครวินาทีnode_id ของ Non-Uniform Memory Access (NUMA)
DirectReclaimLatencyการจัดการหน่วยความจำเวลาในการตอบสนองของการเรียกคืนหน่วยความจำโดยตรงแบบซิงโครนัสในหน่วยไมโครวินาทีสถานะ oom_score_adj ของกระบวนการที่หน่วยความจำไม่เพียงพอ (OOM)
CompactionDurationการจัดการหน่วยความจำระยะเวลาการดำเนินการของการบีบอัดหน่วยความจำทั่วทั้งระบบในหน่วยไมโครวินาที ซึ่งแทนที่ฮุกของผู้ให้บริการการบีบอัดแบบเดิมทั่วทั้งระบบ
CmaAllocationLatencyการจัดการหน่วยความจำเวลาในการตอบสนองของการจัดสรรหน้า Contiguous Memory Allocator (CMA) ในหน่วยไมโครวินาทีชื่อฮีป CMA
DmabufRequestSizesการจัดการหน่วยความจำขนาดคำขอการจัดสรร DMA-BUF ในหน่วยไบต์ชื่อฮีปของตัวส่งออก
SuspendResumeLatencyการจัดการพลังงานเวลาในการตอบสนองในระยะการระงับและการกลับมาทำงานต่อของเคอร์เนลในหน่วยไมโครวินาทีระยะการดำเนินการระงับ

เพิ่มเมตริก Telemetry ของเคอร์เนล

หากต้องการเพิ่มเมตริกตัวสะสมใหม่ลงในไปป์ไลน์ Telemetry ของเคอร์เนล eBPF ให้ทำตามขั้นตอนต่อไปนี้ตามลำดับ

  1. เขียนโปรแกรม eBPF ไว้ที่ system/bpfprogs/telemetry/ และสร้างไบนารีลงใน /system/etc/bpf/kernelstats/
  2. ตรวจสอบความเข้ากันได้ของเลย์เอาต์ Struct BTF สำหรับผู้อ่าน BTF ของ JNI ทั่วไป
  3. ลงทะเบียนอะตอมแบบดึงข้อมูลใน statsd
  4. กำหนดสคีมา protobuf ของอะตอมแบบดึงข้อมูล

ส่วนต่อไปนี้ใช้ DirectReclaimLatency (directReclaim.c) เป็นตัวอย่าง

เขียนโปรแกรม eBPF

ใน system/bpfprogs/telemetry/ ให้รวม <bpf_telemetry.h> และกำหนดคีย์ Map และ Map ตัวสะสมเอาต์พุต ใช้ตัวระบุจำนวนเต็มหรือ Enum ในคีย์ BPF Map แทนสตริงดิบเพื่อลดหน่วยความจำที่ใช้ของ Map และรักษาความเป็นส่วนตัว

#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 ให้บันทึกการประทับเวลาเริ่มต้นเมื่อเข้าสู่ระบบ คำนวณบัคเก็ตเวลาในการตอบสนอง log2 เมื่อออกจากระบบโดยใช้ GET_LATENCY_BUCKET และเพิ่มรายการ Map ตัวสะสมด้วย 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;
}

ตรวจสอบความเข้ากันได้ของเลย์เอาต์ Struct BTF

คุณไม่จำเป็นต้องเขียนโค้ด JNI C++ ที่กำหนดเองเมื่อเพิ่ม Map ฮิสโตแกรมใหม่ ตราบใดที่ Struct คีย์และค่า Map ใช้ประเภทจำนวนเต็ม, Enum และอาร์เรย์คงที่มาตรฐาน ผู้อ่าน JNI ทั่วไปจะแก้ไขเลย์เอาต์ Struct แบบไดนามิกที่รันไทม์ใน 3 ระยะแทนการแคสต์รายการ BPF Map เป็น Struct C ที่ฮาร์ดโค้ดไว้ที่เวลาคอมไพล์

  • การเชื่อมโยง ELF กับ Map: เลเยอร์ JNI จะแยกวิเคราะห์ข้อมูลเมตา BTF โดยตรงจากไฟล์ BPF ELF ที่คอมไพล์แล้วบนดิสก์ เช่น /system/etc/bpf/kernelstats/directReclaim.bpf โดยใช้ฟังก์ชัน btf__parse ของ libbpf
  • การแก้ไขประเภท BTF: เมื่อเข้าถึงครั้งแรก ผู้อ่านจะสร้างและแคชการแสดงโครงสร้างของ Struct C คีย์และค่าของ Map โดยคำนวณออฟเซ็ตไบต์สะสมสำหรับสมาชิก Struct, ความกว้างไบต์ของจำนวนเต็มและ Enum รวมถึงขนาดองค์ประกอบอาร์เรย์จากตัวชี้ฐาน
  • การค้นหาและการซีเรียลไลซ์แบบกลุ่ม: เมื่อ statsd ดึงข้อมูล Map เลเยอร์ JNI จะออกคำสั่ง BPF_MAP_LOOKUP_BATCH รายการเดียวไปยัง Syscall sys_bpf เพื่ออ่านรายการแฮชทั้งหมดแบบกลุ่ม และคัดลอกรหัสคีย์ที่แก้ไขแล้วและอาร์เรย์บัคเก็ตลงในอาร์เรย์ long[] Java ดั้งเดิมโดยตรงโดยใช้ฟังก์ชัน SetLongArrayRegion ของ JNI

หาก Struct C ของ BPF เปลี่ยนไปในเวอร์ชันที่จะเผยแพร่ในอนาคต ส่วน BTF ที่คอมไพล์ใหม่จะอัปเดตโดยอัตโนมัติ และผู้อ่าน JNI จะปรับตัวที่รันไทม์โดยไม่ต้องคอมไพล์ JNI ใหม่

ลงทะเบียนอะตอมแบบดึงข้อมูลใน statsd

ใน StatsPullAtomService.java ตัวช่วย pullKernelTelemetry ทั่วไปจะอ่าน BPF Map ที่ปักหมุดไว้ คำนวณค่าเดลต้าของบัคเก็ตเทียบกับจำนวนที่แคชไว้ของการดึงข้อมูลครั้งก่อน และส่งรายการ StatsEvent การเพิ่มเมตริก Telemetry ใหม่ของเคอร์เนลต้องมีการเพิ่มขนาดเล็กเพียง 3 รายการเท่านั้น

// 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 ของอะตอมแบบดึงข้อมูล

กำหนดข้อความอะตอมแบบดึงข้อมูลที่มีการพิมพ์อย่างเข้มงวดใน frameworks/proto_logging/ ให้ตรงกับคีย์มิติข้อมูลและอาร์เรย์ฮิสโตแกรม log2 ขนาด 32 บัคเก็ตที่สร้างโดย BPF Map

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