دليل هندسة أداء لينكس: من تشخيص الاختناقات حتى أعماق النواة عبر eBPF

دليل هندسة أداء لينكس: من تشخيص الاختناقات حتى أعماق النواة عبر eBPF

عندما تتدهور استجابة خادم الإنتاج فجأة، يقع أغلب المهندسين في فخ تشغيل أوامر عشوائية مثل top أو إعادة تشغيل الخدمات أملاً في زوال المشكلة. تحسين الأداء (Performance Tuning) ليس وصفة جاهزة تُنسخ من الإنترنت، بل هو تخصص هندسي يبدأ بتحديد العنق الفعلي للزجاجة اعتماداً على قياسات دقيقة داخل النواة (Kernel)، مروراً بفهم سلوك العتاد، وصولاً إلى التدخل الجراحي المدروس دون تعطيل استقرار النظام.

منهجيات التشخيص الهندسي: الفرق بين التخمين والتحليل المنظم

البدء بالأدوات قبل المنهجية هو السبب الأول لفشل جلسات استكشاف الأخطاء وإصلاحها (Troubleshooting). يحتاج مهندس الأنظمة إلى إطار عمل يوجه تفكيره خطوة بخطوة لاستبعاد الفرضيات وتقليص مساحة البحث.

منهجية USE لتحليل موارد العتاد

ابتكر هذه المنهجية المهندس Brendan Gregg، وتُطبق على كل مورد مادي في الخادم (معالج، ذاكرة، وحدات تخزين، واجهات شبكة). تقوم المنهجية على فحص ثلاثة مقاييس لكل مورد:

  • الاستخدام (Utilization): النسبة المئوية للوقت الذي كان فيه المورد مشغولاً بخدمة الطلبات.
  • التشبع (Saturation): حجم الطلبات الإضافية المصطفة في طابور الانتظار لعدم قدرة المورد على خدمتها فوراً. التشبع هو المؤشر الحقيقي لوجود اختناق، حتى لو لم يصل الاستخدام إلى 100%.
  • الأخطاء (Errors): عدد الحالات الشاذة التي واجهها المورد (مثل حزم الشبكة المسقطة أو أخطاء قراءة الكتل التالفة).

منهجية RED للخدمات البرمجية

بينما تركز منهجية USE على العتاد من الأسفل إلى الأعلى، تركز منهجية RED على الخدمات وتطبيقات الويب الموجهة للمستخدمين من منظور المعاملات البرمجية:

  • معدل الطلبات (Rate): عدد الطلبات التي يستقبلها التطبيق في الثانية (RPS).
  • الأخطاء (Errors): عدد الطلبات الفاشلة (مثل استجابات HTTP 5xx).
  • المدة الزمنية (Duration): الزمن المستغرق لإتمام كل طلب، ويُقاس عبر المئينين 95 و 99 (p95 / p99) وليس بالمتوسط الحسابي المضلل.

فك شفرة المعالج والذاكرة: ما وراء قراءات top التقليدية

تعتمد النواة الحديثة على آليات دقيقة لتوزيع الحوسبة لا تكشفها الأوامر القديمة بصورة كاملة. مع الانتقال إلى مجدول EEVDF (Earliest Eligible Virtual Deadline First) ابتداءً من نواة Linux 6.6 بدلاً من مجدول CFS القديم، أصبح التنافس على زمن المعالج يُدار وفق مواعيد نهائية افتراضية صارمة تتطلب فهماً عميقاً لسلوك الخيوط البرمجية (Threads).

تحليل زمن التنفيذ (On-CPU) وزمن الحظر (Off-CPU)

عندما تستهلك الخدمة 100% من قدرة المعالج، فإنها تكون في حالة On-CPU، ويمكن هنا التقاط العينات البرمجية عبر perf وتوليد الرسوم البيانية اللهبية (Flame Graphs) لمعرفة الدوال التابعة للأكواد البرمجية التي تستهلك دورات المعالج. لكن المشكلة الأخطر تحدث عندما يكون الخادم بطيئاً ومع ذلك استهلاك المعالج منخفض؛ هنا يكمن اختناق Off-CPU، حيث تقضي الخيوط البرمجية وقتها في انتظار قفل (Mutex Lock)، أو عملية إدخال وإخراج للقرص، أو صفحة ذاكرة جرى ترحيلها، وهو ما لا يُكتشف إلا بالتتبع الديناميكي للحدث sched:sched_switch.

