المقدمة

ناس كتير بتستخدم Kubernetes مش مهم هل لحاجه ضرورية ولا لا .. ولكن انا بعتبره حاجة كدا مما عم به البلاء أو الموضة لكن بما اننا بنستخدمه والناس كلها بتستخدمه فمحتاجين نفهم شويه تفاصيل.

المقال ده هيكون طويل شوية لذكر تفاصيل داخلية وهيكون في الآخر الخلاصة الي أنت محتاج تعرفها لو مش عايز تتوه في التفاصيل.

المهم إن Kubernetes فيه جزء الناس أغلبها ملمين بيه ألا وهو الـ CRUD الي بتعمله وانت بتعمل Apply لشوية specs مكتوبة بـ Yaml format ودا فقط بيروح يعمل CRUD في etcd db ودا الـ db engine الي بيستخدمه Kubernetes.

التفاصيل دي بتاخدها components داخليه بتشتغل عليها منها الـ high level ومنها الي بيتعامل مع الـ Linux kernel لكن في المقال دا عايز اتكلم عن جزء الـ Resources Request and Limits وهتكلم تحديدًا عن الـ CPU. وفي مقال تاني ممكن نتكلم عن الـ Memory.


CPU Requests & Limits

هل سألت نفسك قبل كدا لما بتحط CPU limits او memory limits او CPU requests او memory requests الأرقام دي الـ Kubernetes بيعمل بيها ايه او الـ Linux kernel بيتعامل معاها ازاي؟

والإجابة هي إن Kubernetes بيستخدم حاجة اسمها Cgroup عشان يدير الـ resources زي الـ memory والـ CPU لكل container قايمة علي الـ Node من خلال Kubernetes. والمسؤول عن الجزء دا هو الـ Kubelet والي تقدر تعتبره المخ بتاع Kubernetes.

المهم إن Kubernetes بيقسم أو بيوزع الـ Pods في ثلاثة تصنيفات بناءًا علي الإعدادات بتاعة الـ memory والـ cpu الي انت بتحطها في الـ Specs بتاعة الـ Pod وبناءًا عليه ايضًا وجود الـ workload او الـ pod بتاعك في أي من التصنيفات دي بتتغير طريقة التعامل معاها وبقاءها في حالة الـ node pressure او التضحية بيها ودا بيُسمى Quality of Service او QOS اختصارًا والي ممكن تشوفه لما تعرض الـ Specs لـ Pod موجوده عن طريق

 kubectl get pod pod-name -o yaml 

هتلاقي فيه managed field اسمه qos.

طيب ايه التقسيمات دي وتأثيرها ايه علي دورة حياة الـ workload بتاعتك وعلي الـ Out of memory issue ؟ دي كلها حاجات مهم نكون ملمين بيها ولكن أنا في المقال ده هتكلم فقط او أكثر علي الجانب الخاص بالـ CPU.


تصنيفات الـ QOS

Guaranteed

ودي شروطها إن :

  • الـ CPU requests = الـ CPU limits
  • والـ Memory requests = الـ Memory limits

يعني يكونوا قد بعض .. طيب دا بيترتب عليه ايه؟

بيترتب عليه أداء أفضل وأولوية بمعني إن في حالة الضغط أو ال node pressure والحاجة للتضحية ببعض ال workloads البروفايل دا بيكون آخر حاجة بيتم التضحية بيها وبتاخد أداء مضمون.

ولكن لل CPU limits عيوب هنتكلم عنها لاحقًا ودا عادة اختيار لما تكون عايز predictable CPU availability زي أنواع من ال workloads الي بتعتمد في أنماط التصميم وتحسين الأداء علي حساب ال CPU المتاح زي ال work schedulers وال concurrency rate limit والي غالبا لا ينطبق علي أغلب ال web apps. وده لإن عادة بتتسخدم الأنماط دي في ال db engines وال Message queues وغيرها.


Burstable

ودي شروطها إن:

  • ال Resources Request تكون أقل من ال Resources Limits

ودا معناه إن هتاخد الي طلبته ولكن هتقدر تستخدم resources أكتر لو احتجت أكتر من اللي المطلوب بشرط إن يكون متاح دا اصلًا علي ال node وهنسرد دا في تفاصيل لاحقة ازاي دا بيتوزع في كل حالة والفرق بين ال request وال limits.

طبعًا البروفايل دا عُرضة لتأثير ظروف ال node في استهلاك resources اكتر من المطلوب ، وكذلك أقل أولوية للبقاء من البروفايل الأول اللي هو الـ Guaranteed ، لكن انك متحطش cpu limits مفيد ويوصي به وهقول دا كمان شوية.


Best Efforts

