الكبح الإجباري للمعالج (CPU Throttling): ليه تطبيقك بطيء رغم إن العقدة فاضية
مستوى المقال: محترف. ده شرح لمشكلة إنتاج بتضرب فرق كتير على Kubernetes، وبيفترض إنك متعوّد على البودات والحدود (requests / limits) وتقدر توصل للـ shell جوّه الحاوية.
لو الـ p99 latency بتاع خدمتك بيقفز لثانيتين، وانت شايف إن استهلاك الـ CPU على العقدة تحت 40%، ركّز: غالبًا مش محتاج عقدة أقوى ولا ريبليكات زيادة. اللي بيحصل فعلاً إن الـ kernel بيجمّد بودك بسبب حاجة اسمها CFS quota. المقال ده هيوريك تكتشفها بأمر واحد وتصلّحها.
المشكلة باختصار
بتحطّ limits.cpu على البود عشان تحمي العقدة. المفاجأة إن الحد ده مش سقف بيشتغل "لو زاد الاستهلاك"، ده سقف بيتحاسب كل 100 مللي ثانية على حدة. لو تطبيقك خلّص حصته بدري في الفترة، الـ kernel بيوقفه تمامًا لحد ما الفترة اللي بعدها تبدأ. النتيجة: latency عالي ومتوسط CPU منخفض في نفس الوقت، وده بالظبط اللي بيخلّي الناس تشك في السيرفر بدل ما تشك في الحد اللي هي حاطّاه بإيدها.
المفهوم: CFS quota بمثال بسيط ثم علميًا
تخيّل خط موبايل بباقة "40 دقيقة كل ساعة". لو اتكلمت الـ 40 دقيقة كلها في أول 15 دقيقة، الخط بيتقفل فورًا وتفضل مستني 45 دقيقة لحد ما الساعة الجديدة تبدأ. انت استهلكت 40 دقيقة بس في ساعة كاملة، فالمتوسط بتاعك يبان قليل، لكنك عمليًا كنت "متقفول" معظم الوقت. ده بالظبط اللي بيعمله الـ CPU throttling مع بودك.
علميًا: الـ CFS (Completely Fair Scheduler) في لينكس بيطبّق حد الـ CPU عن طريق قيمتين في الـ cgroup: cpu.cfs_period_us (افتراضيًا 100000، يعني 100 مللي ثانية) و cpu.cfs_quota_us. لمّا تكتب limits.cpu: "1"، ده بيتحوّل لـ quota = 100ms لكل فترة 100ms. و limits.cpu: "500m" يعني quota = 50ms. النقطة المهمة إن الحصّة دي بتتقسم على كل خيوط التطبيق مجمّعة. لو عندك 8 خيوط شغّالة بالتوازي وحدّك "1"، الـ 8 خيوط بيستهلكوا الـ 100ms في حوالي 12ms من الزمن الحقيقي، وبعدها البود يتجمّد 88ms. ده اللي بيفجّر الـ tail latency.
إزاي تتأكد إن ده اللي بيحصل فعلاً
متخمّنش. الرقم اللي بيحسم الموضوع موجود جوّه الحاوية في cpu.stat.
# cgroup v2 (الأحدث)
kubectl exec -it <pod> -- cat /sys/fs/cgroup/cpu.stat
# بتطلّع: nr_periods, nr_throttled, throttled_usec
# cgroup v1
kubectl exec -it <pod> -- cat /sys/fs/cgroup/cpu,cpuacct/cpu.stat
# بتطلّع: nr_periods, nr_throttled, throttled_timeاحسب نسبة الكبح: nr_throttled / nr_periods. لو النسبة فوق 25%، عندك مشكلة حقيقية بتدفع تكلفتها latency. على مستوى الكلاستر كله استخدم Prometheus بدل ما تدخل كل بود بإيدك:
rate(container_cpu_cfs_throttled_periods_total[5m])
/
rate(container_cpu_cfs_periods_total[5m])سيناريو واقعي بأرقام
خدمة Node.js بتعمل image processing، محطوط عليها limits.cpu: "1"، وبتشغّل thread pool بـ 8 خيوط على عقدة 16 vCPU. تحت حمل 900 طلب/ثانية: الـ p99 كان 2100ms، ومتوسط استهلاك الـ CPU المعروض 55%، ونسبة الكبح من cpu.stat كانت 63%. رفعنا الحد لـ "4" فبقت نسبة الكبح 6% والـ p99 نزل لـ 240ms — من غير ما نلمس الكود ولا نكبّر العقدة. الافتراض هنا إن العقدة فيها فسحة CPU فعلاً؛ لو العقدة نفسها مزنوقة، رفع الحد بيأجّل المشكلة مش بيحلّها.
الحلول والـ trade-offs
- ظبط الحد على درجة التوازي الحقيقية. لو تطبيقك بيشغّل 4 خيوط فعّالة، حدّ "1" غلط من الأساس. خلّي الحد ≥ عدد الخيوط النشطة. المكسب: الكبح بيختفي. الخسارة: بودات أقل على كل عقدة.
- قلّل توازي التطبيق نفسه بدل ما تكبّر الحد. في Go اضبط
GOMAXPROCSليساوي حدّ الـ CPU (استخدم مكتبةautomaxprocs). في الـ JVM اضبط-XX:ActiveProcessorCount. المكسب: التطبيق بيحترم حدّه من جوّه فيقلّل التنافس. الخسارة: أقصى throughput لكل بود بيقل. - شيل حدّ الـ CPU خالص واعتمد على requests بس. ده بيلغي الكبح تمامًا. الـ trade-off هنا حقيقي وصريح: بتكسب latency، بتخسر العزل — بود واحد ممكن ياكل CPU الجيران وقت الذروة (noisy neighbor). ينفع لو بتثق في الـ requests وعندك مراقبة كويسة على العقدة.
تحذير على مستوى الـ kernel
لو الكلاستر شغّال kernel أقدم من 5.4، فيه باج معروف في CFS كان بيكبح أكتر من اللازم حتى لو استهلاكك تحت الحد. الإصلاح دخل في نواة 5.4، وميزة cpu.cfs_burst_us اللي بتسمح باستهلاك انفجاري محدود دخلت في 5.14. الافتراض إن معظم شروحات "شيل الـ limits" القديمة اتكتبت في زمن الباج ده؛ على kernel حديث الموضوع أهدى بكتير، فاقيس على نُواتك انت قبل ما تاخد قرار جذري.
متى لا تستخدم هذه الطريقة
لو نسبة الكبح عندك تحت 5%، سيبها؛ مش كل throttling مشكلة تستاهل وقتك. ولو شغّال على منصّة مشتركة (multi-tenant) والعزل عندك أهم من آخر 100 مللي ثانية latency، متشيلش الـ limits — الكبح هنا هو الميزة مش العيب. وكمان لو حِملك CPU-bound فعلاً وثابت (مش انفجاري)، رفع الحد مش هيساعد كتير؛ محتاج تحسّن الكود نفسه أو تكبّر أفقيًا.
الخطوة التالية
افتح أكتر خدمة بتشتكي من الـ latency، ونفّذ cat /sys/fs/cgroup/cpu.stat جوّه بودها واحسب nr_throttled / nr_periods. لو طلعت فوق 25%، جرّب ترفع الحد أو تظبط GOMAXPROCS وقيس الـ p99 قبل وبعد. الرقم ده لوحده بيحسم إذا كنت بتدفع فاتورة كبح إجباري من غير ما تدري.
المصادر
- Kubernetes Docs — Managing Resources for Containers (CPU limits): https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- Linux Kernel — CFS Bandwidth Control (sched-bwc): https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html
- Kubernetes Issue #67577 — CFS quotas can lead to unnecessary throttling: https://github.com/kubernetes/kubernetes/issues/67577
- Linux commit de53fd7aedb1 — sched/fair: fix low CPU usage with high throttling (نواة 5.4): https://github.com/torvalds/linux/commit/de53fd7aedb1
- Linux Kernel — cgroup v2 CPU controller (cpu.stat): https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html