سهّل التوجه الحديث في توزيعات مثل Fedora و Ubuntu تفعيل مؤشرات الإطارات (-fno-omit-frame-pointer) افتراضياً، مما خفض كلفة تفريغ مكدس الاستدعاءات (Stack Unwinding) لتصبح أقل من 1% في بيئات الإنتاج، مما أتاح المراقبة اللحظية الدائمة.

مؤشرات ضغط النواة (PSI): البديل المعاصر لمعدل الحمل

يعد مقياس Load Average الكلاسيكي مضللاً لأنه يجمع العمليات التي تعمل فعلياً مع العمليات العالقة في حالة انتظار غير قابلة للمقاطعة (Uninterruptible Sleep أو الحالة D في ناتج top)، دون إيضاح المورد المسبب. حلت النواة الحديثة هذه المعضلة بإدراج نظام Pressure Stall Information (PSI) عبر المسار /proc/pressure/.

يقسم PSI الضغط إلى مستويين:

  • some: يعكس نسبة الوقت الذي تعطلت فيه بعض المهام نتيجة نقص المورد بينما استمرت مهام أخرى في العمل.
  • full: يمثل الكارثة الفعلية، حيث توقفت جميع المهام التنفيذية في النظام بنسبة 100% بسبب انتظار هذا المورد تحديداً (مثل انشغال المعالج بالكامل في تدوير صفحات الذاكرة Thrashing).

تشريح إدارة الذاكرة وظاهرة OOM Killer

تستخدم نواة لينكس الذاكرة غير المستخدمة كـ Page Cache و Buffer لتسريع الوصول إلى الملفات المخزنة. تتألف الذاكرة النشطة من صفحات مجهولة (Anonymous Memory مثل مكدس الذاكرة للتطبيقات Heap/Stack) وصفحات مرتبطة بملفات (File-backed Memory).

يتحكم المعامل vm.swappiness في نسبة رغبة النواة في تفريغ الصفحات المجهولة إلى مساحة التبديل (Swap) مقابل استرجاع الذاكرة المخصصة للملفات. عند استنفاد كامل الذاكرة وعجز النواة عن توفير صفحات جديدة، يتدخل معالج النقص الحاد للذاكرة (Out-Of-Memory Killer) لحساب نقاط التهديف (badness score) لكل عملية وإنهاء أضخمها قسرياً لحماية سلامة النواة.

اختناقات وحدات التخزين وجدولة الإدخال والإخراج (Block I/O)

الخطأ الأكبر في قياس سرعة أقراص التخزين هو الاعتماد فقط على حجم البيانات المنقولة في الثانية (Throughput مثل MB/s). ما يحسم أداء قواعد البيانات والخوادم الموزعة هو زمن الاستجابة (I/O Latency) وعدد العمليات المنفذة في الثانية (IOPS).

عندما تتعامل مع وسائط تخزين فائقة السرعة مثل أقراص NVMe، تصبح مجدولات الإدخال القديمة عبئاً على النواة. توفر طبقة الجدولة متعددة الطوابير (blk-mq) عدة مجدولات:

  • none: الخيار الأمثل لأقراص NVMe الحديثة، حيث يُمرر الطلب مباشرة إلى طوابير العتاد دون إهدار دورات المعالج في إعادة الترتيب.
  • mq-deadline: الخيار المتوازن لأقراص SATA SSD، ويضمن منع تجويع أي طلب قراءة أو كتابة بفرض مهلة قصوى.
  • bfq: مجدول يركز على العدالة التامة، ويناسب وحدات التخزين الميكانيكية التقليدية (HDD) أو أجهزة سطح المكتب، لكنه غير مناسب إطلاقاً لخوادم قواعد البيانات الضخمة.