ودا لو محطتش أي limits ولا requests .. في الحالة دي ال Pod بتاعتك هتستخدم المتاح أو الي هيتحط لها تاكله أو تموت من الجوع وأول حاجة بيتم بيها التضحية في حالة الضغط وكمان عرضه للـ throttling.


الفرق بين الـ CPU Request والـ CPU Limits

طيب احنا محتاجين نفهم ايه الفرق بين ال CPU request وال CPU limits غير الفرق الواضح من المسميات الي هو اختصارا ال request دا الي طلبته وال limits دا اخري.

الـ CPU بيتحسب بوحدة اسمها cpu كل 1 CPU معناها يا إمّا core حقيقي لو السيرفر physical، يا إمّا core افتراضي لو شغال على VM. وطبعًا ينفع تطلب جزء من الـ core، يعني لو حاطط 0.5 أو 500m يبقى إنت طالب نص كور.

و100m يعني 0.1 CPU (مائة millicpu) المهم إن الـ CPU دا رقم ثابت مش نسبة، يعني 500m هيفضل نفس القدرة الحسابية سواء الـ container شغال على جهاز فيه core واحد أو 48 cores.

الكلام دا تأصيل فقط للي جاي بعد كدا ولكن في النهاية ال container الي هي شايلة ال application بتاعك دي عبارة عن process قايمة علي linux ومش Kubernetes الي بيـ manage ولا بيتكلم مع ال hardware ولا هو موجود تحت كدا .. لكن المنوط بيه من الحكاية دي هو الـ Kernel فلازم ال Kernel يفهم الارقام دي.


طيب هل بيفهمها فعلا ؟ الاجابه لآ عشان كدا Kubernetes بيعمل ترجمة للأرقام دي لصيغة تانية بيفهمها ال Kernel والي بيترجمها لما يسمي بالـ CPU shares دا في ال version الأول من cgroup او ال weight في ال cgroup version 2 هنتكلم عن دا كمان شويه.

المهم ان فيه حاجتين مهمين بيحصلوا هنا كل حاجه منهم مسوول عنها ال Kernel والتانية مسؤول عنها ال CRI او ال Container runtime ودي الي بيقوم عليها ال container بتاعتك زي Docker للتقريب لان فيه أنواع تانية كتير.

  • ال Kernel هو الي بيعمل ال crgroups في linux ودي ال interface الي فيها التعليمات الي بتقول ازاي هيحصل resouce allocation لل process دي الي هي ال container في حالتنا
  • ال CRI بتحط المعلومات عن كل POD تحت cgroup directory وده بيكون بلغة يفهمها ال Kernel.

طيب السؤال هنا هل لما بتقول ل Kubernetes أنا ال request بتاعي 1 Core ولا نص Core هيديك فعلًا ال النص Core او الكور ملاكي ؟ لو فاهم كدا فالمقال دا ليك لانها مبتتحسبش كدا.

أولًا محتاجين نفهم ازاي ال Kernel بيوزع ال CPUs علي ال Process الأول قبل ما نرجع تاني ل Kubernetes عشان نربط الدنيا كويس ال Linux scheduler عادة بيتسخدم توزيع وقت ال CPU وليس حجز ال Cores بالكامل الا لو حصل اسثتناءات فهو بيوزع وقت ال cpu علي ال process بشكل عادل (مش متساوي) ودا عن طريق مفهوم weight الي ذكرناه فوق سريعا في cgroup v2 يعني علي حسب ال weight بتاعك هتاخد حصص من وقت ال CPU والي بطبيعه الحال بتسمع في ال performance بتاع ال application بتاعك ودا طبقًا ل CFS او ال completely fair scheduler

طيب ايه الاستثناءات؟ أنت تقدر تحجز فعلا Core كامل لبروسيس ملاكي عن طريق cpuset هيسيب لك مصدر تعرف منه اكتر عن ال cpuset


CPU Manager Policy

طيب نرجع تاني ل Kubernetes بعد ما عرفنا الدنيا بتمشي ازاي تحت. ال Kubernetes عنده اتنين من ال CPU manager policy وهما واحده Default وواحده exclusive جدا:

None

يعني مفيش تعليمات وهنا هتتعامل طبقًا لل QOS الي اتكلمنا عنها في الاول الا في استثناء وحيد. المهم أنت ال application بتاعك هياخد حصه من ال CPU shares في حالات التنافس او الضغط وهوضح دا بعد شوية طيب ايه الاستثناء؟

فاكر ال Guaranteed profile دا في الطبيعي زيه زي غيره هياخد cpu times الا لو الرقم الي حطيته في ال cpu request هو رقم صحيح ( 1، 2 ، 3 ) هنا هتاخد كور حصري غير كدا فانت موجود في ال shared pool وال shared pool فيه كل ال cores بعد ما نشيل منه المحجوز لل os وال kubelet وله كورز حصرية.

