اخبار منصات الأفلام

اكتشاف الجيران المزعجين باستخدام eBPF | بواسطة مدونة Netflix للتكنولوجيا | سبتمبر 2024


ال sched_wakeup و sched_wakeup_new يتم استدعاء الخطافات عندما تغير حالة العملية من “السكون” إلى “القابل للتشغيل”. إنها تتيح لنا تحديد متى تكون العملية جاهزة للتشغيل وتنتظر وقت وحدة المعالجة المركزية. خلال هذا الحدث، نقوم بإنشاء طابع زمني وتخزينه في خريطة تجزئة eBPF باستخدام معرف العملية كمفتاح.

struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_TASK_ENTRIES);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u64));
} runq_lat SEC(".maps");

SEC("tp_btf/sched_wakeup")
int tp_sched_wakeup(u64 *ctx)
{
struct task_struct *task = (void *)ctx[0];
u32 pid = task->pid;
u64 ts = bpf_ktime_get_ns();

bpf_map_update_elem(&runq_lat, &pid, &ts, BPF_NOEXIST);
return 0;
}

على العكس من ذلك، sched_switch يتم تشغيل الخطاف عندما تقوم وحدة المعالجة المركزية بالتبديل بين العمليات. يوفر هذا الخطاف مؤشرات للعملية التي تستخدم وحدة المعالجة المركزية حاليًا والعملية التي على وشك تولي المسؤولية. نستخدم معرف عملية المهمة القادمة (PID) لجلب الطابع الزمني من خريطة eBPF. يمثل هذا الطابع الزمني وقت دخول العملية إلى قائمة الانتظار التي قمنا بتخزينها مسبقًا. نقوم بعد ذلك بحساب زمن وصول قائمة انتظار التشغيل ببساطة عن طريق طرح الطوابع الزمنية.

SEC("tp_btf/sched_switch")
int tp_sched_switch(u64 *ctx)
{
struct task_struct *prev = (struct task_struct *)ctx[1];
struct task_struct *next = (struct task_struct *)ctx[2];
u32 prev_pid = prev->pid;
u32 next_pid = next->pid;

// fetch timestamp of when the next task was enqueued
u64 *tsp = bpf_map_lookup_elem(&runq_lat, &next_pid);
if (tsp == NULL) {
return 0; // missed enqueue
}

// calculate runq latency before deleting the stored timestamp
u64 now = bpf_ktime_get_ns();
u64 runq_lat = now - *tsp;

// delete pid from enqueued map
bpf_map_delete_elem(&runq_lat, &next_pid);
....

إحدى مزايا eBPF هي قدرته على توفير مؤشرات إلى هياكل بيانات kernel الفعلية التي تمثل العمليات أو الخيوط، والمعروفة أيضًا باسم المهام في مصطلحات kernel. تتيح هذه الميزة الوصول إلى ثروة من المعلومات المخزنة حول العملية. لقد طلبنا معرف cgroup الخاص بالعملية لربطه بحاوية لحالة الاستخدام المحددة لدينا. ومع ذلك، يتم حماية معلومات مجموعة cgroup في البنية بواسطة قفل RCU (تحديث قراءة النسخ).

للوصول بأمان إلى هذه المعلومات المحمية بواسطة RCU، يمكننا الاستفادة من kfuncs في eBPF. kfuncs هي وظائف kernel يمكن استدعاؤها من برامج eBPF. هناك kfuncs المتاحة لقفل وفتح الأقسام الهامة لجانب القراءة RCU. تضمن هذه الوظائف بقاء برنامج eBPF الخاص بنا آمنًا وفعالًا أثناء استرداد معرف cgroup من بنية المهمة.

void bpf_rcu_read_lock(void) __ksym;
void bpf_rcu_read_unlock(void) __ksym;

u64 get_task_cgroup_id(struct task_struct *task)
{
struct css_set *cgroups;
u64 cgroup_id;
bpf_rcu_read_lock();
cgroups = task->cgroups;
cgroup_id = cgroups->dfl_cgrp->kn->id;
bpf_rcu_read_unlock();
return cgroup_id;
}

بعد أن أصبحت البيانات جاهزة، يجب علينا حزمها وإرسالها إلى مساحة المستخدمين. لهذا الغرض، اخترنا المخزن المؤقت الدائري eBPF. إنه فعال وعالي الأداء وسهل الاستخدام. يمكنه التعامل مع سجلات البيانات ذات الطول المتغير ويسمح بقراءة البيانات دون الحاجة إلى نسخ ذاكرة إضافية أو مكالمات النظام. ومع ذلك، فإن الكم الهائل من نقاط البيانات كان يتسبب في أن يستخدم برنامج مساحة المستخدم قدرًا كبيرًا جدًا من وحدة المعالجة المركزية، لذلك قمنا بتطبيق محدد للمعدل في eBPF لأخذ عينات من البيانات بشكل فعال.

struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, MAX_TASK_ENTRIES);
__uint(key_size, sizeof(u64));
__uint(value_size, sizeof(u64));
} cgroup_id_to_last_event_ts SEC(".maps");