سكربت الفحص السريع للأداء (System Triage)

يوفر السكربت التالي فحصاً شاملاً لحالة النظام في غضون ثوانٍ معدودة بالاعتماد على مؤشرات النواة ومقاييس الضغط الحديثة لتحديد المورد المختنق قبل اتخاذ أي قرار:

#!/usr/bin/env bash
# Quick system health triage script based on modern kernel metrics
set -euo pipefail

echo "=== 1. Checking Kernel Pressure Stall Information (PSI) ==="
for res in cpu memory io; do
    if [ -f "/proc/pressure/$res" ]; then
        echo "--- Pressure on $res ---"
        cat "/proc/pressure/$res"
    else
        echo "PSI is not supported for $res on this kernel."
    fi
done

echo ""
echo "=== 2. CPU Run Queue and Blocking Processes (vmstat) ==="
# Field 'r' shows runnable threads, 'b' shows threads in uninterruptible sleep
vmstat 1 3

echo ""
echo "=== 3. Memory Allocation and OOM Killer Activations ==="
free -h
echo "--- Recent OOM killer invocations: ---"
dmesg -T | grep -i -E 'oom[-_]killer|out of memory' | tail -n 5 || echo "No recent OOM events found."

echo ""
echo "=== 4. Disk Latency and Saturation (iostat) ==="
# Check disk await times and percentage of utilization
if command -v iostat > /dev/null 2>&1; then
    iostat -xz 1 2
else
    echo "sysstat package (iostat) is not installed."
fi

echo ""
echo "=== 5. Top Contenders for CPU and Memory ==="
ps -eo pid,ppid,user,%cpu,%mem,stat,comm --sort=-%cpu | head -n 6

يقوم السكربت بالخطوات التحليلية التالية:

  • القسم الأول يستخرج نسب التعطل اللحظية من واجهة PSI لمعرفة إن كان المعالج أو الذاكرة أو التخزين يتعرض لضغط حرج خلال الدقائق الأخيرة.
  • القسم الثاني يشغل vmstat؛ راقب العمود r (العمليات الجاهزة للتنفيذ وتنتظر دورها في المعالج) والعمود b (العمليات المعلقة بانتظار الإدخال والإخراج).
  • القسم الثالث يحلل الذاكرة الحقيقية مع قراءة السجلات الحية لـ dmesg للتأكد من عدم قيام النواة بتصفية عمليات حيوية بصمت.
  • القسم الرابع يراقب زمن الاستجابة await عبر iostat؛ ارتفاعه لأكثر من بضعة أجزاء من الألف من الثانية مع وصول %util إلى 100% يعني أن طابور وحدة التخزين قد تشبع بالكامل.
  • القسم الأخير يفرز العمليات المستهلكة للموارد ويوضح حالتها البرمجية عبر حقل stat.

التتبع اللحظي لمشاكل التخزين عبر تقنية eBPF وbpftrace

عندما تلاحظ بطء قراءة الملفات في قاعدة بيانات، فإن الأوامر العادية تخبرك فقط بمعدل القراءة، لكنها تعجز عن كشف هل البطء صادر من طبقة نظام الملفات (VFS) أم من المحرك المادي للقرص. هنا تتجلى قوة تقنية eBPF التي تتيح تشغيل كود آمن ومضغوط داخل النواة مباشرة لحساب الفوارق الزمنية دون الحاجة إلى أدوات تزيد العبء مثل strace.

# Run with root privileges to trace VFS read latency distribution
sudo bpftrace -e '
kprobe:vfs_read {
    @start[tid] = nsecs;
}

kretprobe:vfs_read /@start[tid]/ {
    $duration_us = (nsecs - @start[tid]) / 1000;
    @vfs_latency_us = hist($duration_us);
    delete(@start[tid]);
}

END {
    clear(@start);
}
'