Static

وهنا انت بتقول ل Kubernetes عايزين Cores حصرية ومتستخدمش دي ابدًا الا لو فعلًا فاهم بتعمل ايه وال application بتاعك فعلًا عنده او محتاج Load ثابت وهنا دا بيترجم ل cpuset وفي الحالة دي هتاخد Cores مفيش processes تانية هتقرب ليها او ما يعرف بال Noisy neighbors.

حلو كدا فهمنا الدنيا بتتشغل ازاي بشكل مبدئي وهو ان ال default cpu manager في Kubernetes بيتبع ال CSF.


Kubernetes Scheduler / CPU Requests Example

تعالوا ناخد مثال كدا لل Kubernetes scheduler ازاي بيحط ال Pod بتاعتك احيانا بتشوف عندك POD عطلانة في ال pending state دي ليها اسباب مختلفة أحدهم هو ال resource allocation ودا الي يهمنا هنا:

تخيل عندك Node فيها 3 cpu cores وعندك 3 applications:

  • أول application ليه cpu request 1.5
  • تاني application ليه cpu request 1
  • تالت application ليه cpu request 1

في الحالة دي الأول هيدخل والتاني لكن التالت هيفضل Pending لإن هيفضل نص core والمطلوب core كامل وهيفضل النص core دا فاضي ( لكن خليه معانا عشان هنتكلم عن النص كور الفاضي دا كمان شويه) هنا زي ما قولنا قبل كدا ال Kernel هيوزع ال CPU time بشكل عادل حسب وزن كل واحده ويضمن ان كل Pod تاخد shares علي قد الي طلبته.

لكن نفرض ان اول POD الي واخده 1.5 شغالة تمام لكن التانية الي واخده 1 جالها ضغط من ترافيك وعايز تتنفس اكتر هنا هتاخد CPU زياده من المتاح لحد ما توصل لل CPU limit دا لو محطوط ودا هتاخده من النص كور الي كان فاضل.

لكن ال Kernel هيمنع انها تشارك في حصة الأولى طيب افرض الاتنين بقى عليهم زحمة هنا النص core دا هيتوزع طبقًا لحصة كل واحدة فكدا الاول هتاخد 60 في المية من النص الي فاضل والتانيه هتاخد 40 في المية مثلًا.

طيب ايه الحال مع ال Limits هنا ال Limits مختلف اولًا هو مش ملزم لل scheduler يعني لو انت عامل ال request 1 لكن ال Limit 4 وال Node مفهاش ال 4 دا مش هيمنع ال running ولا ال scheduling الالزام فقط علي ال request وال limit مش مضمون لكن بيؤثر في الي تقدر توصله حسب المتاح في حالات الضغط يعني لو حاطط limit مش هتقدر تعديه لو تحت ضغط حتي لو فيه cpu متاح عشان كدا ال cpu limit كنت قولتلك فوق انها مش فكره كويسة انك تعمل CPU limits الا لو محتاج predictable cpu فالافضل لاغلب التطبيقات العادية انك تاخد حصة من المتاح وقت الضغط مع ضمان اخد الي طلبته.

كدا احنا فهمنا الدنيا بتشتغل ازاي، تعالي نتكلم شوية عن الأرقام؛ اتفقنا إن الأرقام الي أنت بتحطها في ال Spec دي لا يفقها ال Kernel، و Kubernetes محتاج يترجمها وبعدين تشتغل بال CFS لان ال Kernel زي ما قولنا ال Default هو بيوزع CPU time shares. وزي ما ذكرنا فوق ان في Linux فيه 2 versions من ال cgroup فـ Kubernetes بيسبورت الاتنين:

  • الأول (v1): بيشتغل بالـ shares للـ requests، وبالـ quota / period للـ limits.
  • التاني (v2): بيشتغل بالـ weight للـ requests، وبيترجم الـ limits لـ cpu.max (هنتكلم عنها بعد شويه).

إزاي بنحسب الـ Weight والـ Shares؟

طيب الـ shares بيتحسب ازاي من الأرقام الي انت بتدخلها لـ K8s resource specs؟

تخيل عندنا سيرفس الـ request cpu بتاعها 100m، المفروض الكلام دا يتحول لـ shares في حالة cgroup v1 و weight في cgroup v2.

أولاً: بنحسب الـ shares (لأننا هنستخدمه في حساب الـ weight): الـ formula عباره عن: cgroup_shares = (milliCPU * 1024) / 1000

بعد كدا بناخد القيمه دي نعمل لها clamp ما بين 2 و 262144. الـ clamp مهم عشان نضمن ان الرقم الي هيطلع يفضل واقع دايما في الـ range الي نظام التشغيل مستعد يتعامل معاه ويحمي النظام من الـ overflow والـ starvation الخ.