struct runq_event {
u64 prev_cgroup_id;
u64 cgroup_id;
u64 runq_lat;
u64 ts;
};

SEC("tp_btf/sched_switch")
int tp_sched_switch(u64 *ctx)
{
// ....
// The previous code
// ....

u64 prev_cgroup_id = get_task_cgroup_id(prev);
u64 cgroup_id = get_task_cgroup_id(next);

// per-cgroup-id-per-CPU rate-limiting
// to balance observability with performance overhead
u64 *last_ts =
bpf_map_lookup_elem(&cgroup_id_to_last_event_ts, &cgroup_id);
u64 last_ts_val = last_ts == NULL ? 0 : *last_ts;

// check the rate limit for the cgroup_id in consideration
// before doing more work
if (now - last_ts_val < RATE_LIMIT_NS) {
// Rate limit exceeded, drop the event
return 0;
}

struct runq_event *event;
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);

if (event) {
event->prev_cgroup_id = prev_cgroup_id;
event->cgroup_id = cgroup_id;
event->runq_lat = runq_lat;
event->ts = now;
bpf_ringbuf_submit(event, 0);
// Update the last event timestamp for the current cgroup_id
bpf_map_update_elem(&cgroup_id_to_last_event_ts, &cgroup_id,
&now, BPF_ANY);

}

return 0;
}

يقوم تطبيق مساحة المستخدم الخاص بنا، والذي تم تطويره في Go، بمعالجة الأحداث من المخزن المؤقت الحلقي لإصدار المقاييس إلى الواجهة الخلفية للمقاييس الخاصة بنا، Atlas. يتضمن كل حدث نموذجًا لزمن استجابة قائمة انتظار التشغيل بمعرف مجموعة cgroup، والذي نربطه بتشغيل الحاويات على المضيف. نقوم بتصنيفها على أنها خدمة نظام إذا لم يتم العثور على مثل هذا الارتباط. عندما يرتبط معرف مجموعة cgroup بحاوية، فإننا نصدر مقياس أطلس للمؤقت المئوي (runq.latency) لتلك الحاوية. نقوم أيضًا بزيادة مقياس العداد (sched.switch.out) لمراقبة الإجراءات الوقائية التي تحدث لعمليات الحاوية. يتيح لنا الوصول إلى prev_cgroup_id الخاص بالعملية المُسبقة وضع علامة على المقياس مع سبب الشفعة، سواء كان ذلك بسبب عملية داخل نفس الحاوية (أو مجموعة cgroup)، أو عملية في حاوية أخرى، أو خدمة نظام.

من المهم تسليط الضوء على أن كلاً من runq.latency متري و sched.switch.out هناك حاجة إلى مقاييس لتحديد ما إذا كانت الحاوية تتأثر بالجيران المزعجين، وهو الهدف الذي نهدف إلى تحقيقه – فالاعتماد فقط على مقياس runq.latency يمكن أن يؤدي إلى مفاهيم خاطئة. على سبيل المثال، إذا كانت الحاوية عند حد وحدة المعالجة المركزية cgroup أو أكثر منه، فسيقوم المجدول باختناقها، مما يؤدي إلى ارتفاع واضح في زمن انتقال قائمة انتظار التشغيل بسبب التأخير في قائمة الانتظار. إذا أخذنا هذا المقياس في الاعتبار فقط، فقد نعزو بشكل غير صحيح انخفاض الأداء إلى الجيران المزعجين عندما يكون السبب في الواقع هو أن الحاوية تصل إلى حدود طلب وحدة المعالجة المركزية الخاصة بها. ومع ذلك، فإن الارتفاعات المتزامنة في كلا المقياسين، بشكل رئيسي عندما يكون السبب هو عملية حاوية أو نظام مختلفة، تشير بوضوح إلى مشكلة مجاورة مزعجة.

أدناه هو runq.latency مقياس لخادم يقوم بتشغيل حاوية واحدة مع حمل كبير لوحدة المعالجة المركزية. يبلغ متوسط ​​النسبة المئوية 99 83.4 ميكروثانية، وهو بمثابة خط الأساس لدينا. على الرغم من وجود بعض الارتفاعات التي تصل إلى 400 ميكرو ثانية، إلا أن زمن الوصول يظل ضمن المعلمات المقبولة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى