تحليل أداء التطبيقات (Profiling)
تحليل أداء التطبيقات (Profiling)
تُخبرك اختبارات الحمل بأن خدمتك بطيئة، أما تحليل الأداء (Profiling) فيُخبرك بالسبب. بدون هذا التحليل تصبح عملية التحسين مجرد تخمين: يقضي المهندسون أياماً في ضبط connection pools أو إضافة ذاكرة تخزين مؤقت، بينما دالة واحدة ساخنة في طبقة التطبيق تستهلك 60 % من كل دورة CPU. يهدف هذا الدرس إلى إيجاد تلك المسارات الساخنة بمنهجية علمية، على مستوى استدعاءات الدوال والتخصيص في الذاكرة، بنفس الانضباط الذي تطبّقه على تحليل معدل احتراق SLO.
المحوران: المعالج والذاكرة
كل تراجع في الأداء يقع على أحد محورين أو كليهما:
- تحليل أداء المعالج (CPU Profiling) — يقيس أين يُقضى الوقت: أي الدوال تعمل على المعالج وكم من الوقت. يُلتقط بأخذ عينات من call stack بفاصل ثابت (مثلاً 100 Hz) أو بتطعيم كل دخول/خروج للدالة.
- تحليل أداء الذاكرة (Memory Profiling) — يقيس أين تتم عمليات التخصيص: أي مسار استدعاء أطلق
mallocأو ضغط على جامع القمامة. مفيد حين يكون العرض OOMKill أو تأخر GC مرتفعاً أو نمواً مستمراً في RSS (تسرب ذاكرة).
في بيئة الحاويات المكدّسة ستلجأ في الغالب إلى تحليل المعالج أولاً، لأن إشباع CPU هو السبب الرئيسي لتراجع الأداء في الخدمات المعتمدة على الحساب. يأتي تحليل الذاكرة لاحقاً حين تكشف مقاييس RSS أو GC من مكدس Prometheus الخاص بك عن شذوذ.
أخذ العينات مقابل التطعيم
محللات أداء أخذ العينات (perf، pprof في Go، async-profiler للـ JVM) تقاطع التنفيذ دورياً وتسجّل call stack الحالي. الأعباء الإضافية منخفضة (1–3 % من CPU) وآمنة للاستخدام المؤقت في الإنتاج. محللات الأداء بالتطعيم (JaCoCo، Python cProfile، Pyroscope بوضع eBPF) تُغلّف كل استدعاء دالة مما يعطي أعداداً دقيقة لكنه يضيف 10–40 % أعباء إضافية — احتفظ بها للبيئات التدريبية أو نوافذ الكناري الخاضعة للسيطرة.
مخططات اللهب: قراءة المسار الساخن
مخطط اللهب (Flame Graph، من اختراع Brendan Gregg) يضغط آلاف عينات المكدس في صورة واحدة. المحور الأفقي يمثّل عدد العينات (العرض = حصة الوقت)، والمحور الرأسي يمثّل عمق الاستدعاء (الأسفل = نقطة الدخول، الأعلى = الأوراق). الإطارات الأوسع في قمة البرج هي المسار الساخن — الكود الذي يستهلك أكبر وقت CPU دون استدعاء أي شيء آخر.
الرؤية الجوهرية: لا تُحسّن الإطارات العريضة في الأسفل (تلك هي موزّع الطلبات في إطارك البرمجي ولا يمكنك تغييرها). تُحسّن الإطارات العريضة في قمة أطول الأبراج، لأن تلك هي الدوال الورقية التي ينفّذها المعالج فعلياً.
Go: pprof في الإنتاج
تشحن Go مكتبة net/http/pprof مع المكتبة القياسية. استوردها وأظهر نقطة النهاية debug (خلف ingress داخلي أو network policy، وليس أبداً على المنفذ العام):
التقط ملف تعريف CPU لمدة 30 ثانية وأنشئ مخطط لهب محلياً:
JVM: async-profiler
يتجنّب async-profiler تحيّز safepoint في محللات أداء JVMTI (JProfiler، YourKit) باستخدام Linux perf_events أو AsyncGetCallTrace لأخذ عينات من الخيوط بغضّ النظر عن حالة safepoint في الـ JVM. هذه الأداة الصحيحة لتحليل الأداء في إنتاج JVM:
Linux: perf مع FlameGraph لـ Brendan Gregg
للعمليات الأصيلة (C، C++، Rust) أو مسارات kernel-space الساخنة (استدعاءات النظام، برامج eBPF)، يبقى perf المرجع الأساسي:
-O2 والثنائيات المُجرّدة تُنتج إطارات مُسمّاة [unknown]. الحلول: (1) قم بالترجمة مع -fno-omit-frame-pointer (تفعل Go هذا افتراضياً منذ 1.12؛ أضف -XX:+PreserveFramePointer للـ JVM)؛ (2) ثبّت حزم debuginfo؛ (3) استخدم محللات أداء مبنية على eBPF (Parca، Pyroscope بوضع eBPF) التي تحلّ الرموز من DWARF في sidecar دون لمس ثنائي التطبيق.
تحليل الذاكرة: إيجاد التسربات وضغط GC
حين يرتفع process_resident_memory_bytes في Prometheus بنسبة 2 % كل ساعة أو ترتفع jvm_gc_pause_seconds تحت الحمل، الجأ إلى تحليل التخصيص:
- Go heap profile:
curl localhost:6060/debug/pprof/heap > heap.pb.gzثمgo tool pprof -http=:9090 heap.pb.gz. تبديل بين "alloc_space" (إجمالي التخصيصات منذ البداية) و"inuse_space" (الكومة الحية الحالية) — inuse_space يكشف التسربات الحية، وalloc_space يكشف ضغط التخصيص المسبّب لارتفاع GC. - JVM:
jcmd <pid> VM.native_memory summaryللذاكرة خارج الكومة؛ async-profiler مع-e allocلمخططات لهب التخصيص. تغذية مفرغات الكومة (jmap -dump:live) إلى Eclipse MAT للكشف عن التسربات. - Python:
memray(من Bloomberg) يطعّم التخصيصات على مستوى C —memray run --live myservice.pyأوmemray attach <pid>لعملية قيد التشغيل.
التحليل في CI: بوابات الأداء
دمج تحليل الأداء في خط CI الخاص بك يمنع الانتكاسات من الوصول إلى الإنتاج. خطوة نموذجية في GitHub Actions بعد مهمة اختبار الحمل:
طبقة الحكم الهندسي الأعلى
بيانات تحليل الأداء لا قيمة لها إلا بقدر الإجراء الذي تقود إليه. على مستوى كبار/موظفي الهندسة، الانضباط لا يقتصر على قراءة مخطط اللهب، بل على ترجمته إلى تغيير بتأثير مُتوقَّع ومقيس:
- الخط الأساسي أولاً. سجّل مخطط اللهب قبل أي تغيير. بدون خط أساسي لا يمكنك إثبات أن الإصلاح حسّن الإنتاجية فعلاً.
- متغيّر واحد في كل مرة. حسّن إطاراً ساخناً واحداً، أعد اختبار الحمل، أعد التحليل. الجمع بين تغييرات متعددة يجعل تحديد سبب الانتكاسة مستحيلاً.
- احذر التحسين المبكر الدقيق. دالة تستهلك 3 % من CPU وتستغرق ساعتين لإصلاحها صفقة سيئة. ركّز على الإطارات التي تتجاوز 10–15 % أولاً.
- الكمون مقابل الإنتاجية. التحسين على المعالج الذي يُنصّف استخدام CPU قد لا يُنصّف تأخر p99 إذا كان الاختناق في استدعاء خارجي — تحقق من آثار التتبع الموزّع من Jaeger/Tempo جانباً مع مخطط اللهب.