الرقمين دول جم منين؟

  • الـ max (262144): دا حاصل ضرب الـ default cgroup shares (1024) في 256، ودا كان الـ upper bound للـ cpu cores وقت تصميم cgroup v1.
  • الـ min (2): هي الـ min value، فعمليا انت حتي لو حطيت request cpu 0 هتاخد 2 shares مش هتاخد 0. ليه 2 مش واحد في Linux CFS، قيمة weight أو cpu.shares مينفعش تبقى 0 أو 1 لأن ده يسبب arithmetic problems في حسابات الـ scheduler (زي division by zero أو إن vruntime يزيد بسرعة غير منطقية)، وكمان cfs_rq weight هي مجموع weights لكل الـ entities الموجودة على الـ runqueue، فلو weight صغيرة جدًا أو كبيرة جدًا الحسابات تفقد الدقة وfairness تتكسر. وعشان كده أقل قيمة آمنة اتحددت بـ 2، ومع إن الـ default weight هي 1024 مفيش أي practical limitation على الاستخدام، والاختيار ده كله هدفه يحافظ على stability المصدر:
linux/kernel/sched/sched.h at 2687c848e57820651b9f69d30c4710f4219f7dbf · torvalds/linux
Linux kernel source tree. Contribute to torvalds/linux development by creating an account on GitHub.

حالياً في الـ linux distros بتتعامل بـ cgroup v2 الي بتستخدم الـ weight، فلازم يحصل map المعادله الي بتحول الرقم دا:
weight = ((cpu.shares - 2) * 9999) / 262142 + 1
(هتلاقي الـ implementation دا للتحويل في اغلب الـ OCIs زي runc). مثال:

https://github.com/opencontainers/runc/blob/2664c845c9d271420cce5c430546d02e378a2b3b/libcontainer/cgroups/utils.go#L409


تأثير الـ Weight والـ Virtual Time


ملاحظه: دايما الـ weight بيتاخد في الحسبان في حالات الضغط والتنافس بين الـ processes بس، لكن لو انت مشغل process واحده هتاخد من الـ host اكتر لحد الـ limit طالما محتاجه.

فالـ weight بيقول للـ scheduler الـ process تاخد قد ايه من وقت الـ cpu مقارنه بباقي الـ processes.

مثال للتبيسط: لو container الـ weight بتاعها 1024 والتانيه 2048، فالمتوقع ان التانية تاخد ضعف الاولى من وقت الـ cpu لو الاتنين محتاجين.

الـ scheduler عشان ينظم العمليه دي بيستخدم الـ virtual time، وهي متغيرة ومتآثرة بالـ weight
vruntime += actual_runtime * (1024 / process_weight)

فكل ما كان الـ process weight او التاسك أكبر، كل ما فرصتها زادت إن الـ cpu يختار يعمل لها run في الـ cycle عن الـ CPU Limit.

أخيرًا الـ cpu limit الي بتترجم لـ max زي ما قولنا هي مختلفه؛ فهي مش بتتآثر بالـ contention، هي بيحصل لها enforce بغض النظر عن فيه cpu free ولا لأ.


الخلاصة

الخلاصة الي أنت محتاجها بدون كل التفاصيل دي

  1. انت بتتعامل مع cpu time مش dedicated cores في الوضع الطبيعي
  2. فهمك لل QOS بيخليك تشوف ايه المناسب لل workload بتاعتك وطبيعتها
  3. لو الكود بتاعك فيه حساب ال cores عشان تنظم اللوجيك عندك فال limits مهم عشان تاخد predictable number لان بدون limit ال container بي assume ان معاها كل ال cores المتاحه ودا مش حقيقي زي ما وضحنا
  4. ال cpu limits مش دايما فكره كويسه بالعكس في اغلب الاحوال متعملش cpu limits

بعض المصادر

Control CPU Management Policies on the Node
FEATURE STATE: Kubernetes v1.26 [stable] Kubernetes keeps many aspects of how pods execute on nodes abstracted from the user. This is by design. However, some workloads require stronger guarantees in terms of latency and/or performance in order to operate acceptably. The kubelet provides methods to enable more complex workload placement policies while keeping the abstraction free from explicit placement directives. For detailed information on resource management, please refer to the Resource Management for Pods and Containers documentation.
Resource Management for Pods and Containers
When you specify a Pod, you can optionally specify how much of each resource a container needs. The most common resources to specify are CPU and memory (RAM); there are others. When you specify the resource request for containers in a Pod, the kube-scheduler uses this information to decide which node to place the Pod on. When you specify a resource limit for a container, the kubelet enforces those limits so that the running container is not allowed to use more of that resource than the limit you set.
cpuset(7) - Linux manual page
sched_setaffinity(2) - Linux manual page
cgroups(7) - Linux manual page