หน้านี้แสดงภาพรวมของเฟรมเวิร์ก 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_kernelstatsSELinux ระดับไดเรกทอรี และแก้ไขออฟเซ็ตสมาชิก 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 ชั่วโมงผ่าน statsd | Userspace ใช้ข้อมูลอย่างต่อเนื่องจากบัฟเฟอร์แบบวงแหวนเพื่อหลีกเลี่ยงเหตุการณ์ที่ถูกทิ้ง |
| ประเภท BPF Map ทั่วไป | BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH | BPF_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 ให้ทำตามขั้นตอนต่อไปนี้ตามลำดับ
- เขียนโปรแกรม eBPF ไว้ที่
system/bpfprogs/telemetry/และสร้างไบนารีลงใน/system/etc/bpf/kernelstats/ - ตรวจสอบความเข้ากันได้ของเลย์เอาต์ Struct BTF สำหรับผู้อ่าน BTF ของ JNI ทั่วไป
- ลงทะเบียนอะตอมแบบดึงข้อมูลใน
statsd - กำหนดสคีมา 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รายการเดียวไปยัง Syscallsys_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;
}