تفصيل عمل البرنامج السطر بالسطر:

  • kprobe:vfs_read: يعترض نقطة الدخول لدالة النواة المسؤولة عن قراءة الملفات في طبقة التخزين الافتراضية، ويسجل الطابع الزمني بالنانو ثانية داخل خريطة مفهرسة بمعرف الخيط البرمجي tid.
  • kretprobe:vfs_read: يعترض لحظة خروج الدالة بعد إتمام القراءة، ويحسب الفارق الزمني ويحوله إلى ميكروثانية.
  • hist($duration_us): يجمع الأزمنة في رسم بياني لوغاريتمي (Histogram) يوضح بدقة متناهية توزيع زمن العمليات، مما يكشف الحالات الشاذة (Outliers) التي قد تستغرق ثواني معدودة وتتسبب في تجميد تطبيقك.
  • delete(@start[tid]): يفرغ مساحة الذاكرة المخصصة للخيط لتفادي تسريب الذاكرة داخل مصفوفات النواة.

الأخطاء الشائعة في تشخيص الأداء وإعدادات النواة

  • الذعر من انخفاض مؤشر Free Memory: يفترض المهندس المبتدئ أن انخفاض القيمة في عمود free يعني نفاد الذاكرة. في أنظمة لينكس، الذاكرة غير المستخدمة هي ذاكرة مهدرة، لذا تستغلها النواة كـ buff/cache. المؤشر الحقيقي لقدرة النظام على تشغيل برمجيات جديدة هو عمود available وليس free.
  • تشغيل أداة strace على خدمات حية في الإنتاج: يوقف أمر strace العملية المستهدفة ويفوض معالجتها للنواة عند كل استدعاء نظامي (System Call)، مما قد يرفع زمن استجابة الخدمة بنسبة تتراوح بين 10 إلى 50 ضعفاً، ويؤدي مباشرة إلى انهيار قواعد البيانات تحت الضغط. الحل البديل هو استخدام أدوات مبنية على eBPF مثل bpftrace أو perf ذات الأثر الهامشي (Negligible Overhead).
  • النسخ الأعمى لمعاملات sysctl: تداول إعدادات جاهزة عبر المنتديات مثل رفع vm.dirty_ratio أو تضخيم أحجام net.core.rmem_max دون دراسة حجم الذاكرة المادية وطبيعة الأحمال يؤدي إلى تجميد النظام أثناء عمليات الكتابة المكثفة للقرص (I/O Flush Spikes) أو حدوث أخطاء فادحة في بروتوكول TCP.
  • التوسعة العشوائية للخادم (Blind Scale-Up): مضاعفة عدد أنوية المعالج وحجم الذاكرة لحل مشكلة بطء استجابة التطبيق، في حين أن المشكلة الحقيقية قد تكون اختناقاً في قفل جدول قاعدة بيانات (Database Lock Contention) أو بطء الإدخال والإخراج للقرص؛ في هذه الحالة لن تؤدي زيادة المعالجات إلا إلى زيادة كلفة الفاتورة السحابية دون أي تحسن في الأداء.

متى تعتمد أدوات التتبع المتقدمة ومتى تكتفي بالمؤشرات القياسية؟

لا تتطلب كل مشكلة أداء تشغيل برامج eBPF وتوليد Flame Graphs؛ إذا كانت المشكلة واضحة من خلال قراءات free -h أو سجلات dmesg التي تفيد بتدخل OOM Killer، أو تظهر في top كعملية تستهلك المعالج بنسبة 100% نتيجة حلقة تكرار لانهائية، فإن الأدوات القياسية كافية تماماً وتوفر وقت المهندس. في المقابل، يصبح الانتقال الحتمي إلى أدوات التتبع العميق (Dynamic Tracing)، ومؤشرات PSI، وتحليل اختناقات Off-CPU إلزامياً عندما تواجه مشكلات بطء عشوائية، أو زيادة حادة في أزمنة استجابة المئينين p95/p99 للخدمات دون وجود استهلاك ملحوظ لموارد العتاد السطحية، أو عند تشخيص تداخل الحاويات داخل بيئات Kubernetes متعددة المستأجرين.